Trang này trình bày các ví dụ về cách sử dụng nhiều API xúc giác để tạo 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 bao gồm các ví dụ sau:
- Bố cục cố định theo dòng thời gian với
VibrationEffect.Builder- Soạn bằng các giá trị đặt trước: Sắp xếp các cảm giác xúc giác được xác định trước.
- Soạn nhạc bằng đường bao và giá trị đặt trước: Kết hợp đường bao và giá trị đặt trước theo dòng thời gian.
- Sử dụng lại và dịch chuyển sự kiện: Dịch chuyển và sử dụng lại các sự kiện hiện có trong thành phần kết hợp.
- Lặp lại thành phần: Tạo hiệu ứng lặp lại trên dòng thời gian.
- Kiểu rung tuỳ chỉnh
- Kiểu tăng dần: Kiể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ó dự phòng: Bản minh hoạ dự phòng.
- Cấu trúc cơ bản của hiệu ứng rung
- Dạng sóng rung có đường bao
- Bouncing spring (Lò xo nảy): Hiệu ứng nảy lò xo sử dụng các hiệu ứng bao bọc cơ bản.
- Phóng tên lửa: Hiệu ứng phóng tên lửa sử dụng hiệu ứng đường bao dạng sóng.
Để biết thêm ví dụ, hãy xem bài viết Thêm phản hồi xúc giác vào sự kiện và luôn tuân thủ các nguyên tắc thiết kế phản hồi xúc giác.
Sử dụng phương án dự phòng để xử lý khả năng tương thích của thiết bị
Khi triển khai 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 nền tảng 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 tính năng dự phòng tự động ở cấp khung. Nếu một thiết bị không hỗ trợ sẵnPresetđược yêu cầu hoặcEnvelopecơ bản, thì khung sẽ tự động chuyển đổi thành một giải pháp thay thế phù hợp tại thời điểm phát với nỗ lực tối đa. Bạn không cần phải kiểm tra thủ công các chức năng của thiết bị theo từng thành phần gốc trước khi phát các hiệu ứng được tạo bằngVibrationEffect.Builder.- Trường hợp ngoại lệ: Đường bao dạng sóng nâng cao được tạo bằng
WaveformEnvelopeBuilderkhông hỗ trợ tính năng tự động dự phòng vì các đường bao này phụ thuộc vào mối liên kết 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ệ: Đườ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ó tính năng dự phòng tự động. Nếu một thành phần chứa dù chỉ một thành phần gốc không được hỗ trợ, thì toàn bộ hiệu ứng 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 biên độ: Biên độ khác 0 được làm tròn lên 100% trên các thiết bị không có chế độ kiểm soát biê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 xúc giác gốc
Android có một số nguyên hàm xúc giác khác nhau về cả biên độ và tần số. Bạn có thể sử dụng một hoặc nhiều dữ liệu gốc kết hợp với nhau để đạ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 tỷ lệ khác nhau theo tỷ lệ 1,4 trở lên để nhận biết rõ hơn sự khác biệt về cường độ.
Sử dụng tỷ lệ 0, 5, 0, 7 và 1 để tạo phiên bản cường độ thấp, trung bình và cao của một màu gốc.
Tạo kiểu rung tuỳ chỉnh
Kiểu rung thường được dùng trong phản hồi xúc giác thu hút sự 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 kiểu rung dài, 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.
Hiệu ứng dạng sóng thường dễ nhận biết, 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 phát trong môi trường yên tĩnh. Việc tăng biên độ mục tiêu quá nhanh cũng có thể tạo ra tiếng vo ve có thể nghe thấy. Thiết kế các mẫu dạng sóng để làm mượt các hiệu ứng chuyển đổi biên độ nhằm tạo hiệu ứng tăng và giảm dần.
Ví dụ về kiểu rung
Các phần sau đây cung cấp một số ví dụ về kiể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ố:
- Thời gian: một mảng thời lượng, tính bằng mili giây, cho mỗi phân đoạn dạng sóng.
- Biên độ: biên độ 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à biên độ 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 rung hai lần, có khoảng nghỉ 350 mili giây giữa các lần rung. Xung đầu tiên là một đoạn dốc mượt mà lên đến biên độ tối đa và xung thứ hai là một đoạn 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.
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 ) )
Mẫu lặp lại
Dạng sóng cũng có thể được phát lặp lại cho đến khi bị 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, độ rung sẽ tiếp tục cho đến khi bị huỷ rõ ràng trong dịch vụ:
fun 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. val repeatingEffect: VibrationEffect = VibrationEffect.createWaveform( timings, amplitudes, repeat ) // repeatingEffect can be used in multiple places. vibrator.vibrate(repeatingEffect) } fun 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 phải 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ó dữ liệu dự phòng
Việc 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ị cấp thấp không có chức 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 kiể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 kiểu BẬT/TẮT đơn giản hơn có thể phát dưới dạng dự phòng.
if (vibrator.hasAmplitudeControl()) { vibrator.vibrate( VibrationEffect.createWaveform( smoothTimings, amplitudes, smoothRepeatIdx ) ) } else { vibrator.vibrate( VibrationEffect.createWaveform( onOffTimings, onOffRepeatIdx ) ) }
Bố cục được neo vào dòng thời gian với 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 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 riêng biệt theo trình tự thời gian tuyệt đối bằng startTimeMillis.
VibrationEffect.Builder hỗ trợ kết hợp nhiều kiểu rung:
- Giá trị đặt trước: Các xung 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 động, bao gồm cả đường bao cơ bản không phụ thuộc vào phần cứng (
BasicEnvelopeBuilder) và đường bao điều tần nâng cao (WaveformEnvelopeBuilder). - VibrationEffects hiện có: Thành phần gốc (
VibrationEffect.Composition), dạng sóng 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ằng cách sử dụngaddEvents()hoặc sao chép hàm khởi tạo. - Chuỗi lặp lại: Các mẫu liên tục được định cấu hình bằng
setRepeatingEffect().
Các kiểu rung được tạo bằng VibrationEffect.Builder có tính năng tự động dự phòng tích hợp sẵn ở cấp khung trên các thành phần (bao gồm cả giá trị đặt trước, đường bao cơ bản và hiệu ứng rung kết hợp), đảm bảo trải nghiệm người dùng nhất quán trên nhiều thiết bị mà không cần kiểm tra chức năng theo cách thủ công. (Đường bao dạng sóng nâng cao được tạo bằng WaveformEnvelopeBuilder cần có sự hỗ trợ của phần cứng và không có tính năng hỗ trợ dự phòng tự động.)
Soạn thư bằng các chế độ đặt sẵn
Sử dụng VibrationEffect.Preset để thêm các xung 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 giá trị đặt trước để cung cấp độ rung rõ ràng, ngắn và dễ chịu, phù hợp với các nguyên tắc về xúc giác để có 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 hoạt động, hãy xem bài viết Sơ lược về bộ truyền động rung.
Các chế độ đặt trước thay thế các dữ liệu gốc ngắn trong API VibrationEffect.Composition.
Đối với 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), hãy sử dụng dạng sóng bao bọc (PWLE).
Mỗi giá trị đặt trước có thể được chỉ định một tỷ lệ tuỳ chọn từ 0.0f đến 1.0f và được đặt ở một thời gian bắt đầu rõ ràng (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 đường bao 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 giá trị đặt trước để tạo ra các mẫu xúc giác phong phú, nhiều phân đoạn.
Dưới đây là ví dụ về cách phát một đường bao tăng dần và giảm dần mượt mà, theo sau là một giá trị đặt trước cho tiếng nhấp 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à dời các 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 đối tượng VibrationEffect.Event của đối tượng đó bằng getEvents() và thêm chúng bằng một độ lệch bằng addEvents(startTimeShiftMillis, events) (hoặc truyền trực tiếp hiệu ứng đế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 dữ liệu gốc của chúng thành giá trị đặt trước, cho phép hỗ trợ dự phòng trong 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ẫ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);
Quản lý thời gian, xác thực và độ lệch
Khi thiết kế các thành phần kết hợp bằng VibrationEffect.Builder, hãy lưu ý các quy tắc về thời gian và quy tắc xác thực 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 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 thời gian xây dựng: Trình tạo thực hiện xác thực nỗ lực tối đa tại thời điểm
build()bằng cách sử dụng thời lượng phần tử đã biết (hoặc tối thiểu 1 mili giây cho giá trị đặt trước). Nếu phát hiện thấy sự trùng lặp không thể xảy ra, thì hàm này sẽ gửi mộtIllegalArgumentException. - Chuyển đổi tuần tự khi phát: Nếu một 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 khung 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 độ lệch, 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 gốc rung
Phần này trình bày cách tạo hiệu ứng rung bằng VibrationEffect.Composition. Thành phần gốc (được mô tả ở phần trước trên trang này) do nhà sản xuất thiết bị triển khai. Các hiệu ứng này tạo ra độ rung rõ ràng, ngắn và dễ chịu, phù hợp với các nguyên tắc về 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 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ó tính năng tự động dự phòng cho các dữ liệu gốc không được hỗ trợ. Vì thế:
Trước khi kích hoạt tính năng 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 dữ liệu gốc mà bạn đang sử dụng.
Tắt tập hợp trải nghiệm nhất quán không được hỗ trợ, chứ không chỉ tắt những hiệu ứng thiếu một thành phần gốc.
- 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 gốc của thành phần kết hợp thành giá trị đặt trước và tự động cung cấp tính năng dự phòng trong thời gian chạy nếu thiết bị của người dùng không hỗ trợ bất kỳ thành phần gốc nào.
Hiệu ứng rung kết hợp
Bạn có thể tạo hiệu ứng rung kết hợp bằng
VibrationEffect.Composition. Dưới đây là ví dụ về hiệu ứng tăng chậm, theo sau là hiệu ứng nhấp mạnh:
vibrator.vibrate( VibrationEffect.startComposition().addPrimitive( VibrationEffect.Composition.PRIMITIVE_SLOW_RISE ).addPrimitive( VibrationEffect.Composition.PRIMITIVE_CLICK ).compose() )
Một bản nhạc được tạo bằng cách thêm các thành phần gốc để phát theo trình tự. Mỗi nguyên hàm cũng có thể mở rộng, vì vậy, bạn có thể kiểm soát biên độ rung do mỗi nguyên hàm tạo ra. Thang đo được xác định là một giá trị từ 0 đến 1, trong đó 0 thực sự liên kết với biên độ tối thiểu mà người dùng có thể (gần như không) cảm nhận được thành phần gốc này.
Tạo biến thể trong các thành phần rung cơ bản
Nếu bạn muốn tạo phiên bản yếu và mạnh của cùng một màu gốc, hãy tạo tỷ lệ cường độ 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ố tạo nhiều hơn ba mức cường độ của cùng một màu gốc, vì chúng không khác biệt về mặt cảm nhận. Ví dụ: sử dụng tỷ lệ 0,5, 0,7 và 1 để tạo các phiên bản cường độ thấp, trung bình và cao của một hiệu ứng gốc.
Thêm khoảng trống giữa các thành phần rung gốc
Cấu trúc cũng có thể chỉ định độ trễ được thêm vào giữa các thành phần gốc liên tiếp. Độ trễ này được biểu thị bằng mili giây kể từ khi kết thúc dữ liệu gốc trước đó. Nhìn chung, khoảng cách từ 5 đến 10 mili giây giữa hai dữ liệu gốc là quá ngắn để có thể phát hiện được. Sử dụng khoảng trống theo thứ 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 giữa hai dữ liệu gốc. Dưới đây là ví dụ về một thành phần có độ trễ:
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() )
Kiểm tra xem những dữ liệu gốc nào được hỗ trợ
Bạn có thể sử dụng các API sau để xác minh khả năng hỗ trợ của thiết bị đối với các dữ liệu gốc cụ thể:
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. }
Bạn cũng có thể kiểm tra nhiều thành phần gốc rồi quyết định thành phần nào sẽ được kết hợp dựa trên mức hỗ trợ của thiết bị:
val primitives: IntArray = intArrayOf( VibrationEffect.Composition.PRIMITIVE_LOW_TICK, VibrationEffect.Composition.PRIMITIVE_TICK, VibrationEffect.Composition.PRIMITIVE_CLICK ) val supported: BooleanArray = vibrator.arePrimitivesSupported(*primitives)
Ví dụ về các kiểu rung
Các phần sau đây cung cấp một số ví dụ về cấu trúc rung, được lấy từ ứng dụng mẫu về xúc giác trên GitHub.
Kháng cự (với số lượng bọ ve thấp)
Bạn có thể kiểm soát biên độ của độ rung gốc để truyền tải phản hồi hữu ích cho một hành động đang diễn ra. Bạn có thể sử dụng các giá trị tỷ lệ gần nhau để tạo hiệu ứng tăng dần mượt mà cho một đối tượng gốc. Độ 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 hoạt động tương tác của người dùng. Điều này được minh hoạ trong ví dụ sau về ảnh động của khung hiển thị được điều khiển bằng cử chỉ kéo và 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 độ rung trên thiết bị.
@Composable fun ResistScreen() { // Control variables for the dragging of the indicator. var isDragging by remember { mutableStateOf(false) } var dragOffset by remember { mutableFloatStateOf(0f) } // Only vibrates while the user is dragging LaunchedEffect(isDragging) { // Continuously run the effect for vibration to occur even when the view // is not being drawn, when user stops dragging midway through gesture. while (isDragging) { // 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() ) } } 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) } }
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 có thời lượng khác nhau. Chỉ có một dữ liệu gốc để giảm dần, PRIMITIVE_QUICK_FALL. Các dữ liệu gốc này hoạt động hiệu quả hơn khi kết hợp với nhau để tạo ra một phân đoạn dạng sóng tăng dần về cường độ rồi giảm dần. Bạn có thể căn chỉnh các dữ liệu gốc được điều chỉnh theo tỷ lệ để ngăn biên độ tăng đột ngột giữa các dữ liệu gốc, đ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 nhận thức, 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 trọng tâm sang phần giảm.
Dưới đây là ví dụ về cách áp dụng thành phần này để mở rộng và thu gọn một vòng tròn. Hiệu ứng tăng dần có thể tăng cường cảm giác mở rộng trong ảnh động. Sự kết hợp giữa hiệu ứng tăng và giảm giúp nhấn mạnh hiệu ứng 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 độ rung trên một thiết bị.
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) Column( Modifier .clickable( onClick = { 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) } }
Lắc lư (có xoay)
Một trong những nguyên tắc chính về phản hồi xúc giác là làm hài lòng người dùng. Một cách thú vị để tạo hiệu ứng rung dễ chịu và bất ngờ là sử dụng PRIMITIVE_SPIN. Hàm gố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 lắc lư và không ổn định, hiệu ứng này có thể được tăng cường hơn nữa bằng cách áp dụng tỷ lệ ngẫu nhiên trên mỗi đối tượng gốc. Bạn cũng có thể thử nghiệm khoảng cách giữa các nguyên hàm quay liên tiếp. Hai lần xoay không có khoảng trống (0 mili giây ở giữa) tạo ra cảm giác xoay chặt. Việc tăng khoảng cách giữa các lần quay từ 10 lên 50 mili giây sẽ mang lại cảm giác quay lỏng hơn và có thể dùng để khớ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 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ẻ.
Dưới đây là ví dụ về một hình dạng đàn hồi nảy trở lại sau khi được 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, được phát với cường độ khác nhau, tỷ lệ thuận với độ dịch chuyển của hiệu ứng nảy.
Hình 3. Dạng sóng này biểu thị gia tốc đầu ra của độ rung trên thiết bị.
@Composable fun WobbleScreen() { // Control variables for the dragging and animating state of the elastic. var dragDistance by remember { mutableFloatStateOf(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 ), // The spring ends on its target. An end value of 0f means that the // shape is back at rest, so the wobble is over. finishedListener = { endValue -> if (endValue == 0f) isWobbling = false }, ) LaunchedEffect(isWobbling) { while (isWobbling) { 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 = { // Wobble only if the shape is far enough from rest to vibrate. // It then springs back to 0f, and the finishedListener ends // the wobble. val displacement = dragDistanceAnimated / MAX_DRAG_DISTANCE isWobbling = displacement > SPIN_MIN_DISPLACEMENT dragDistance = 0f }, orientation = Orientation.Vertical, state = rememberDraggableState { delta -> isWobbling = false dragDistance += delta } ) ) { // Draw the wobbling shape using the animated spring-like value. The // lambda defers the read to the draw phase, so the animation doesn't // recompose this screen on every frame. 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) }
Nảy (kèm tiếng thình thịch)
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ề một 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ể.
Dưới đây là ví dụ về ảnh động thả bóng được cải thiện bằng hiệu ứng tiếng thình thịch phát mỗi khi 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 độ rung trên thiết bị.
enum class BallPosition { Start, End } @Composable fun BounceScreen() { // Control variable for the state of the ball. var ballPosition by remember { mutableStateOf(BallPosition.Start) } // Plain holder for the bounce count. It's read only in the effect and in // the click handler, never in composition, so it needs no snapshot state. val bounceCount = remember { intArrayOf(0) } // Animation for the bouncing ball. val transitionData = updateTransitionData(ballPosition) val collisionData = updateCollisionData(transitionData) // Ball is about to contact floor. The effect runs only when the collision // state changes, so it vibrates once per collision. Nothing here suspends, // so a keyed SideEffect does the job without launching a coroutine. SideEffect(collisionData.collisionWithFloor) { if (collisionData.collisionWithFloor) { val vibrationScale = 0.7.pow(bounceCount[0]++).toFloat() vibrator.vibrate( VibrationEffect.startComposition().addPrimitive( VibrationEffect.Composition.PRIMITIVE_THUD, vibrationScale ).compose() ) } } Box( Modifier .fillMaxSize() .clickable { if (transitionData.isAtStart) { ballPosition = BallPosition.End } else { ballPosition = BallPosition.Start bounceCount[0] = 0 } }, ) { // Build the ball UI based on the current state. BouncingBall(transitionData) } }
Dạng sóng rung có đường 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 đường bao dạng sóng cho phép 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 (cấp độ API 36), hệ thống cung cấp các API sau để tạo đường bao dạng sóng rung bằng cách xác định một chuỗi điểm điều khiển:
BasicEnvelopeBuilder: Một phương pháp dễ tiếp cận để tạo hiệu ứng xúc giác không phụ thuộc vào phần cứng.WaveformEnvelopeBuilder: Cách tiếp cận 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 phương án dự phòng cho hiệu ứng bao bọc. 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 bọc 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 kiểu rung tuỳ chỉnh hoặc mẫu rung làm phương án dự phòng.
Để tạo thêm các hiệu ứng bao bọc cơ bản, hãy sử dụng BasicEnvelopeBuilder với các tham số sau:
- Giá trị cường độ trong phạm vi \( [0, 1] \), thể hiện 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 chung mà thiết bị có thể đạt được.
Giá trị độ sắc nét trong phạm vi \( [0, 1] \), thể hiện độ rõ nét của độ rung. Giá trị thấp hơn sẽ mang lại độ rung mượt mà hơn, trong khi giá trị cao hơn sẽ tạo ra cảm giác rõ rệt 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 gần nhất (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 cao độ thấp đến độ rung cao độ 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 nâng cao hơn về xúc giác, bạn có thể xác định hiệu ứng bao bọc bằng WaveformEnvelopeBuilder. Khi sử dụng đối tượng này, bạn có thể truy cập vào
bản đồ ánh xạ tần số với gia tốc đầu ra (FOAM) thông qua
VibratorFrequencyProfile.
- Giá trị biên độ trong dải \( [0, 1] \), đại diện cho 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) thể hiện thời gian (tính bằng mili giây) cần thiết để chuyển từ điểm kiểm soát gần nhất sang điểm kiểm soát mới.
Đoạn mã sau đây cho thấy một dạng sóng mẫu xác định hiệu ứng rung 400 mili giây. Bắt đầu bằng một đoạn dốc biên độ 50 mili giây, từ tắt đến đầy đủ, ở tần số không đổi 60 Hz. Sau đó, tần số tăng lên 120 Hz trong 100 mili giây tiếp theo và duy trì ở mức đó trong 200 mili giây. Cuối cùng, biên độ giảm xuống \( 0 \)và tần số trở về 60 Hz trong 50 mili giây cuối:
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ó đường bao.
Lò xo nảy bật
Một mẫu trước đó sử dụng PRIMITIVE_THUD để mô phỏng các tương tác nảy vật lý. API đường bao 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 độ rung.
Điều này dẫn đến phản hồi xúc giác theo dõi chính xác hơn các sự kiện ảnh động.
Dưới đây là ví dụ về một lò xo rơi tự do với ảnh động được cải tiến bằng hiệu ứng bao bọc cơ bản được phát mỗi khi lò xo nảy lên từ cuối màn hình:
Hình 5. Biểu đồ dạng sóng gia tốc đầu ra cho một độ rung mô phỏng lò xo nảy.
@Composable fun BouncingSpringAnimation() { // The spring's height above the floor: 1f is the drop height, 0f is the floor. val springHeight = remember { Animatable(1f) } var isAnimating by remember { mutableStateOf(true) } LaunchedEffect(isAnimating) { if (!isAnimating) return@LaunchedEffect springHeight.snapTo(1f) // Each bounce reaches a fraction of the previous height. var bounceHeight = 1f while (bounceHeight > MIN_BOUNCE_HEIGHT) { // Fall to the floor, speeding up like a falling object. A fall from // a greater height takes longer. val fallDurationMs = (DROP_DURATION_MS * sqrt(bounceHeight)).toInt() springHeight.animateTo(0f, tween(fallDurationMs, easing = EaseInQuad)) // The spring hits the floor: play a "boing" envelope effect that fades // out. A higher bounce gives a stronger and sharper vibration. 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(bounceHeight, bounceHeight * MAX_SHARPNESS, 20L) // Add a control point to fade out the vibration intensity while // maintaining sharpness. .addControlPoint(0f, bounceHeight * MAX_SHARPNESS, fallDurationMs.toLong()) .build() ) // Bounce back up to a lower height, slowing down like a thrown object. // When the next bounce is too small, the spring stays on the floor. bounceHeight *= BOUNCE_DAMPING if (bounceHeight > MIN_BOUNCE_HEIGHT) { val riseDurationMs = (DROP_DURATION_MS * sqrt(bounceHeight)).toInt() springHeight.animateTo(bounceHeight, tween(riseDurationMs, easing = EaseOutQuad)) } } isAnimating = false } Box( modifier = Modifier .fillMaxSize() .clickable(enabled = !isAnimating) { isAnimating = true } .drawBehind { // Read the height in the draw phase, and scale it to the distance // between the top of the box and the floor. val floorTop = size.height - FLOOR_HEIGHT.toPx() val dropDistance = floorTop - SPRING_HEIGHT.toPx() drawSpring(topY = dropDistance * (1f - springHeight.value)) drawFloor(top = floorTop) } ) { if (!isAnimating) { Text("Tap to restart", Modifier.align(Alignment.Center)) } } }
Phóng tên lửa
Một mẫu trước đó cho thấy cách sử dụng API bao bọc cơ bản để mô phỏng phản ứng lò xo nảy. WaveformEnvelopeBuilder cho phép kiểm soát chính xác toàn bộ dải tần số của thiết bị, nhờ đó tạo ra hiệu ứng xúc giác tuỳ chỉnh cao. Bằng cách kết hợp thông tin 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 chức năng tần số cụ thể.
Dưới đây là ví dụ minh hoạ mô phỏng phóng tên lửa bằng kiểu rung động. Hiệu ứng này chuyển từ công suất gia tốc tần số tối thiểu được hỗ trợ (0,1 G) sang tần số cộng hưởng, luôn duy trì biên độ đầu vào là 10%. Điều này cho phép hiệu ứng bắt đầu bằng một đầu ra mạnh mẽ hợp lý và tăng cường độ cũng như độ sắc nét cảm nhận được, ngay cả khi biên độ truyền động là như nhau. Khi đạt đến mức cộng hưởng, tần số hiệu ứng sẽ giảm xuống mức tối thiểu, được coi là cường độ và độ sắc nét giảm dần. Điều này tạo ra cảm giác kháng cự ban đầu, sau đó là cảm giác được giải phóng, mô phỏng quá trình phóng vào không gian.
Bạn không thể tạo hiệu ứng này bằng API đường bao cơ bản, vì API này tóm tắt 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á mức cộng hưởng, có khả năng gây ra hiện tượng giảm gia tốc ngoài ý muốn.
Hình 6. Biểu đồ dạng sóng gia tốc đầu ra cho một độ rung mô phỏng quá trình phóng tên lửa.
@Composable fun RocketLaunchAnimation() { 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) ) ) animation.snapTo(0f) isLaunched = false } } Canvas( modifier = Modifier .fillMaxSize() .clickable { 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.toLong(), 0.7f) isLaunched = true } } .background(colorResource(R.color.background)) ) { // Read the animation in the draw phase, and scale it to the canvas height. drawRocket(size.height * animation.value) } } 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 - rampUpDurationMs - 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 phân đoạn biên độ và tần số trong một lần rung. Một ví dụ khác về thiết kế như vậy là 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ị dấu ấn sinh học của tín hiệu điện tâm đồ (ECG) thông thường bằng các phân đoạn rung có biên độ và tần số nhất định. LavaBeats là một ví dụ trong đó hai phân đoạn đặc trưng của bản ghi ECG được biểu thị dưới dạng hai xung được phân tách bằng độ trễ thời gian. Xung đặc trưng đầu tiên là phức bộ QRS, xuất hiện dưới dạng đỉ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 hơn (xem Hình 7).
Sử dụng WaveformEnvelopeBuilder để tạo nhiều lần lặp lại của hai xung này, được phân tách bằng độ trễ xung cố định từ xung thứ nhất đến xung thứ hai. Xung đầu tiên có thể là tín hiệu chirp bắt đầu ở tần số thấp và kết thúc ở tần số cao hơn trong một khoảng thời gian ngắn. Xung thứ hai có thể được biểu thị dưới dạng một chu kỳ của sóng hình sin tần số thấp. Chúng ta có thể kết hợp hai xung thành một nhịp và lặp lại nhịp đó nhiều lần, có khoảng trễ giữa các nhịp, theo tốc độ nhịp thông thường mỗi phút (nhịp/phút). Kết quả là 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ề phản hồi xúc giác của chúng tôi trên GitHub và cảm nhận hiệu ứng đi kèm với hình ảnh đèn dung nham nhịp nhàng theo 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 nhịp đập 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 của bản ghi điện tâm đồ có phức bộ QRS và sóng T
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 */ data class BeatParameter( val description: String = "", val value: Float = 0f, val range: ClosedFloatingPointRange<Float> = 0f..1f, val steps: Int = 0, val isFrequencyType: Boolean = false, )