Trang này trình bày các ví dụ về cách sử dụng nhiều API haptics để tạo hiệu ứng tuỳ chỉnh trong ứng dụng Android. Vì hầu hết thông tin trên trang này đều dựa vào kiến thức chuyên sâu về cách hoạt động của bộ truyền động rung, nên bạn nên đọc bài viết Sơ lược về bộ truyền động rung.
Trang này bao gồm các ví dụ sau.
- Chế độ rung tuỳ chỉnh
- Mẫu tăng dần: Một mẫu bắt đầu một cách suôn sẻ.
- Mẫu lặp lại: Mẫu không có điểm kết thúc.
- Mẫu có phương thức dự phòng: Bản minh hoạ phương thức dự phòng.
- Cấu trúc rung
Để biết 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 thủ nguyên tắc thiết kế xúc giác.
Sử dụng phương án dự phòng để xử lý khả năng tương thích với thiết bị
Khi triển khai hiệu ứng tuỳ chỉnh, hãy cân nhắc những điều sau:
- Khả năng thiết bị cần thiết cho hiệu ứng
- Việc cần làm khi thiết bị không phát được hiệu ứng
Tài liệu tham khảo API haptics của Android cung cấp thông tin chi tiết về cách kiểm tra tính năng hỗ trợ cho các thành phần liên quan đến haptics để ứng dụng của bạn có thể mang lại trải nghiệm tổng thể nhất quán.
Tuỳ thuộc vào trường hợp sử dụng, bạn có thể muốn tắt hiệu ứng tuỳ chỉnh hoặc cung cấp hiệu ứng tuỳ chỉnh thay thế dựa trên nhiều khả năng tiềm năng.
Lập kế hoạch cho các lớp cấp cao sau đây về khả năng của thiết bị:
Nếu bạn đang sử dụng các thành phần gốc của phản hồi xúc giác: thiết bị hỗ trợ các thành phần gốc đó mà hiệu ứng tuỳ chỉnh cần đến. (Xem phần tiếp theo để biết thông tin chi tiết về các đối tượng gốc.)
Thiết bị có chế độ điều khiển biên độ.
Thiết bị có tính năng hỗ trợ rung cơ bản (bật/tắt) – nói cách khác, những thiết bị thiếu tính năng điều khiển biên độ.
Nếu lựa chọn hiệu ứng xúc giác của ứng dụng của bạn tính đến các danh mục này, thì trải nghiệm xúc giác của người dùng sẽ vẫn có thể dự đoán được trên mọi thiết bị.
Sử dụng các nguyên hàm xúc giác
Android bao gồm một số nguyên hàm haptics khác nhau về cả biên độ và tần số. Bạn có thể sử dụng một đối tượng gốc hoặc kết hợp nhiều đối tượng gốc để đạt được hiệu ứng xúc giác phong phú.
- Sử dụng độ trễ từ 50 mili giây trở lên cho các khoảng trống có thể nhận biết được giữa hai đối tượng gốc, đồng thời tính đến thời lượng đối tượng gốc nếu có thể.
- Sử dụng các thang đo có tỷ lệ chênh lệch từ 1,4 trở lên để người xem dễ nhận biết sự khác biệt về cường độ.
Sử dụng tỷ lệ 0,5, 0,7 và 1,0 để tạo phiên bản cường độ thấp, trung bình và cao của một đối tượng gốc.
Tạo kiểu rung tuỳ chỉnh
Các mẫ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 mẫ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 có thể dễ dàng nhận biết, nhưng độ rung dài đột ngột có thể làm 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 dần biên độ mục tiêu quá nhanh cũng có thể tạo ra tiếng ùm ùm. Đề xuất về việc thiết kế mẫu dạng sóng là làm mượt các chuyển đổi biên độ để tạo hiệu ứng tăng và giảm.
Mẫu: Mẫu tăng dần
Các dạng sóng được biểu thị dưới dạng VibrationEffect
với ba tham số:
- Timings (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 sóng hình.
- Amplitudes (Độ biên độ): độ biên độ rung mong muốn cho mỗi thời lượng đượ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ị cho 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à một ví dụ về dạng sóng xung nhịp hai lần với thời gian tạm dừng là 350 mili giây giữa các xung nhịp. Xung đầu tiên là một dải tăng dần mượt mà đến biên độ tối đa và xung thứ hai là một dải tăng 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 // Do not 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; // Do not repeat. vibrator.vibrate(VibrationEffect.createWaveform(timings, amplitudes, repeatIndex));
Mẫu: Lặp lại mẫu
Bạn cũng có thể phát sóng hình dạng sóng nhiều lần cho đến khi huỷ. Cách tạo sóng hình lặp lại là đặt tham số "lặp lại" không âm. Khi bạn phát một sóng hình lặp lại, độ rung sẽ tiếp tục cho đến khi bạn 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 đối với các sự kiện không liên tục cần có hành động của người dù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: Mẫu có phương án dự phòng
Việc kiểm soát biên độ của một tín hiệu rung là một chức năng phụ thuộc vào phần cứng. Việc phát sóng âm 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, bạn nên đảm bảo rằng mẫu của mình không tạo ra hiệu ứng ù tai 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 dưới dạng dự phòng.
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)); }
Tạo thành phần rung
Phần này trình bày các cách kết hợp các hiệu ứng này thành hiệu ứng tuỳ chỉnh dài hơn và phức tạp hơn, đồng thời khám phá các hiệu ứng xúc giác phong phú bằng cách sử dụng các tính năng phần cứng nâng cao hơn. Bạn có thể sử dụng các tổ hợp hiệu ứng thay đổi biên độ và tần số để tạo hiệu ứng xúc giác phức tạp hơn trên các thiết bị có bộ truyền động xúc giác có băng thông tần số rộng hơn.
Quy trình tạo các mẫu rung tuỳ chỉnh, được mô tả trước đó trên trang này, giải thích cách kiểm soát biên độ rung để tạo hiệu ứng mượt mà khi tăng và giảm. Công nghệ haptics phong phú cải thiện khái niệm này bằng cách khám phá phạm vi tần số rộng hơn của bộ rung thiết bị để làm cho hiệu ứng trở nên mượt mà hơn. Các dạng sóng này đặc biệt hiệu quả trong việc tạo hiệu ứng crescendo hoặc diminuendo.
Thành phần gốc của thành phần Compose, được mô tả ở trên trang này, do nhà sản xuất thiết bị triển khai. Chúng tạo ra một độ rung sắc nét, ngắn và dễ chịu, phù hợp với Nguyên tắc haptics để tạo ra cảm giác haptics 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 Giới thiệu về bộ truyền động rung.
Android không cung cấp phương án dự phòng cho các thành phần có các thành phần gốc không được hỗ trợ. Bạn nên thực hiện các bước sau:
Trước khi kích hoạt tính năng haptics 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 đối tượng gốc mà bạn đang sử dụng.
Tắt nhóm trải nghiệm nhất quán không được hỗ trợ, chứ không chỉ các hiệu ứng bị thiếu một thành phần gốc. Sau đây là thông tin khác về cách kiểm tra tính năng hỗ trợ của thiết bị.
Bạn có thể tạo hiệu ứng rung được kết hợp bằng VibrationEffect.Composition
.
Dưới đây là ví dụ về hiệu ứng tăng dần, sau đó là hiệu ứng nhấp 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());
Bạn có thể tạo một thành phần bằng cách thêm các thành phần gốc để phát theo trình tự. Mỗi đối tượng gốc cũng có thể mở rộng, vì vậy, bạn có thể kiểm soát biên độ của độ rung do mỗi đối tượng gốc tạo ra. Tỷ lệ đượ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ể cảm nhận được (gần như không).
Nếu bạn muốn tạo một phiên bản yếu và mạnh của cùng một đối tượng gốc, bạn nên sử dụng tỷ lệ chênh lệch từ 1, 4 trở lên để dễ dàng nhận thấy sự khác biệt về cường độ. Đừng cố tạo nhiều hơn 3 cấp độ cường độ của cùng một đối tượng 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, 0 để tạo phiên bản cường độ thấp, trung bình và cao của một đối tượng gốc.
Cấu trúc cũng có thể chỉ định độ trễ cần 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 nguyên hàm trước đó. Nhìn chung, khoảng cách 5 đến 10 mili giây giữa hai đối tượng gốc là quá ngắn để phát hiện được. Hãy cân nhắc sử dụng khoảng cách có thứ tự từ 50 mili giây trở lên nếu bạn muốn tạo khoảng cách rõ ràng giữa hai đối tượng gốc. 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());
Bạn có thể sử dụng các API sau để xác minh khả năng hỗ trợ thiết bị cho các đối tượng gốc 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 đối tượng gốc, sau đó quyết định đối tượng gốc nào sẽ được kết hợp dựa trên cấp độ hỗ trợ 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);
Mẫu: Kháng (với số lần đánh dấu thấp)
Bạn có thể kiểm soát biên độ của độ rung nguyên thuỷ để 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ệ có khoảng cách gần nhau để tạo hiệu ứng crescendo mượt mà của một đối tượng gốc. Độ trễ giữa các mẫu gốc liên tiếp cũng có thể được đặt độ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 trong khung hiển thị được điều khiển bằng cử chỉ kéo và được tăng cường bằng tính năng phản hồi xúc giác.

Hình 1. Hình dạng sóng này thể hiện gia tốc đầu ra của độ rung trên một 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ẫu: Mở rộng (với sự tăng và giảm)
Có hai phương thức gốc để tăng cường cường độ rung được cảm nhận: PRIMITIVE_QUICK_RISE
và PRIMITIVE_SLOW_RISE
.
Cả hai chiến dịch đề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 nguyên hàm để giảm dần, đó là PRIMITIVE_QUICK_FALL
.
Các đối tượng gốc này hoạt động hiệu quả hơn với nhau để tạo ra một đoạn sóng tăng cường độ rồi giảm dần. Bạn có thể căn chỉnh các đối tượng gốc theo tỷ lệ để ngăn chặn sự tăng đột biến về biên độ giữa các đối tượng gốc. Điều này cũng hiệu quả trong việc 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 hơn là 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 có thể tăng cường cảm giác mở rộng trong ảnh động. Việc kết hợp hiệu ứng tăng và giảm giúp làm nổi bật thao tác thu gọn ở cuối ảnh động.

Hình 2. Hình dạng sóng này thể hiện gia tốc đầu ra của độ rung trên một 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; } }
Mẫu: Lắc (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ị để giới thiệu hiệu ứng rung bất ngờ và dễ chịu là sử dụng PRIMITIVE_SPIN
.
Loại dữ liệu gốc này hiệu quả nhất khi được gọi nhiều lần. Việc nối nhiều vòng quay có thể tạo ra hiệu ứng lắc lư và không ổn định. Bạn có thể tăng cường hiệu ứng này bằng cách áp dụng tỷ lệ ngẫu nhiên trên mỗi hình ảnh gốc. Bạn cũng có thể thử nghiệm khoảng cách giữa các nguyên hàm xoay liên tiếp. Hai vòng quay 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 thời gian giữa các vòng quay từ 10 đến 50 mili giây sẽ tạo cảm giác xoay lỏng hơn và có thể được dùng để khớp với thời lượng của video hoặc ảnh động.
Bạn không nên sử dụng khoảng thời gian 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ẻ.
Dưới đây là ví dụ về một hình dạng đàn hồi bật trở lại sau khi được kéo xuống rồi thả ra. Hoạt ảnh được nâng cao bằng một cặp hiệu ứng xoay, được phát với cường độ khác nhau tương ứng với độ dịch chuyển của độ nảy.

Hình 3. Hình dạng sóng này thể hiện gia tốc đầu ra của độ rung trên một 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 [-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 [-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) } }
Mẫu: Tiếng ồn khi bật lại (có tiếng ồn)
Một ứng dụng nâng cao khác của hiệu ứng rung là mô phỏng các tương tác thực tế. PRIMITIVE_THUD
có thể tạo hiệu ứng mạnh mẽ và âm vang, có thể kết hợp với hình ảnh trực quan của 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 đơn giản được nâng cao bằng hiệu ứng âm thanh phát mỗi khi bóng bật ra khỏi đáy màn hình:

Hình 4. Hình dạng sóng này thể hiện gia tốc đầu ra của độ rung trên một 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; } } }); } }