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 ngoài đường dạng sóng rung chuẩn trong ứng dụng Android.
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ó dự phòng: Minh hoạ 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 cần có của thiết bị để có 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 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 dùng 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 tính năng 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à sóng.
Hiệu ứng dạng sóng thường dễ nhận biết, nhưng độ rung 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 cường độ mục tiêu quá nhanh cũng có thể tạo ra tiếng ù. 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
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.
- Độ 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 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à 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 // 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 sóng hình dạng sóng nhiều lần cho đến khi huỷ. Cách tạo sóng lặp lại là đặt tham số repeat
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 được 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 có 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 một dạng sóng trên thiết bị cấp thấp mà 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 điều chỉnh cho phù hợp với 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 làm 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 độ rung thành các 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 tính năng cảm ứng đa dạng 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.
Quá trình tạo kiể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 tăng và giảm mượt 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 mang lại cảm giác rung rõ ràng, ngắn và dễ chịu, phù hợp với nguyên tắc haptics để mang lại 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ợ. Do đó, hãy 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.
Thông tin khác về cách kiểm tra khả năng hỗ trợ của thiết bị sẽ được trình bày trong các phần sau.
Tạo hiệu ứng rung kết hợp
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 chậm, theo 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).
Tạo biến thể trong các nguyên hàm rung
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, 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 3 cấp độ cường độ của cùng một nguyên hàm, 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 các phiên bản cường độ thấp, trung bình và cao của một đối tượng gốc.
Thêm khoảng trống giữa các nguyên hàm rung
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. Sử dụng khoảng cách khoảng 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 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 các loại 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ợ 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);
Ví dụ về 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, được lấy từ ứng dụng mẫu haptics trên GitHub.
Kháng cự (với số lần đánh dấu thấp)
Bạn có thể kiểm soát biên độ của chế độ 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ệ cách nhau 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 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 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ở rộng (với sự tăng và giảm)
Có hai phương thức gốc để tăng cường độ rung được cảm nhận: 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 hàm gốc để giảm dần, PRIMITIVE_QUICK_FALL
. Các đối tượng gốc này hoạt động hiệu quả hơn cùng 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 cảm nhận, mọi người luôn chú ý đến phần tăng nhiều hơn phần giảm, vì vậy, bạn có thể sử dụng phần tăng ngắn hơn phần giảm để 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.Đường 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;
}
}
Lắc lư (có vòng quay)
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 từng 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 quay liên tiếp. Hai vòng quay không có khoảng trống (0 ms ở 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 lên 50 ms 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.
Đừng sử dụng khoảng thời gian dài hơn 100 ms, 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 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)
}
}
Bật nảy (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 được nâng cao bằng hiệu ứng tiếng ồn phát ra 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;
}
}
});
}
}