Đưa ra ví dụ và hướng dẫn về cách tạo hiệu ứng xúc giác tuỳ chỉnh trong Android, bao gồm cả các thành phần được cố định theo dòng thời gian bằng VibrationEffect.Builder, các mẫu rung tuỳ chỉnh và các phong bì dạng sóng nâng cao. từ khoá_công khai: > Android, xúc giác, hiệu ứng tuỳ chỉnh, rung, API xúc giác, mẫu rung, VibrationEffect.Builder, thành phần, nguyên tắc xúc giác, phong bì dạng sóng, giao diện người dùng
Trang này trình bày các ví dụ về cách sử dụng nhiều API phản hồi xúc giác để tạo các hiệu ứng tuỳ chỉnh ngoài dạng sóng rung tiêu chuẩn trong một ứng dụng Android.
Trang này có các ví dụ sau:
- Thành phần được cố định trên dòng thời gian bằng
VibrationEffect.Builder- Tạo bằng các chế độ đặt sẵn: Sắp xếp theo trình tự các cảm giác xúc giác được xác định trước.
- Compose bằng phong bì và chế độ đặt sẵn: Kết hợp phong bì và chế độ đặt sẵn dọc theo một dòng thời gian.
- Sử dụng lại và di chuyển sự kiện: Di chuyển và sử dụng lại các sự kiện thành phần hiện có.
- Thành phần lặp lại: Tạo hiệu ứng lặp lại trên dòng thời gian.
- Chế độ rung tuỳ chỉnh
- Mẫu tăng tốc: Mẫu bắt đầu một cách mượt mà.
- Mẫu lặp lại: Mẫu không có điểm kết thúc.
- Mẫu có cơ chế dự phòng: Bản minh hoạ cơ chế dự phòng.
- Thành phần nguyên thuỷ của chế độ rung
- Dạng sóng rung có bao sóng
- Lò xo bật: Hiệu ứng lò xo bật bằng các hiệu ứng phong bì cơ bản.
- Phóng tên lửa: Hiệu ứng phóng tên lửa sử dụng hiệu ứng bao sóng.
Để xem thêm ví dụ, hãy xem phần Thêm phản hồi xúc giác vào sự kiện và luôn tuân theo các nguyên tắc thiết kế về phản hồi xúc giác.
Sử dụng các giải pháp dự phòng để xử lý khả năng tương thích của thiết bị
Khi triển khai các hiệu ứng xúc giác tuỳ chỉnh, khả năng tương thích của thiết bị và hành vi dự phòng sẽ phụ thuộc vào giao diện API mà bạn chọn:
VibrationEffect.Builder(Nên dùng): Kể từ Android 16 (Quý 4 năm 2026), các hiệu ứng được tạo bằngVibrationEffect.Buildersẽ bao gồm cơ chế dự phòng tự động ở cấp khung. Nếu một thiết bị không hỗ trợ tự nhiênPresethoặcEnvelopecơ bản theo yêu cầu, thì khung sẽ tự động dịch yêu cầu đó thành một lựa chọn thay thế phù hợp tại thời điểm phát với nỗ lực cao nhất. Bạn không cần kiểm tra thủ công các chức năng của từng thiết bị nguyên thuỷ trước khi phát các hiệu ứng được tạo bằngVibrationEffect.Builder.- Trường hợp ngoại lệ: Các đường bao dạng sóng nâng cao được tạo bằng
WaveformEnvelopeBuilderkhông hỗ trợ cơ chế dự phòng tự động vì chúng phụ thuộc vào các ánh xạ tần số phần cứng cụ thể (FOAM). Nếu không được hỗ trợ, các tệp này sẽ không phát.
- Trường hợp ngoại lệ: Các đường bao dạng sóng nâng cao được tạo bằng
VibrationEffect.Composition: Các thành phần được tạo bằng APIstartComposition()không có cơ chế dự phòng tự động. Nếu một thành phần chứa dù chỉ một thành phần cơ bản không được hỗ trợ, thì toàn bộ mẫu rung sẽ không phát được. Bạn phải kiểm tra các chức năng theo cách thủ công bằng cách sử dụngvibrator.arePrimitivesSupported().- Dạng sóng có chế độ kiểm soát độ lớn: Độ lớn khác 0 sẽ được làm tròn lên 100% trên những thiết bị không có chế độ kiểm soát độ lớn. Kiểm tra
vibrator.hasAmplitudeControl()và quay lại mẫu BẬT/TẮT được thiết kế rõ ràng nếu cần.
Sử dụng các thành phần cơ bản về xúc giác
Android có một số nguyên hàm phản hồi xúc giác có biên độ và tần số khác nhau. Bạn có thể chỉ dùng một nguyên tắc hoặc kết hợp nhiều nguyên tắc để đạt được hiệu ứng xúc giác phong phú.
- Sử dụng độ trễ từ 50 mili giây trở lên để tạo khoảng cách rõ ràng giữa hai thành phần cơ bản, đồng thời tính đến thời lượng của thành phần cơ bản nếu có thể.
- Sử dụng các thang có tỷ lệ khác nhau từ 1,4 trở lên để cảm nhận rõ hơn sự khác biệt về cường độ.
Sử dụng các tỷ lệ 0,5, 0,7 và 1,0 để tạo phiên bản có cường độ thấp, trung bình và cao của một thành phần cơ bản.
Tạo kiểu rung tuỳ chỉnh
Các mẫu rung thường được dùng trong chế độ xúc giác chú ý, chẳng hạn như thông báo và nhạc chuông. Dịch vụ Vibrator có thể phát các mẫu rung dài làm thay đổi biên độ rung theo thời gian. Những hiệu ứng như vậy được gọi là dạng sóng.
Người dùng thường cảm nhận được hiệu ứng dạng sóng, nhưng những rung động dài đột ngột có thể khiến người dùng giật mình nếu được phát trong môi trường yên tĩnh. Việc tăng biên độ lên mức mục tiêu quá nhanh cũng có thể tạo ra tiếng ồn ù ù có thể nghe thấy. Thiết kế các mẫu dạng sóng để làm mượt các chuyển đổi biên độ nhằm tạo hiệu ứng tăng và giảm.
Ví dụ về kiểu rung
Các phần sau đây cung cấp một số ví dụ về các mẫu rung:
Mẫu tăng dần số lượng
Dạng sóng được biểu thị dưới dạng VibrationEffect với 3 tham số:
- Timings:một mảng gồm các khoảng thời gian (tính bằng mili giây) cho từng đoạn sóng.
- Cường độ: cường độ rung mong muốn cho mỗi khoảng thời gian được chỉ định trong đối số đầu tiên, được biểu thị bằng một giá trị số nguyên từ 0 đến 255, trong đó 0 biểu thị "trạng thái tắt" của bộ rung và 255 là cường độ tối đa của thiết bị.
- Chỉ mục lặp lại: chỉ mục trong mảng được chỉ định trong đối số đầu tiên để bắt đầu lặp lại dạng sóng hoặc -1 nếu chỉ phát mẫu một lần.
Dưới đây là ví dụ về dạng sóng nhấp nháy 2 lần với khoảng dừng 350 mili giây giữa các lần nhấp nháy. Xung đầu tiên là một đường dốc mượt mà lên đến biên độ tối đa và xung thứ hai là một đường dốc nhanh để giữ biên độ tối đa. Việc dừng ở cuối được xác định bằng giá trị chỉ mục lặp lại âm.
Kotlin
val timings: LongArray = longArrayOf(
50, 50, 50, 50, 50, 100, 350, 25, 25, 25, 25, 200)
val amplitudes: IntArray = intArrayOf(
33, 51, 75, 113, 170, 255, 0, 38, 62, 100, 160, 255)
val repeatIndex = -1 // Don't repeat.
vibrator.vibrate(VibrationEffect.createWaveform(
timings, amplitudes, repeatIndex))
Java
long[] timings = new long[] {
50, 50, 50, 50, 50, 100, 350, 25, 25, 25, 25, 200 };
int[] amplitudes = new int[] {
33, 51, 75, 113, 170, 255, 0, 38, 62, 100, 160, 255 };
int repeatIndex = -1; // Don't repeat.
vibrator.vibrate(VibrationEffect.createWaveform(
timings, amplitudes, repeatIndex));
Mẫu lặp lại
Bạn cũng có thể phát lặp lại dạng sóng cho đến khi huỷ. Cách tạo dạng sóng lặp lại là đặt tham số repeat không âm. Khi bạn phát một dạng sóng lặp lại, chế độ rung sẽ tiếp tục cho đến khi bị huỷ rõ ràng trong dịch vụ:
Kotlin
void startVibrating() {
val timings: LongArray = longArrayOf(50, 50, 100, 50, 50)
val amplitudes: IntArray = intArrayOf(64, 128, 255, 128, 64)
val repeat = 1 // Repeat from the second entry, index = 1.
VibrationEffect repeatingEffect = VibrationEffect.createWaveform(
timings, amplitudes, repeat)
// repeatingEffect can be used in multiple places.
vibrator.vibrate(repeatingEffect)
}
void stopVibrating() {
vibrator.cancel()
}
Java
void startVibrating() {
long[] timings = new long[] { 50, 50, 100, 50, 50 };
int[] amplitudes = new int[] { 64, 128, 255, 128, 64 };
int repeat = 1; // Repeat from the second entry, index = 1.
VibrationEffect repeatingEffect = VibrationEffect.createWaveform(
timings, amplitudes, repeat);
// repeatingEffect can be used in multiple places.
vibrator.vibrate(repeatingEffect);
}
void stopVibrating() {
vibrator.cancel();
}
Điều này rất hữu ích cho các sự kiện không liên tục yêu cầu người dùng thực hiện hành động để xác nhận. Ví dụ về các sự kiện như vậy bao gồm cuộc gọi điện thoại đến và chuông báo được kích hoạt.
Mẫu có cơ chế dự phòng
Kiểm soát biên độ rung là một khả năng phụ thuộc vào phần cứng. Việc phát dạng sóng trên một thiết bị tầm thấp mà không có khả năng này sẽ khiến thiết bị rung ở biên độ tối đa cho mỗi mục nhập dương trong mảng biên độ. Nếu ứng dụng của bạn cần hỗ trợ các thiết bị như vậy, hãy sử dụng một mẫu không tạo ra hiệu ứng rung khi phát trong điều kiện đó hoặc thiết kế một mẫu BẬT/TẮT đơn giản hơn có thể phát thay thế.
Kotlin
if (vibrator.hasAmplitudeControl()) {
vibrator.vibrate(VibrationEffect.createWaveform(
smoothTimings, amplitudes, smoothRepeatIdx))
} else {
vibrator.vibrate(VibrationEffect.createWaveform(
onOffTimings, onOffRepeatIdx))
}
Java
if (vibrator.hasAmplitudeControl()) {
vibrator.vibrate(VibrationEffect.createWaveform(
smoothTimings, amplitudes, smoothRepeatIdx));
} else {
vibrator.vibrate(VibrationEffect.createWaveform(
onOffTimings, onOffRepeatIdx));
}
Các thành phần được cố định trên dòng thời gian bằng VibrationEffect.Builder
Kể từ Android 16 (26Q4), VibrationEffect.Builder là API ưu tiên để tạo các hiệu ứng và thành phần rung phức tạp. API này cho phép bạn tạo ra những cảm giác xúc giác biểu cảm bằng cách sắp xếp các phần tử xúc giác rời rạc dọc theo một dòng thời gian tuyệt đối bằng startTimeMillis.
VibrationEffect.Builder hỗ trợ kết hợp nhiều loại chế độ rung:
- Cài đặt sẵn: Các xung phản hồi xúc giác được xác định trước (
VibrationEffect.Preset) như lượt nhấp và lượt đánh dấu. - Đường bao: Dạng sóng liên tục linh hoạt, bao gồm đường bao cơ bản không phụ thuộc vào phần cứng (
BasicEnvelopeBuilder) và đường bao điều biến tần số nâng cao (WaveformEnvelopeBuilder). - VibrationEffect hiện có: Thành phần nguyên thuỷ (
VibrationEffect.Composition), dạng sóng theo bước (VibrationEffect.createWaveform), hiệu ứng được xác định trước (VibrationEffect.createPredefined) và hiệu ứng một lần (VibrationEffect.createOneShot) được nhập bằngaddEvents()hoặc sao chép hàm khởi tạo. - Trình tự lặp lại: Các mẫu liên tục được định cấu hình bằng
setRepeatingEffect().
Các chế độ rung được tạo bằng VibrationEffect.Builder có sẵn cơ chế dự phòng tự động ở cấp khung trên các phần tử (bao gồm cả chế độ đặt sẵn, bao thư cơ bản và hiệu ứng rung kết hợp), đảm bảo trải nghiệm nhất quán cho người dùng trên nhiều thiết bị mà không cần kiểm tra khả năng theo cách thủ công. (Các đường bao dạng sóng nâng cao được tạo bằng WaveformEnvelopeBuilder yêu cầu phần cứng hỗ trợ và không có tính năng hỗ trợ dự phòng tự động.)
Tạo bằng chế độ đặt sẵn
Sử dụng VibrationEffect.Preset để thêm các xung phản hồi xúc giác ngắn, phổ biến và được xác định trước (chẳng hạn như PRESET_CLICK, PRESET_TICK hoặc PRESET_LOW_TICK) vào một thành phần.
Nhà sản xuất thiết bị triển khai các chế độ đặt sẵn để mang đến chế độ rung rõ ràng, ngắn và dễ chịu, phù hợp với các nguyên tắc về phản hồi xúc giác để có phản hồi xúc giác rõ ràng. Để biết thêm thông tin chi tiết về các chức năng này và cách chúng hoạt động, hãy xem bài viết Sơ lược về bộ truyền động rung.
Các chế độ đặt sẵn sẽ thay thế các thành phần cơ bản ngắn trong API VibrationEffect.Composition.
Đối với những cảm giác xúc giác kéo dài hoặc liên tục (chẳng hạn như tăng và giảm dần), hãy sử dụng dạng sóng bao (PWLE).
Bạn có thể chỉ định cho mỗi chế độ đặt sẵn một tỷ lệ tuỳ chọn từ 0.0f đến 1.0f và được đặt tại một thời gian bắt đầu cụ thể (tính bằng mili giây) kể từ khi bắt đầu thành phần.
Kotlin
val clickPreset = VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_CLICK, /* scale= */ 0.8f
)
val tickPreset = VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_TICK, /* scale= */ 0.5f
)
val effect = VibrationEffect.Builder()
.addPreset(/* startTimeMillis= */ 0L, clickPreset)
.addPreset(/* startTimeMillis= */ 100L, tickPreset)
.build()
vibrator.vibrate(effect)
Java
VibrationEffect.Preset clickPreset = VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_CLICK, /* scale= */ 0.8f
);
VibrationEffect.Preset tickPreset = VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_TICK, /* scale= */ 0.5f
);
VibrationEffect effect = new VibrationEffect.Builder()
.addPreset(/* startTimeMillis= */ 0L, clickPreset)
.addPreset(/* startTimeMillis= */ 100L, tickPreset)
.build();
vibrator.vibrate(effect);
Soạn nhạc bằng phong bì và chế độ đặt sẵn
Bạn có thể kết hợp liền mạch các thực thể VibrationEffect.Envelope (được tạo bằng BasicEnvelopeBuilder hoặc WaveformEnvelopeBuilder) với các chế độ đặt sẵn để tạo ra các mẫu xúc giác đa dạng, nhiều phân đoạn.
Sau đây là ví dụ về một phong bì tăng dần và giảm dần mượt mà, theo sau là một chế độ cài đặt sẵn cho tiếng nhấp chuột rõ ràng:
Kotlin
val basicEnvelope = VibrationEffect.Envelope.create(
VibrationEffect.BasicEnvelopeBuilder()
.setInitialSharpness(0.0f)
.addControlPoint(1.0f, 1.0f, 300L)
.addControlPoint(0.0f, 0.5f, 100L)
)
val clickPreset = VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_CLICK, 0.9f
)
val mixedEffect = VibrationEffect.Builder()
.addEnvelope(/* startTimeMillis= */ 0L, basicEnvelope)
.addPreset(/* startTimeMillis= */ 450L, clickPreset)
.build()
vibrator.vibrate(mixedEffect)
Java
VibrationEffect.Envelope basicEnvelope = VibrationEffect.Envelope.create(
new VibrationEffect.BasicEnvelopeBuilder()
.setInitialSharpness(0.0f)
.addControlPoint(1.0f, 1.0f, 300L)
.addControlPoint(0.0f, 0.5f, 100L)
);
VibrationEffect.Preset clickPreset = VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_CLICK, 0.9f
);
VibrationEffect mixedEffect = new VibrationEffect.Builder()
.addEnvelope(/* startTimeMillis= */ 0L, basicEnvelope)
.addPreset(/* startTimeMillis= */ 450L, clickPreset)
.build();
vibrator.vibrate(mixedEffect);
Sử dụng lại và di chuyển sự kiện hiện có
Để sử dụng lại hoặc nối một VibrationEffect hiện có (bao gồm cả VibrationEffect.Composition), hãy truy xuất danh sách các đối tượng VibrationEffect.Event bằng cách sử dụng getEvents() và nối các đối tượng đó bằng một độ lệch bằng cách sử dụng addEvents(startTimeShiftMillis, events) (hoặc truyền hiệu ứng trực tiếp đến hàm khởi tạo VibrationEffect.Builder(effect)). Khi nhập các thực thể VibrationEffect.Composition theo cách này, khung sẽ tự động chuyển đổi các nguyên tắc của chúng thành các giá trị đặt sẵn, cho phép hỗ trợ dự phòng thời gian chạy.
Kotlin
val existingEffect = VibrationEffect.Builder()
.addPreset(
0L,
VibrationEffect.Preset.create(VibrationEffect.Preset.PRESET_CLICK)
)
.addPreset(
80L,
VibrationEffect.Preset.create(VibrationEffect.Preset.PRESET_TICK)
)
.build()
// Shift and append the existing events 200ms into the new composition.
val combinedEffect = VibrationEffect.Builder()
.addEvents(/* startTimeShiftMillis= */ 200L, existingEffect.events)
.build()
vibrator.vibrate(combinedEffect)
Java
VibrationEffect existingEffect = new VibrationEffect.Builder()
.addPreset(
0L,
VibrationEffect.Preset.create(VibrationEffect.Preset.PRESET_CLICK)
)
.addPreset(
80L,
VibrationEffect.Preset.create(VibrationEffect.Preset.PRESET_TICK)
)
.build();
// Shift and append the existing events 200ms into the new composition.
VibrationEffect combinedEffect = new VibrationEffect.Builder()
.addEvents(/* startTimeShiftMillis= */ 200L, existingEffect.getEvents())
.build();
vibrator.vibrate(combinedEffect);
Tạo các thành phần lặp lại
Sử dụng setRepeatingEffect(startTimeMillis, repeatingEffect, durationMillis) để thêm một mẫu lặp lại vào một thành phần:
Kotlin
val repeatingPattern = VibrationEffect.Builder()
.addPreset(
0L,
VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_CLICK, 1.0f
)
)
.addPreset(
150L,
VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_LOW_TICK, 0.6f
)
)
.build()
val repeatingEffect = VibrationEffect.Builder()
.setRepeatingEffect(
/* startTimeMillis= */ 0L,
/* effect= */ repeatingPattern,
/* durationMillis= */ 300L
)
.build()
vibrator.vibrate(repeatingEffect)
Java
VibrationEffect repeatingPattern = new VibrationEffect.Builder()
.addPreset(
0L,
VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_CLICK, 1.0f
)
)
.addPreset(
150L,
VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_LOW_TICK, 0.6f
)
)
.build();
VibrationEffect repeatingEffect = new VibrationEffect.Builder()
.setRepeatingEffect(
/* startTimeMillis= */ 0L,
/* effect= */ repeatingPattern,
/* durationMillis= */ 300L
)
.build();
vibrator.vibrate(repeatingEffect);
Thời gian, xác thực và quản lý độ lệch
Khi thiết kế thành phần bằng VibrationEffect.Builder, hãy lưu ý các quy tắc xác thực và thời gian sau:
- Thời gian bắt đầu tăng dần: Mọi phần tử được thêm vào trình tạo đều phải có
startTimeMillislớn hơn hoặc bằng thời gian bắt đầu của phần tử trước đó. - Xác thực trong thời gian xây dựng: Trình tạo thực hiện quy trình xác thực tốt nhất tại thời điểm
build()bằng cách sử dụng khoảng thời gian đã biết của phần tử (hoặc tối thiểu 1 mili giây đối với các chế độ đặt sẵn). Nếu phát hiện thấy một khoảng thời gian trùng lặp không thể xảy ra, thì phương thức này sẽ gửi mộtIllegalArgumentException. - Phát tuần tự: Nếu phần tử rung trước đó vẫn đang phát khi thời gian bắt đầu của phần tử tiếp theo đến, thì khung sẽ tự động chuyển phần tử tiếp theo sang vị trí thời gian sớm nhất có thể. Điều này đảm bảo rằng các sự kiện không chồng chéo và không có rung động nào bị bỏ qua, mặc dù có thể gây ra độ lệch thời gian nhỏ nếu các sự kiện được lên lịch quá gần nhau. Để giảm thiểu hiện tượng trôi, hãy dành đủ thời gian (ví dụ: 50 mili giây trở lên) giữa các sự kiện xúc giác liên tiếp.
Tạo thành phần nguyên thuỷ rung
Phần này trình bày cách tạo các mẫu rung bằng VibrationEffect.Composition. Các nguyên tắc cơ bản về thành phần (được mô tả trước đó trên trang này) do nhà sản xuất thiết bị triển khai. Chúng cung cấp một chế độ rung rõ ràng, ngắn gọn và dễ chịu, phù hợp với các nguyên tắc về phản hồi xúc giác để mang lại phản hồi xúc giác rõ ràng. Để biết thêm thông tin chi tiết về các chức năng này và cách chúng hoạt động, hãy xem bài viết Sơ lược về bộ truyền động rung.
Không giống như VibrationEffect.Builder, API VibrationEffect.Composition không có các phương án dự phòng tự động cho các thành phần cơ bản không được hỗ trợ. Vì vậy:
Trước khi kích hoạt chế độ phản hồi xúc giác nâng cao, hãy kiểm tra để đảm bảo rằng một thiết bị nhất định hỗ trợ tất cả các thành phần cơ bản mà bạn đang sử dụng.
Tắt bộ trải nghiệm nhất quán không được hỗ trợ, chứ không chỉ những hiệu ứng thiếu một thành phần cơ bản.
- Kotlin:
val fallbackEffect = VibrationEffect.Builder(compositionEffect).build()(hoặc.addEvents(0L, compositionEffect.events)) - Java:
VibrationEffect fallbackEffect =new VibrationEffect.Builder(compositionEffect).build();(hoặc.addEvents(0L, compositionEffect.getEvents()))
Khi được tạo bằng VibrationEffect.Builder, khung sẽ chuyển đổi các thành phần kết hợp thành các chế độ đặt sẵn và tự động cung cấp giải pháp dự phòng trong thời gian chạy nếu có thành phần nào không được hỗ trợ trên thiết bị của người dùng.
Hiệu ứng rung kết hợp
Bạn có thể tạo các hiệu ứng rung kết hợp bằng VibrationEffect.Composition. Dưới đây là ví dụ về hiệu ứng tăng dần chậm, sau đó là hiệu ứng nhấp chuột mạnh:
Kotlin
vibrator.vibrate(
VibrationEffect.startComposition().addPrimitive(
VibrationEffect.Composition.PRIMITIVE_SLOW_RISE
).addPrimitive(
VibrationEffect.Composition.PRIMITIVE_CLICK
).compose()
)
Java
vibrator.vibrate(
VibrationEffect.startComposition()
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_SLOW_RISE)
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
.compose());
Một thành phần được tạo bằng cách thêm các thành phần cơ bản để phát theo trình tự. Mỗi thành phần cơ bản cũng có thể mở rộng, vì vậy, bạn có thể kiểm soát biên độ rung do từng thành phần tạo ra. Thang đo được xác định là một giá trị từ 0 đến 1, trong đó 0 thực sự tương ứng với biên độ tối thiểu mà người dùng có thể (hầu như) cảm nhận được nguyên tắc này.
Tạo các biến thể trong các thành phần rung cơ bản
Nếu bạn muốn tạo một phiên bản yếu và một phiên bản mạnh của cùng một thành phần cơ bản, hãy tạo tỷ lệ sức mạnh từ 1, 4 trở lên để có thể dễ dàng nhận thấy sự khác biệt về cường độ. Đừng cố gắng tạo nhiều hơn 3 mức cường độ của cùng một thành phần cơ bản, vì chúng không khác biệt về cảm quan. Ví dụ: sử dụng các tỷ lệ 0, 5, 0, 7 và 1, 0 để tạo các phiên bản có cường độ thấp, trung bình và cao của một thành phần cơ bản.
Thêm khoảng trống giữa các thành phần rung cơ bản
Thành phần này cũng có thể chỉ định độ trễ cần thêm giữa các thành phần cơ bản liên tiếp. Độ trễ này được biểu thị bằng mili giây kể từ khi kết thúc thành phần cơ bản trước đó. Nhìn chung, khoảng trống từ 5 đến 10 mili giây giữa hai thành phần cơ bản là quá ngắn để có thể phát hiện. Sử dụng khoảng trống có độ dài từ 50 mili giây trở lên nếu bạn muốn tạo khoảng trống có thể phân biệt được giữa hai thành phần cơ bản. Dưới đây là ví dụ về một thành phần kết hợp có độ trễ:
Kotlin
val delayMs = 100
vibrator.vibrate(
VibrationEffect.startComposition().addPrimitive(
VibrationEffect.Composition.PRIMITIVE_SPIN, 0.8f
).addPrimitive(
VibrationEffect.Composition.PRIMITIVE_SPIN, 0.6f
).addPrimitive(
VibrationEffect.Composition.PRIMITIVE_THUD, 1.0f, delayMs
).compose()
)
Java
int delayMs = 100;
vibrator.vibrate(
VibrationEffect.startComposition()
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_SPIN, 0.8f)
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_SPIN, 0.6f)
.addPrimitive(
VibrationEffect.Composition.PRIMITIVE_THUD, 1.0f, delayMs)
.compose());
Kiểm tra xem những thành phần cơ bản nào được hỗ trợ
Bạn có thể dùng các API sau để xác minh khả năng hỗ trợ thiết bị cho các thành phần cơ bản cụ thể:
Kotlin
val primitive = VibrationEffect.Composition.PRIMITIVE_LOW_TICK
if (vibrator.areAllPrimitivesSupported(primitive)) {
vibrator.vibrate(VibrationEffect.startComposition()
.addPrimitive(primitive).compose())
} else {
// Play a predefined effect or custom pattern as a fallback.
}
Java
int primitive = VibrationEffect.Composition.PRIMITIVE_LOW_TICK;
if (vibrator.areAllPrimitivesSupported(primitive)) {
vibrator.vibrate(VibrationEffect.startComposition()
.addPrimitive(primitive).compose());
} else {
// Play a predefined effect or custom pattern as a fallback.
}
Bạn cũng có thể kiểm tra nhiều thành phần cơ bản rồi quyết định thành phần nào cần kết hợp dựa trên mức độ hỗ trợ của thiết bị:
Kotlin
val effects: IntArray = intArrayOf(
VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
VibrationEffect.Composition.PRIMITIVE_TICK,
VibrationEffect.Composition.PRIMITIVE_CLICK
)
val supported: BooleanArray = vibrator.arePrimitivesSupported(primitives)
Java
int[] primitives = new int[] {
VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
VibrationEffect.Composition.PRIMITIVE_TICK,
VibrationEffect.Composition.PRIMITIVE_CLICK
};
boolean[] supported = vibrator.arePrimitivesSupported(effects);
Ví dụ về các thành phần rung
Các phần sau đây cung cấp một số ví dụ về thành phần rung, lấy từ ứng dụng mẫu về phản hồi xúc giác trên GitHub.
Chống lại (với số lượng đánh dấu thấp)
Bạn có thể kiểm soát biên độ của chế độ rung cơ bản để truyền tải thông tin phản hồi hữu ích cho một hành động đang diễn ra. Bạn có thể dùng các giá trị tỷ lệ có khoảng cách gần nhau để tạo hiệu ứng tăng dần mượt mà cho một thành phần cơ bản. Độ trễ giữa các nguyên hàm liên tiếp cũng có thể được đặt linh động dựa trên lượt tương tác của người dùng. Điều này được minh hoạ trong ví dụ sau về một ảnh động của khung hiển thị do cử chỉ kéo điều khiển và được tăng cường bằng phản hồi xúc giác.
Hình 1. Dạng sóng này biểu thị gia tốc đầu ra của chế độ rung trên thiết bị.
Kotlin
@Composable
fun ResistScreen() {
// Control variables for the dragging of the indicator.
var isDragging by remember { mutableStateOf(false) }
var dragOffset by remember { mutableStateOf(0f) }
// Only vibrates while the user is dragging
if (isDragging) {
LaunchedEffect(Unit) {
// Continuously run the effect for vibration to occur even when the view
// is not being drawn, when user stops dragging midway through gesture.
while (true) {
// Calculate the interval inversely proportional to the drag offset.
val vibrationInterval = calculateVibrationInterval(dragOffset)
// Calculate the scale directly proportional to the drag offset.
val vibrationScale = calculateVibrationScale(dragOffset)
delay(vibrationInterval)
vibrator.vibrate(
VibrationEffect.startComposition().addPrimitive(
VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
vibrationScale
).compose()
)
}
}
}
Screen() {
Column(
Modifier
.draggable(
orientation = Orientation.Vertical,
onDragStarted = {
isDragging = true
},
onDragStopped = {
isDragging = false
},
state = rememberDraggableState { delta ->
dragOffset += delta
}
)
) {
// Build the indicator UI based on how much the user has dragged it.
ResistIndicator(dragOffset)
}
}
}
Java
class DragListener implements View.OnTouchListener {
// Control variables for the dragging of the indicator.
private int startY;
private int vibrationInterval;
private float vibrationScale;
@Override
public boolean onTouch(View view, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startY = event.getRawY();
vibrationInterval = calculateVibrationInterval(0);
vibrationScale = calculateVibrationScale(0);
startVibration();
break;
case MotionEvent.ACTION_MOVE:
float dragOffset = event.getRawY() - startY;
// Calculate the interval inversely proportional to the drag offset.
vibrationInterval = calculateVibrationInterval(dragOffset);
// Calculate the scale directly proportional to the drag offset.
vibrationScale = calculateVibrationScale(dragOffset);
// Build the indicator UI based on how much the user has dragged it.
updateIndicator(dragOffset);
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
// Only vibrates while the user is dragging
cancelVibration();
break;
}
return true;
}
private void startVibration() {
vibrator.vibrate(
VibrationEffect.startComposition()
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
vibrationScale)
.compose());
// Continuously run the effect for vibration to occur even when the view
// is not being drawn, when user stops dragging midway through gesture.
handler.postDelayed(this::startVibration, vibrationInterval);
}
private void cancelVibration() {
handler.removeCallbacksAndMessages(null);
}
}
Mở rộng (có tăng và giảm)
Có hai nguyên hàm để tăng cường độ rung cảm nhận được: PRIMITIVE_QUICK_RISE và PRIMITIVE_SLOW_RISE. Cả hai đều đạt được cùng một mục tiêu, nhưng với thời lượng khác nhau. Chỉ có một dữ liệu nguyên thuỷ để giảm dần, PRIMITIVE_QUICK_FALL. Các thành phần cơ bản này hoạt động hiệu quả hơn khi kết hợp với nhau để tạo ra một đoạn sóng có cường độ tăng lên rồi giảm dần. Bạn có thể căn chỉnh các thành phần cơ bản được điều chỉnh tỷ lệ để tránh sự thay đổi đột ngột về biên độ giữa các thành phần đó. Điều này cũng giúp kéo dài thời lượng hiệu ứng tổng thể.
Về mặt cảm quan, mọi người luôn chú ý đến phần tăng nhiều hơn phần giảm, vì vậy, việc làm cho phần tăng ngắn hơn phần giảm có thể được dùng để chuyển sự chú ý sang phần giảm.
Sau đây là ví dụ về ứng dụng của thành phần này để mở rộng và thu gọn một vòng tròn. Hiệu ứng tăng có thể làm tăng cảm giác mở rộng trong quá trình tạo ảnh động. Sự kết hợp giữa hiệu ứng tăng và giảm giúp nhấn mạnh việc thu gọn ở cuối ảnh động.
Hình 2. Dạng sóng này biểu thị gia tốc đầu ra của chế độ rung trên thiết bị.
Kotlin
enum class ExpandShapeState {
Collapsed,
Expanded
}
@Composable
fun ExpandScreen() {
// Control variable for the state of the indicator.
var currentState by remember { mutableStateOf(ExpandShapeState.Collapsed) }
// Animation between expanded and collapsed states.
val transitionData = updateTransitionData(currentState)
Screen() {
Column(
Modifier
.clickable(
{
if (currentState == ExpandShapeState.Collapsed) {
currentState = ExpandShapeState.Expanded
vibrator.vibrate(
VibrationEffect.startComposition().addPrimitive(
VibrationEffect.Composition.PRIMITIVE_SLOW_RISE,
0.3f
).addPrimitive(
VibrationEffect.Composition.PRIMITIVE_QUICK_FALL,
0.3f
).compose()
)
} else {
currentState = ExpandShapeState.Collapsed
vibrator.vibrate(
VibrationEffect.startComposition().addPrimitive(
VibrationEffect.Composition.PRIMITIVE_SLOW_RISE
).compose()
)
}
)
) {
// Build the indicator UI based on the current state.
ExpandIndicator(transitionData)
}
}
}
Java
class ClickListener implements View.OnClickListener {
private final Animation expandAnimation;
private final Animation collapseAnimation;
private boolean isExpanded;
ClickListener(Context context) {
expandAnimation = AnimationUtils.loadAnimation(context, R.anim.expand);
expandAnimation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
vibrator.vibrate(
VibrationEffect.startComposition()
.addPrimitive(
VibrationEffect.Composition.PRIMITIVE_SLOW_RISE, 0.3f)
.addPrimitive(
VibrationEffect.Composition.PRIMITIVE_QUICK_FALL, 0.3f)
.compose());
}
});
collapseAnimation = AnimationUtils
.loadAnimation(context, R.anim.collapse);
collapseAnimation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
vibrator.vibrate(
VibrationEffect.startComposition()
.addPrimitive(
VibrationEffect.Composition.PRIMITIVE_SLOW_RISE)
.compose());
}
});
}
@Override
public void onClick(View view) {
view.startAnimation(isExpanded ? collapseAnimation : expandAnimation);
isExpanded = !isExpanded;
}
}
Lắc lư (có xoay)
Một trong những nguyên tắc quan trọng về phản hồi xúc giác là mang đến trải nghiệm thú vị cho người dùng. Một cách thú vị để giới thiệu hiệu ứng rung bất ngờ nhưng dễ chịu là sử dụng PRIMITIVE_SPIN. Nguyên tắc này hiệu quả nhất khi được gọi nhiều lần. Nhiều vòng quay được nối với nhau có thể tạo ra hiệu ứng rung lắc và không ổn định. Bạn có thể tăng cường hiệu ứng này hơn nữa bằng cách áp dụng một tỷ lệ thu phóng ngẫu nhiên cho từng thành phần cơ bản. Bạn cũng có thể thử nghiệm khoảng cách giữa các phần tử cơ bản xoay liên tiếp. Hai lần xoay mà không có khoảng trống (0 mili giây ở giữa) sẽ tạo cảm giác xoay nhanh. Việc tăng khoảng cách giữa các lần xoay từ 10 lên 50 mili giây sẽ tạo cảm giác xoay lỏng lẻo hơn và có thể được dùng để điều chỉnh cho phù hợp với thời lượng của video hoặc ảnh động.
Đừng sử dụng khoảng trống dài hơn 100 mili giây, vì các vòng quay liên tiếp sẽ không còn tích hợp tốt và bắt đầu có cảm giác như các hiệu ứng riêng lẻ.
Sau đây là ví dụ về một hình dạng đàn hồi bật trở lại sau khi bị kéo xuống rồi thả ra. Ảnh động được cải thiện bằng một cặp hiệu ứng xoay, phát với cường độ khác nhau tỷ lệ thuận với độ lệch của chuyển động nảy.
Hình 3. Dạng sóng này biểu thị gia tốc đầu ra của chế độ rung trên thiết bị.
Kotlin
@Composable
fun WobbleScreen() {
// Control variables for the dragging and animating state of the elastic.
var dragDistance by remember { mutableStateOf(0f) }
var isWobbling by remember { mutableStateOf(false) }
// Use drag distance to create an animated float value behaving like a spring.
val dragDistanceAnimated by animateFloatAsState(
targetValue = if (dragDistance > 0f) dragDistance else 0f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioHighBouncy,
stiffness = Spring.StiffnessMedium
),
)
if (isWobbling) {
LaunchedEffect(Unit) {
while (true) {
val displacement = dragDistanceAnimated / MAX_DRAG_DISTANCE
// Use some sort of minimum displacement so the final few frames
// of animation don't generate a vibration.
if (displacement > SPIN_MIN_DISPLACEMENT) {
vibrator.vibrate(
VibrationEffect.startComposition().addPrimitive(
VibrationEffect.Composition.PRIMITIVE_SPIN,
nextSpinScale(displacement)
).addPrimitive(
VibrationEffect.Composition.PRIMITIVE_SPIN,
nextSpinScale(displacement)
).compose()
)
}
// Delay the next check for a sufficient duration until the
// current composition finishes. Note that you can use
// Vibrator.getPrimitiveDurations API to calculcate the delay.
delay(VIBRATION_DURATION)
}
}
}
Box(
Modifier
.fillMaxSize()
.draggable(
onDragStopped = {
isWobbling = true
dragDistance = 0f
},
orientation = Orientation.Vertical,
state = rememberDraggableState { delta ->
isWobbling = false
dragDistance += delta
}
)
) {
// Draw the wobbling shape using the animated spring-like value.
WobbleShape(dragDistanceAnimated)
}
}
// Calculate a random scale for each spin to vary the full effect.
fun nextSpinScale(displacement: Float): Float {
// Generate a random offset in the range [-0.1, +0.1] to be added to the
// vibration scale so the spin effects have slightly different values.
val randomOffset: Float = Random.Default.nextFloat() * 0.2f - 0.1f
return (displacement + randomOffset).absoluteValue.coerceIn(0f, 1f)
}
Java
class AnimationListener implements DynamicAnimation.OnAnimationUpdateListener {
private final Random vibrationRandom = new Random(seed);
private final long lastVibrationUptime;
@Override
public void onAnimationUpdate(
DynamicAnimation animation, float value, float velocity) {
// Delay the next check for a sufficient duration until the current
// composition finishes. Note that you can use
// Vibrator.getPrimitiveDurations API to calculcate the delay.
if (SystemClock.uptimeMillis() - lastVibrationUptime < VIBRATION_DURATION) {
return;
}
float displacement = calculateRelativeDisplacement(value);
// Use some sort of minimum displacement so the final few frames
// of animation don't generate a vibration.
if (displacement < SPIN_MIN_DISPLACEMENT) {
return;
}
lastVibrationUptime = SystemClock.uptimeMillis();
vibrator.vibrate(
VibrationEffect.startComposition()
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_SPIN,
nextSpinScale(displacement))
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_SPIN,
nextSpinScale(displacement))
.compose());
}
// Calculate a random scale for each spin to vary the full effect.
float nextSpinScale(float displacement) {
// Generate a random offset in the range [-0.1,+0.1] to be added to
// the vibration scale so the spin effects have slightly different
// values.
float randomOffset = vibrationRandom.nextFloat() * 0.2f - 0.1f
return MathUtils.clamp(displacement + randomOffset, 0f, 1f)
}
}
Nảy (có tiếng động)
Một ứng dụng nâng cao khác của hiệu ứng rung là mô phỏng các hoạt động tương tác vật lý. PRIMITIVE_THUD có thể tạo ra hiệu ứng mạnh mẽ và vang dội, có thể kết hợp với hình ảnh trực quan về tác động, chẳng hạn như trong video hoặc ảnh động, để tăng cường trải nghiệm tổng thể.
Sau đây là ví dụ về ảnh động quả bóng rơi được tăng cường bằng hiệu ứng tiếng động mỗi khi quả bóng nảy lên từ cuối màn hình:
Hình 4. Dạng sóng này biểu thị gia tốc đầu ra của chế độ rung trên thiết bị.
Kotlin
enum class BallPosition {
Start,
End
}
@Composable
fun BounceScreen() {
// Control variable for the state of the ball.
var ballPosition by remember { mutableStateOf(BallPosition.Start) }
var bounceCount by remember { mutableStateOf(0) }
// Animation for the bouncing ball.
var transitionData = updateTransitionData(ballPosition)
val collisionData = updateCollisionData(transitionData)
// Ball is about to contact floor, only vibrating once per collision.
var hasVibratedForBallContact by remember { mutableStateOf(false) }
if (collisionData.collisionWithFloor) {
if (!hasVibratedForBallContact) {
val vibrationScale = 0.7.pow(bounceCount++).toFloat()
vibrator.vibrate(
VibrationEffect.startComposition().addPrimitive(
VibrationEffect.Composition.PRIMITIVE_THUD,
vibrationScale
).compose()
)
hasVibratedForBallContact = true
}
} else {
// Reset for next contact with floor.
hasVibratedForBallContact = false
}
Screen() {
Box(
Modifier
.fillMaxSize()
.clickable {
if (transitionData.isAtStart) {
ballPosition = BallPosition.End
} else {
ballPosition = BallPosition.Start
bounceCount = 0
}
},
) {
// Build the ball UI based on the current state.
BouncingBall(transitionData)
}
}
}
Java
class ClickListener implements View.OnClickListener {
@Override
public void onClick(View view) {
view.animate()
.translationY(targetY)
.setDuration(3000)
.setInterpolator(new BounceInterpolator())
.setUpdateListener(new AnimatorUpdateListener() {
boolean hasVibratedForBallContact = false;
int bounceCount = 0;
@Override
public void onAnimationUpdate(ValueAnimator animator) {
boolean valueBeyondThreshold = (float) animator.getAnimatedValue() > 0.98;
if (valueBeyondThreshold) {
if (!hasVibratedForBallContact) {
float vibrationScale = (float) Math.pow(0.7, bounceCount++);
vibrator.vibrate(
VibrationEffect.startComposition()
.addPrimitive(
VibrationEffect.Composition.PRIMITIVE_THUD,
vibrationScale)
.compose());
hasVibratedForBallContact = true;
}
} else {
// Reset for next contact with floor.
hasVibratedForBallContact = false;
}
}
});
}
}
Dạng sóng rung có bao
Quy trình tạo kiểu rung tuỳ chỉnh cho phép bạn kiểm soát biên độ rung để tạo hiệu ứng tăng và giảm mượt mà. Phần này giải thích cách tạo hiệu ứng xúc giác động bằng cách sử dụng bao sóng. Nhờ đó, bạn có thể kiểm soát chính xác biên độ và tần số rung theo thời gian. Điều này cho phép bạn tạo ra trải nghiệm xúc giác phong phú và tinh tế hơn.
Kể từ Android 16 (API cấp 36), hệ thống cung cấp các API sau để tạo một phong bì dạng sóng rung bằng cách xác định một chuỗi các điểm kiểm soát:
BasicEnvelopeBuilder: Một phương pháp dễ tiếp cận để tạo hiệu ứng xúc giác độc lập với phần cứng.WaveformEnvelopeBuilder: Phương pháp nâng cao hơn để tạo hiệu ứng xúc giác; yêu cầu bạn phải quen thuộc với phần cứng xúc giác.
Android không cung cấp các giải pháp dự phòng cho hiệu ứng phong bì. Nếu bạn cần được hỗ trợ, hãy hoàn tất các bước sau:
- Kiểm tra xem một thiết bị nhất định có hỗ trợ hiệu ứng bao hay không bằng cách sử dụng
Vibrator.areEnvelopeEffectsSupported(). - Tắt bộ trải nghiệm nhất quán không được hỗ trợ hoặc sử dụng các mẫu rung tuỳ chỉnh hoặc thành phần làm giải pháp thay thế dự phòng.
Để tạo thêm các hiệu ứng cơ bản cho phong bì, hãy sử dụng BasicEnvelopeBuilder với các tham số sau:
- Giá trị cường độ trong phạm vi \( [0, 1] \), biểu thị cường độ rung cảm nhận được. Ví dụ: giá trị \( 0.5 \)được coi là một nửa cường độ tối đa toàn cầu mà thiết bị có thể đạt được.
Giá trị độ sắc nét trong phạm vi \( [0, 1] \), thể hiện độ sắc nét của chế độ rung. Giá trị thấp hơn sẽ tạo ra độ rung mượt mà hơn, trong khi giá trị cao hơn sẽ tạo ra cảm giác rung mạnh hơn.
Giá trị duration (thời lượng) biểu thị thời gian (tính bằng mili giây) cần thiết để chuyển đổi từ điểm kiểm soát cuối cùng (tức là một cặp cường độ và độ sắc nét) sang điểm kiểm soát mới.
Dưới đây là ví dụ về dạng sóng tăng cường độ từ độ rung có tần số thấp đến độ rung có tần số cao, cường độ tối đa trong 500 mili giây, sau đó giảm xuống\( 0 \) (tắt) trong 100 mili giây.
vibrator.vibrate(VibrationEffect.BasicEnvelopeBuilder()
.setInitialSharpness(0.0f)
.addControlPoint(1.0f, 1.0f, 500)
.addControlPoint(0.0f, 1.0f, 100)
.build()
)
Nếu có kiến thức chuyên sâu hơn về phản hồi xúc giác, bạn có thể xác định các hiệu ứng bao bằng WaveformEnvelopeBuilder. Khi sử dụng đối tượng này, bạn có thể truy cập vào ánh xạ tần số sang gia tốc đầu ra (FOAM) thông qua VibratorFrequencyProfile.
- Một giá trị biên độ trong phạm vi \( [0, 1] \), biểu thị cường độ rung có thể đạt được ở tần số nhất định, do FOAM của thiết bị xác định. Ví dụ: giá trị \( 0.5 \) tạo ra một nửa gia tốc đầu ra tối đa có thể đạt được ở tần số đã cho.
Giá trị tần số, được chỉ định bằng Hertz.
Giá trị duration (thời lượng) biểu thị thời gian (tính bằng mili giây) cần thiết để chuyển đổi từ điểm kiểm soát cuối cùng sang điểm kiểm soát mới.
Đoạn mã sau đây cho thấy một ví dụ về dạng sóng xác định hiệu ứng rung 400 mili giây. Quá trình này bắt đầu bằng một đường dốc biên độ 50 ms, từ trạng thái tắt đến trạng thái bật hoàn toàn, ở tần số không đổi là 60 Hz. Sau đó, tần số tăng lên 120 Hz trong 100 ms tiếp theo và duy trì ở mức đó trong 200 ms. Cuối cùng, biên độ giảm xuống \( 0 \)và tần số trở về 60 Hz trong 50 ms cuối cùng:
vibrator.vibrate(VibrationEffect.WaveformEnvelopeBuilder()
.addControlPoint(1.0f, 60f, 50)
.addControlPoint(1.0f, 120f, 100)
.addControlPoint(1.0f, 120f, 200)
.addControlPoint(0.0f, 60f, 50)
.build()
)
Các phần sau đây cung cấp một số ví dụ về dạng sóng rung có bao.
Lò xo nảy
Một mẫu trước đó sử dụng PRIMITIVE_THUD để mô phỏng các lượt tương tác vật lý khi nảy. API bao thư cơ bản cung cấp khả năng kiểm soát chi tiết hơn đáng kể, cho phép bạn điều chỉnh chính xác cường độ và độ sắc nét của chế độ rung.
Điều này dẫn đến phản hồi xúc giác chính xác hơn theo các sự kiện có hiệu ứng động.
Sau đây là ví dụ về một lò xo rơi tự do với ảnh động được tăng cường bằng hiệu ứng bao thư cơ bản phát mỗi khi lò xo bật ra khỏi cuối màn hình:
Hình 5. Một biểu đồ dạng sóng gia tốc đầu ra cho một rung động mô phỏng lò xo bật.
@Composable
fun BouncingSpringAnimation() {
var springX by remember { mutableStateOf(SPRING_WIDTH) }
var springY by remember { mutableStateOf(SPRING_HEIGHT) }
var velocityX by remember { mutableFloatStateOf(INITIAL_VELOCITY) }
var velocityY by remember { mutableFloatStateOf(INITIAL_VELOCITY) }
var sharpness by remember { mutableFloatStateOf(INITIAL_SHARPNESS) }
var intensity by remember { mutableFloatStateOf(INITIAL_INTENSITY) }
var multiplier by remember { mutableFloatStateOf(INITIAL_MULTIPLIER) }
var bottomBounceCount by remember { mutableIntStateOf(0) }
var animationStartTime by remember { mutableLongStateOf(0L) }
var isAnimating by remember { mutableStateOf(false) }
val (screenHeight, screenWidth) = getScreenDimensions(context)
LaunchedEffect(isAnimating) {
animationStartTime = System.currentTimeMillis()
isAnimating = true
while (isAnimating) {
velocityY += GRAVITY
springX += velocityX.dp
springY += velocityY.dp
// Handle bottom collision
if (springY > screenHeight - FLOOR_HEIGHT - SPRING_HEIGHT / 2) {
// Set the spring's y-position to the bottom bounce point, to keep it
// above the floor.
springY = screenHeight - FLOOR_HEIGHT - SPRING_HEIGHT / 2
// Reverse the vertical velocity and apply damping to simulate a bounce.
velocityY *= -BOUNCE_DAMPING
bottomBounceCount++
// Calculate the fade-out duration of the vibration based on the
// vertical velocity.
val fadeOutDuration =
((abs(velocityY) / GRAVITY) * FRAME_DELAY_MS).toLong()
// Create a "boing" envelope vibration effect that fades out.
vibrator.vibrate(
VibrationEffect.BasicEnvelopeBuilder()
// Starting from zero sharpness here, will simulate a smoother
// "boing" effect.
.setInitialSharpness(0f)
// Add a control point to reach the target intensity and
// sharpness very quickly.
.addControlPoint(intensity, sharpness, 20L)
// Add a control point to fade out the vibration intensity while
// maintaining sharpness.
.addControlPoint(0f, sharpness, fadeOutDuration)
.build()
)
// Decrease the intensity and sharpness of the vibration for subsequent
// bounces, and reduce the multiplier to create a fading effect.
intensity *= multiplier
sharpness *= multiplier
multiplier -= 0.1f
}
if (springX > screenWidth - SPRING_WIDTH / 2) {
// Prevent the spring from moving beyond the right edge of the screen.
springX = screenWidth - SPRING_WIDTH / 2
}
// Check for 3 bottom bounces and then slow down.
if (bottomBounceCount >= MAX_BOTTOM_BOUNCE &&
System.currentTimeMillis() - animationStartTime > 1000) {
velocityX *= 0.9f
velocityY *= 0.9f
}
delay(FRAME_DELAY_MS) // Control animation speed.
// Determine if the animation should continue based on the spring's
// position and velocity.
isAnimating = (springY < screenHeight + SPRING_HEIGHT ||
springX < screenWidth + SPRING_WIDTH)
&& (velocityX >= 0.1f || velocityY >= 0.1f)
}
}
Box(
modifier = Modifier
.fillMaxSize()
.noRippleClickable {
if (!isAnimating) {
resetAnimation()
}
}
.width(screenWidth)
.height(screenHeight)
) {
DrawSpring(mutableStateOf(springX), mutableStateOf(springY))
DrawFloor()
if (!isAnimating) {
DrawText("Tap to restart")
}
}
}
Phóng tên lửa
Một mẫu trước đó cho thấy cách sử dụng API bao thư cơ bản để mô phỏng phản ứng của lò xo đàn hồi. WaveformEnvelopeBuilder mở khoá khả năng kiểm soát chính xác toàn bộ dải tần số của thiết bị, cho phép tạo ra các hiệu ứng xúc giác có độ tuỳ chỉnh cao. Bằng cách kết hợp dữ liệu này với dữ liệu FOAM, bạn có thể điều chỉnh độ rung cho phù hợp với các khả năng tần số cụ thể.
Sau đây là một ví dụ minh hoạ mô phỏng hoạt động phóng tên lửa bằng cách sử dụng một kiểu rung động linh hoạt. Hiệu ứng này có đầu ra gia tốc tần số tối thiểu được hỗ trợ là 0, 1 G, đến tần số cộng hưởng, luôn duy trì đầu vào biên độ 10%. Điều này cho phép hiệu ứng bắt đầu với đầu ra tương đối mạnh và tăng cường độ và độ sắc nét cảm nhận được, mặc dù biên độ điều khiển là như nhau. Khi đạt đến cộng hưởng, tần số hiệu ứng sẽ giảm xuống mức tối thiểu, được cảm nhận là cường độ và độ sắc nét giảm. Điều này tạo ra cảm giác có lực cản ban đầu, sau đó là lực đẩy, mô phỏng một vụ phóng vào không gian.
Không thể tạo hiệu ứng này bằng API bao thư cơ bản, vì API này sẽ trừu tượng hoá thông tin dành riêng cho thiết bị về tần số cộng hưởng và đường cong gia tốc đầu ra. Việc tăng độ sắc nét có thể đẩy tần số tương đương vượt quá cộng hưởng, có khả năng gây ra hiện tượng giảm tốc ngoài ý muốn.
Hình 6. Biểu đồ dạng sóng gia tốc đầu ra cho một rung động mô phỏng quá trình phóng tên lửa.
@Composable
fun RocketLaunchAnimation() {
val context = LocalContext.current
val screenHeight = remember { mutableFloatStateOf(0f) }
var rocketPositionY by remember { mutableFloatStateOf(0f) }
var isLaunched by remember { mutableStateOf(false) }
val animation = remember { Animatable(0f) }
val animationDuration = 3000
LaunchedEffect(isLaunched) {
if (isLaunched) {
animation.animateTo(
1.2f, // Overshoot so that the rocket goes off the screen.
animationSpec = tween(
durationMillis = animationDuration,
// Applies an easing curve with a slow start and rapid acceleration
// towards the end.
easing = CubicBezierEasing(1f, 0f, 0.75f, 1f)
)
) {
rocketPositionY = screenHeight.floatValue * value
}
animation.snapTo(0f)
rocketPositionY = 0f;
isLaunched = false;
}
}
Box(
modifier = Modifier
.fillMaxSize()
.noRippleClickable {
if (!isLaunched) {
// Play vibration with same duration as the animation, using 70% of
// the time for the rise of the vibration, to match the easing curve
// defined previously.
playVibration(vibrator, animationDuration, 0.7f)
isLaunched = true
}
}
.background(Color(context.getColor(R.color.background)))
.onSizeChanged { screenHeight.floatValue = it.height.toFloat() }
) {
drawRocket(rocketPositionY)
}
}
private fun playVibration(
vibrator: Vibrator,
totalDurationMs: Long,
riseBias: Float,
minOutputAccelerationGs: Float = 0.1f,
) {
require(riseBias in 0f..1f) { "Rise bias must be between 0 and 1." }
if (!vibrator.areEnvelopeEffectsSupported()) {
return
}
val resonantFrequency = vibrator.resonantFrequency
if (resonantFrequency.isNaN()) {
// Device doesn't have or expose a resonant frequency.
return
}
val startFrequency = vibrator.frequencyProfile?.getFrequencyRange(minOutputAccelerationGs)?.lower ?: return
if (startFrequency >= resonantFrequency) {
// Vibrator can't generate the minimum required output at lower frequencies.
return
}
val minDurationMs = vibrator.envelopeEffectInfo.minControlPointDurationMillis
val rampUpDurationMs = (riseBias * totalDurationMs).toLong() - minDurationMs
val rampDownDurationMs = totalDurationMs - rampUpDuration - minDurationMs
vibrator.vibrate(
VibrationEffect.WaveformEnvelopeBuilder()
// Quickly reach the target output at the start frequency
.addControlPoint(0.1f, startFrequency, minDurationMs)
.addControlPoint(0.1f, resonantFrequency, rampUpDurationMs)
.addControlPoint(0.1f, startFrequency, rampDownDurationMs)
// Controlled ramp down to zero to avoid ringing after the vibration.
.addControlPoint(0.0f, startFrequency, minDurationMs)
.build()
)
}
LavaBeats
Như trong ví dụ về Rocket Launch (Phóng tên lửa), API WaveformEnvelopeBuilder cho phép thiết kế nhiều hiệu ứng xúc giác phức tạp bằng cách cho phép kiểm soát các đoạn biên độ và tần số trong một rung động. Một ví dụ khác về thiết kế như vậy là việc mô phỏng các cảm giác vật lý trừu tượng hơn, chẳng hạn như "sự sống động".
Bạn có thể thực hiện việc này bằng cách biểu thị các dấu ấn sinh học của tín hiệu điện tâm đồ (ECG) điển hình bằng các đoạn rung có biên độ và tần số nhất định. LavaBeats là một ví dụ trong đó 2 đoạn đặc trưng của bản ghi điện tâm đồ được biểu thị dưới dạng 2 xung được phân tách bằng độ trễ thời gian. Xung đặc trưng đầu tiên là phức hợp QRS, xuất hiện dưới dạng một đỉnh nhọn có biên độ cao và thời lượng ngắn. Xung thứ hai là sóng T, có biên độ thấp hơn, thời lượng dài hơn và hình dạng mượt mà hơn (xem Hình 7).
Sử dụng WaveformEnvelopeBuilder để tạo ra nhiều lần lặp lại của hai xung này, cách nhau bằng độ trễ cố định từ xung thứ nhất đến xung thứ hai. Xung đầu tiên có thể là tín hiệu tiếng kêu bắt đầu ở tần số thấp và kết thúc ở tần số cao hơn trong thời gian ngắn. Xung thứ hai có thể được biểu thị dưới dạng một chu kỳ duy nhất của một hình sin tần số thấp. Chúng ta có thể kết hợp hai xung này thành một nhịp và lặp lại tổ hợp này nhiều lần với độ trễ ở giữa, theo tốc độ nhịp mỗi phút (bpm) thông thường. Kết quả là một hiệu ứng xúc giác giống như nhịp tim.
Bạn có thể dùng thử LavaBeats trong ứng dụng mẫu về hiệu ứng xúc giác của chúng tôi trên GitHub và cảm nhận hiệu ứng kèm theo hình ảnh trực quan của đèn dung nham có nhịp điệu giống như hiệu ứng xúc giác. Bạn cũng có thể thay đổi chế độ cài đặt của hiệu ứng để tạo ra các cảm giác rung khác nhau bằng cách sửa đổi biên độ, tần số, thời lượng và độ trễ của hai xung.
Hình 7. Một đoạn ghi điện tâm đồ có phức hợp QRS và sóng T
@RequiresApi(Build.VERSION_CODES.BAKLAVA)
private fun createEnvelopeEffect(
beatParameters: List<BeatParameter>
):VibrationEffect =
VibrationEffect.WaveformEnvelopeBuilder()
.apply {
repeat(beatParameters.getNumBeats()) {
// First pulse chirp
addControlPoint(
beatParameters.getFirstPulseAmplitude(),
beatParameters.getFirstPulseStartFreq(),
ENVELOPE_RAMP_DURATION_MILLIS,
)
addControlPoint(
beatParameters.getFirstPulseAmplitude(),
beatParameters.getFirstPulseEndFreq(),
beatParameters.getFirstPulseDurationMillis().toLong(),
)
addControlPoint(
0f,
beatParameters.getFirstPulseEndFreq(),
ENVELOPE_RAMP_DURATION_MILLIS,
)
// Delay between first and second pulse
addControlPoint(
0f,
beatParameters.getFirstPulseEndFreq(),
beatParameters.getFirstToSecondPulseDelayMillis().toLong(),
)
// Second pulse
addControlPoint(
beatParameters.getSecondPulseAmplitude(),
beatParameters.getSecondPulseFreq(),
ENVELOPE_RAMP_DURATION_MILLIS,
)
addControlPoint(
beatParameters.getSecondPulseAmplitude(),
beatParameters.getSecondPulseFreq(),
(1_000 / (2f * beatParameters.getSecondPulseFreq())).toLong(),
)
addControlPoint(
0f,
beatParameters.getSecondPulseFreq(),
ENVELOPE_RAMP_DURATION_MILLIS,
)
addControlPoint(
0f,
beatParameters.getSecondPulseFreq(),
beatParameters.getBeatDelayMillis().toLong(),
)
}
}
.build()
/** A parameter of a haptic beat effect that represents an ECG signal parameter */
@Stable
data class BeatParameter(
val description: String = "",
val value: Float = 0f,
val range: ClosedFloatingPointRange<Float> = 0f..1f,
val steps: Int = 0,
val isFrequencyType: Boolean = false,
)