이 페이지에서는 다양한 햅틱 API를 사용하여 Android 애플리케이션에서 맞춤 효과를 만드는 방법의 예를 설명합니다. 이 페이지의 많은 정보는 진동 액추에이터의 작동 방식에 대한 지식을 바탕으로 하므로 진동 액추에이터 기본 지침서를 읽어보는 것이 좋습니다.
이 페이지에는 다음 예시가 포함되어 있습니다.
추가 예시는 이벤트에 햅틱 반응 추가를 참고하고 항상 햅틱 디자인 원칙을 따르세요.
대체를 사용하여 기기 호환성 처리
맞춤 효과를 구현할 때는 다음 사항을 고려하세요.
- 이 효과에 필요한 기기 기능
- 기기에서 효과를 재생할 수 없는 경우 취해야 할 조치
Android 햅틱 API 참조에서는 앱이 일관된 전반적인 환경을 제공할 수 있도록 햅틱과 관련된 구성요소의 지원을 확인하는 방법을 자세히 설명합니다.
사용 사례에 따라 맞춤 효과를 사용 중지하거나 다양한 잠재적 기능을 기반으로 대체 맞춤 효과를 제공하는 것이 좋습니다.
다음과 같은 기기 기능의 대략적인 클래스를 계획합니다.
햅틱 원시 항목을 사용하는 경우: 맞춤 효과에 필요한 이러한 원시 항목을 지원하는 기기 (원시 항목에 관한 자세한 내용은 다음 섹션을 참고하세요.)
진폭 제어가 있는 기기
기본 진동 지원(켜기/끄기)이 있는 기기(즉, 진폭 제어가 없는 기기)
앱의 햅틱 효과 선택사항이 이러한 카테고리를 고려하는 경우 햅틱 사용자 환경은 모든 개별 기기에서 예측 가능해야 합니다.
햅틱 프리미티브 사용
Android에는 진폭과 주파수가 모두 다른 여러 햅틱 원시 값이 포함되어 있습니다. 하나의 프리미티브만 사용하거나 여러 프리미티브를 조합하여 풍부한 햅틱 효과를 얻을 수 있습니다.
- 두 원시 객체 간에 식별 가능한 간격이 있는 경우 50ms 이상의 지연 시간을 사용하고 가능하면 원시 객체 시간도 고려합니다.
- 강도 차이를 더 잘 인식할 수 있도록 비율이 1.4 이상으로 다른 눈금을 사용하세요.
0.5, 0.7, 1.0의 크기를 사용하여 원시 도형의 강도가 낮은 버전, 중간 버전, 높은 버전을 만듭니다.
맞춤 진동 패턴 만들기
진동 패턴은 알림 및 벨소리와 같은 주의를 환기하는 햅틱에 자주 사용됩니다. Vibrator
서비스는 시간이 지남에 따라 진동 진폭을 변경하는 긴 진동 패턴을 재생할 수 있습니다. 이러한 효과를 웨이브폼이라고 합니다.
파형 효과는 쉽게 인식할 수 있지만 조용한 환경에서 재생하면 갑작스러운 긴 진동으로 인해 사용자가 놀랄 수 있습니다. 타겟 진폭으로 너무 빠르게 전환하면 윙윙거리는 소리가 들릴 수도 있습니다. 파형 패턴을 설계할 때는 진폭 전환을 부드럽게 하여 상승 및 하강 효과를 만드는 것이 좋습니다.
샘플: 단계적 확대 패턴
웨이브폼은 세 가지 매개변수와 함께 VibrationEffect
로 표시됩니다.
- 타이밍: 각 웨이브폼 세그먼트의 재생 시간(밀리초) 배열입니다.
- Amplitudes: 첫 번째 인수에 지정된 각 기간의 원하는 진동 진폭으로, 0~255의 정수 값으로 표시되며, 0은 진동기 '꺼짐'을 나타내고 255는 기기의 최대 진폭을 나타냅니다.
- 반복 색인: 파형 반복을 시작하기 위해 첫 번째 인수에 지정된 배열의 색인 또는 패턴을 한 번만 재생해야 하는 경우 -1입니다.
다음은 펄스 사이에 350ms의 일시중지를 두고 두 번 펄스하는 파형의 예입니다. 첫 번째 펄스는 최대 진폭까지 원활하게 상승하는 램프이고 두 번째 펄스는 최대 진폭을 유지하기 위한 빠른 램프입니다. 끝에서 중지하는 것은 음수 반복 색인 값으로 정의됩니다.
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))
자바
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));
샘플: 반복 패턴
웨이브폼은 취소될 때까지 반복해서 재생할 수도 있습니다. 반복되는 웨이브폼을 만드는 방법은 0이 아닌 'repeat' 매개변수를 설정하는 것입니다. 반복되는 웨이브폼을 재생하면 서비스에서 명시적으로 취소될 때까지 진동이 계속됩니다.
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() }
자바
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(); }
이는 사용자 작업을 통해 확인해야 하는 간헐적인 이벤트에 매우 유용합니다. 이러한 이벤트의 예로는 수신 전화 및 트리거된 알람이 있습니다.
샘플: 대체가 있는 패턴
진동의 진폭을 제어하는 것은 하드웨어 종속 기능입니다. 이 기능이 없는 저가형 기기에서 파형을 재생하면 진폭 배열의 각 양수 항목의 최대 진폭으로 진동합니다. 앱에서 이러한 기기를 수용해야 하는 경우 해당 조건에서 재생할 때 패턴이 윙윙거리는 효과를 생성하지 않도록 하거나 대신 대체로 재생할 수 있는 더 간단한 켜기/끄기 패턴을 설계하는 것이 좋습니다.
Kotlin
if (vibrator.hasAmplitudeControl()) { vibrator.vibrate(VibrationEffect.createWaveform(smoothTimings, amplitudes, smoothRepeatIdx)) } else { vibrator.vibrate(VibrationEffect.createWaveform(onOffTimings, onOffRepeatIdx)) }
자바
if (vibrator.hasAmplitudeControl()) { vibrator.vibrate(VibrationEffect.createWaveform(smoothTimings, amplitudes, smoothRepeatIdx)); } else { vibrator.vibrate(VibrationEffect.createWaveform(onOffTimings, onOffRepeatIdx)); }
진동 구성 만들기
이 섹션에서는 이를 더 길고 복잡한 맞춤 효과로 구성하는 방법을 설명하고, 그 이상으로 더 고급 하드웨어 기능을 사용하여 풍부한 햅틱을 살펴봅니다. 진폭과 주파수를 다르게 하는 효과를 조합하여 주파수 대역폭이 더 넓은 햅틱 액추에이터가 있는 기기에서 더 복잡한 햅틱 효과를 만들 수 있습니다.
이 페이지 앞부분에 설명된 맞춤 진동 패턴을 만드는 프로세스에서는 진동 진폭을 제어하여 원활하게 상향 및 하향 램핑하는 효과를 만드는 방법을 설명합니다. 풍부한 햅틱은 기기 진동기의 더 넓은 주파수 범위를 탐색하여 효과를 더욱 부드럽게 만들어 이 개념을 개선합니다. 이러한 파형은 특히 크레셒도 또는 디미누엔도 효과를 만드는 데 효과적입니다.
이 페이지 앞부분에서 설명한 컴포지션 원시 값은 기기 제조업체에서 구현합니다. 명확한 햅틱을 위한 햅틱 원칙에 따라 선명하고 짧으며 기분 좋은 진동을 제공합니다. 이러한 기능과 작동 방식에 관한 자세한 내용은 진동 액추에이터 입문서를 참고하세요.
Android는 지원되지 않는 원시 요소가 있는 컴포지션에 대한 대체를 제공하지 않습니다. 다음 단계를 따르는 것이 좋습니다.
고급 햅틱을 활성화하기 전에 특정 기기가 사용 중인 모든 프리미티브를 지원하는지 확인합니다.
원시 요소가 누락된 효과뿐만 아니라 지원되지 않는 일관된 환경 세트를 사용 중지합니다. 기기 지원을 확인하는 방법에 관한 자세한 내용은 다음과 같습니다.
VibrationEffect.Composition
를 사용하여 컴포지션된 진동 효과를 만들 수 있습니다.
다음은 천천히 상승하는 효과와 그 뒤에 날카로운 클릭 효과가 오는 예입니다.
Kotlin
vibrator.vibrate( VibrationEffect.startComposition().addPrimitive( VibrationEffect.Composition.PRIMITIVE_SLOW_RISE ).addPrimitive( VibrationEffect.Composition.PRIMITIVE_CLICK ).compose() )
자바
vibrator.vibrate( VibrationEffect.startComposition() .addPrimitive(VibrationEffect.Composition.PRIMITIVE_SLOW_RISE) .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) .compose());
컴포지션은 순서대로 재생할 원시 항목을 추가하여 만듭니다. 각 원시도 확장 가능하므로 각 원시에서 생성되는 진동의 진폭을 제어할 수 있습니다. 크기는 0과 1 사이의 값으로 정의되며, 여기서 0은 사용자가 이 원시 요소를 (간신히) 느낄 수 있는 최소 진폭에 실제로 매핑됩니다.
동일한 원시 도형의 약한 버전과 강한 버전을 만들려면 강도의 차이를 쉽게 인식할 수 있도록 눈금이 1.4배 이상 차이가 나도록 하는 것이 좋습니다. 동일한 원시 도형의 세 개 이상의 강도 수준을 만들려고 하지 마세요. 지각적으로 구별되지 않기 때문입니다. 예를 들어 0.5, 0.7, 1.0의 크기를 사용하여 원시 도형의 낮은, 중간, 높은 강도 버전을 만듭니다.
컴포지션은 연속된 원시 항목 사이에 추가할 지연 시간을 지정할 수도 있습니다. 이 지연 시간은 이전 원시 값이 종료된 후 경과한 밀리초 단위로 표시됩니다. 일반적으로 두 프리미티브 간의 간격이 5~10ms이면 감지하기에는 너무 짧습니다. 두 프리미티브 사이에 구별 가능한 간격을 만들려면 50ms 이상 간격을 사용하는 것이 좋습니다. 지연이 있는 컴포지션의 예는 다음과 같습니다.
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() )
자바
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());
다음 API를 사용하여 특정 프리미티브에 대한 기기 지원을 확인할 수 있습니다.
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. }
자바
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. }
여러 개의 원시를 확인한 후 기기 지원 수준에 따라 컴포지션할 원시를 결정할 수도 있습니다.
Kotlin
val effects: IntArray = intArrayOf( VibrationEffect.Composition.PRIMITIVE_LOW_TICK, VibrationEffect.Composition.PRIMITIVE_TICK, VibrationEffect.Composition.PRIMITIVE_CLICK ) val supported: BooleanArray = vibrator.arePrimitivesSupported(primitives);
자바
int[] primitives = new int[] { VibrationEffect.Composition.PRIMITIVE_LOW_TICK, VibrationEffect.Composition.PRIMITIVE_TICK, VibrationEffect.Composition.PRIMITIVE_CLICK }; boolean[] supported = vibrator.arePrimitivesSupported(effects);
샘플: 저항 (낮은 틱 포함)
원시 진동의 진폭을 제어하여 진행 중인 작업에 유용한 의견을 전달할 수 있습니다. 간격이 좁은 크기 값을 사용하여 원시 도형의 원활한 크레센도 효과를 만들 수 있습니다. 연속된 원시 값 간의 지연 시간은 사용자 상호작용에 따라 동적으로 설정할 수도 있습니다. 이는 드래그 동작으로 제어되고 햅틱으로 보강된 뷰 애니메이션의 다음 예에서 확인할 수 있습니다.

그림 1. 이 웨이브폼은 기기의 진동 출력 가속을 나타냅니다.
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) } } }
자바
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); } }
샘플: 확장 (상승 및 하락 포함)
감지된 진동 강도를 높이는 두 가지 원시 값인 PRIMITIVE_QUICK_RISE
과 PRIMITIVE_SLOW_RISE
이 있습니다.
두 광고 모두 동일한 타겟에 도달하지만 광고 기간이 다릅니다. 감소 래핑에는 PRIMITIVE_QUICK_FALL
라는 단일 프리미티브가 있습니다.
이러한 원시 요소는 함께 사용하면 강도가 증가했다가 사라지는 웨이브폼 세그먼트를 더 효과적으로 만들 수 있습니다. 크기가 조정된 원시 요소를 정렬하여 원시 요소 간의 진폭이 갑자기 급증하는 것을 방지할 수 있으며, 이는 전반적인 효과 지속 시간을 연장하는 데도 효과적입니다. 사람들은 항상 하락하는 부분보다 상승하는 부분을 더 잘 인식하므로 상승하는 부분을 하락하는 부분보다 짧게 만들어 강조점을 하락하는 부분으로 전환할 수 있습니다.
다음은 원을 펼치고 접는 데 이 컴포지션을 적용한 예입니다. 상승 효과는 애니메이션 중에 확장감을 향상시킬 수 있습니다. 상승 및 하강 효과를 조합하면 애니메이션 끝부분의 축소를 강조할 수 있습니다.

그림 2. 이 웨이브폼은 기기의 진동 출력 가속을 나타냅니다.
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) } } }
자바
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; } }
샘플: 흔들림 (회전 포함)
햅틱 원칙 중 하나는 사용자에게 즐거움을 주는 것입니다. 예상치 못한 기분 좋은 진동 효과를 도입하는 재미있는 방법은 PRIMITIVE_SPIN
를 사용하는 것입니다.
이 프리미티브는 두 번 이상 호출될 때 가장 효과적입니다. 여러 스핀을 연결하면 흔들리고 불안정한 효과를 만들 수 있으며, 각 원시 요소에 다소 무작위로 크기 조정을 적용하여 이 효과를 더욱 향상할 수 있습니다. 연속된 스핀 원시 값 간의 간격을 실험해 볼 수도 있습니다. 간격 없이 두 번 회전하면 (사이에 0ms) 빡빡한 회전 느낌이 듭니다. 회전 간격을 10밀리초에서 50밀리초로 늘리면 회전 감각이 느슨해지며 동영상이나 애니메이션의 길이와 일치시키는 데 사용할 수 있습니다.
연속 회전이 더 이상 잘 통합되지 않고 개별 효과처럼 느껴지므로 100ms보다 긴 간격을 사용하는 것은 권장하지 않습니다.
다음은 아래로 드래그한 다음 놓으면 다시 튀어 오르는 탄성 도형의 예입니다. 애니메이션은 튀김 변위에 비례하는 다양한 강도로 재생되는 한 쌍의 회전 효과로 향상됩니다.

그림 3. 이 웨이브폼은 기기의 진동 출력 가속을 나타냅니다.
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) }
자바
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) } }
샘플: 튀김 (둔탁한 소리 포함)
진동 효과의 또 다른 고급 애플리케이션은 물리적 상호작용을 시뮬레이션하는 것입니다. PRIMITIVE_THUD
는 강력하고 잔잔한 효과를 만들 수 있으며, 이를 동영상이나 애니메이션에서 충격의 시각화와 함께 사용하면 전반적인 환경을 개선할 수 있습니다.
다음은 공이 화면 하단에서 튀어오를 때마다 재생되는 둔탁한 효과로 향상된 간단한 공 떨어뜨리기 애니메이션의 예입니다.

그림 4. 이 웨이브폼은 기기의 진동 출력 가속을 나타냅니다.
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; } } }); } }