このページでは、さまざまなハプティクス API を使用して、 Android アプリでカスタム エフェクトを作成する。クラウド インフラストラクチャに関する このページは、振動アクチュエータの仕組みをよく理解していることを前提としています。 バイブレーション アクチュエータの基礎知識もご一読ください。
このページでは、次の例について説明します。
- カスタムのバイブレーション パターン
<ph type="x-smartling-placeholder">
- </ph>
- ランプアップ パターン: 滑らかに始まるパターン。
- 繰り返しパターン: 終わりのないパターン。
- フォールバックありのパターン: フォールバック 見てみましょう。
- バイブレーション コンポジション <ph type="x-smartling-placeholder">
その他の例については、イベントに触覚フィードバックを追加するをご覧ください。 ハプティクスの設計原則に従うこと。
フォールバックを使用してデバイスの互換性を処理する
カスタム エフェクトを実装する際は、次の点を考慮してください。
- エフェクトに必要なデバイス機能
- デバイスがエフェクトを再生できない場合の対処方法
Android ハプティクス API リファレンスに、 ハプティクスに関わるコンポーネントをサポートすることで、アプリが 全体的なエクスペリエンスが向上します
ユースケースに応じて、カスタム エフェクトを無効にしたり、 使用できるさまざまな機能に基づいて代替カスタム効果を使用できます。
デバイス機能の上位クラスについて計画します。
触覚プリミティブを使用する場合: ハプティクス プリミティブをサポートするデバイス 必要があります。(詳細については、次のセクションで 使用しないでください)。
振幅制御を備えたデバイス。
基本バイブレーションのサポート(オン/オフ)を備えたデバイス、つまり 振幅制御が欠如しています
アプリの触覚効果の選択がこれらのカテゴリを考慮している場合、 触覚のユーザー エクスペリエンスは、どのデバイスでも予測可能である必要があります。
触覚プリミティブの使用
Android には、振幅と応答の両面で異なるハプティクス プリミティブがいくつか用意されています。 できます。1 つのプリミティブを単独で使用することも、複数のプリミティブを組み合わせて使用することもできます。 リッチな触覚効果を実現できます
- 50 ms 以上の遅延を設定して、2 つのコンテンツの間に区別できる また、プリミティブ 期間 おすすめします
- 比率が 1.4 以上の異なる尺度を使用し、 感じられるようになります。
0.5、0.7、1.0 のスケールを使用して、低、中、高を作成します 強度バージョンを返します。
カスタムのバイブレーション パターンを作成する
バイブレーション パターンは、通知などのアテンション ハプティクスでよく使用されます
作成できますVibrator
サービスは、次のような長いバイブレーション パターンを再生することができます。
バイブレーションの振幅を経時的に変化させます。このような効果を波形と呼びます。
波形の効果は容易に認識できますが、突然の長いバイブレーションでは 静かな環境でプレイするとユーザーに驚かされる。目標とする振幅に到達 また、速すぎるとブーンという雑音が発生する可能性があります。P-MAX は 波形パターンの設計では、振幅遷移を滑らかにして、 影響します
サンプル: 拡大パターン
波形は、次の 3 つのパラメータを持つ VibrationEffect
として表されます。
- Timings: 各波形の継続時間(ミリ秒単位)の配列 セグメントです
- 振幅: 指定された時間ごとの望ましい振動振幅 最初の引数は、0 から 255 までの整数値で表され、0 は バイブレーターを「オフ」に255 がデバイスの最大値です あります。
- 繰り返しインデックス: 最初の引数で指定した配列のインデックスを 波形の繰り返しを開始します。パターンを 1 回だけ再生する場合は -1 になります。
350 ms の一時停止を挟んで 2 回パルスする波形の例 検出します。最初のパルスは、最大振幅まで滑らかに立ち上がり、 2 つ目は、最大振幅を維持するための迅速なランプです。「終了時」の停止は、 負の繰り返しインデックス値で計算されます。
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));
サンプル: 繰り返しパターン
波形は、キャンセルされるまで繰り返し再生することもできます。作成方法は、 非負の「繰り返し」パラメータを設定します。アテンション機構を 振動は、明示的にキャンセルされるまで継続します。 必要があります。
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(); }
これは、ユーザーによる操作が必要な断続的なイベントに非常に便利です。 確認する必要があります。このようなイベントの例として、電話の着信、 トリガーされたアラームの数。
サンプル: フォールバックのパターン
バイブレーションの振幅を制御することは、 ハードウェアに依存する機能。別のデバイスで波形を再生すると、 この機能がないローエンド デバイスでは、最大振動が発生します。 振幅配列の各正のエントリの振幅を 1 つずつ返します。アプリで 対応している場合は、そのデバイスに応じて、 その状態でパターンを再生してもブーンという雑音が聞こえない。 代わりに、フォールバックとして再生できるシンプルなオン/オフのパターンを設計します。
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)); }
バイブレーション コンポジションを作成する
このセクションでは、 より長く複雑なカスタム エフェクトを使用でき、さらにリッチなカスタム エフェクトも より高度なハードウェア機能を使用して以下の組み合わせを使用できます。 より複雑な触覚効果を生み出すために振幅と周波数を変化させる効果 より広い周波数帯域を持つ触覚アクチュエータを備えたデバイスに適しています。
カスタム バイブレーションの作成プロセス 使用する場合は、このページですでに説明したパターンを使用します。 は、振動の振幅を制御して、 スケールアップ/ダウンしますリッチ ハプティクスでは、 デバイス バイブレーターの周波数範囲を広くして、効果をさらに滑らかにします。 これらの波形は、クレッシェンドやディミヌエンドの音を出すのに特に効果的です できます。
このページで前述した合成プリミティブは、 。シャープ、短く、心地よい振動を実現します これは、クリア ハプティクスのハプティクスの原則に沿ったものです。詳細 これらの機能の詳細と仕組みについては、バイブレーション アクチュエータをご覧ください primer をご覧ください。
Android では、サポートされていないコンポジションのフォールバックは提供されません。 説明します。次の手順を行うことをおすすめします。
高度なハプティクスを有効にする前に、お使いのデバイスがサポートしていることをご確認ください すべてのプリミティブがチェックされます
特定のプロパティだけでなく、サポート対象外の一貫したエクスペリエンスを 新しいプリミティブが存在しない場合です。ステータスを確認する方法について詳しくは、 次のように表示されます。
VibrationEffect.Composition
で合成振動効果を作成できます。
以下は、ゆっくりと立ち上がる効果の後に鋭いクリック効果が加わる例です。
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());
コンポジションは、順番に再生されるプリミティブを追加することで作成されます。各 スケーラブルであるため、バイブレーションの振幅を 自動的に生成されます。スケールは 0 ~ 1 の値として定義されます。 ここで 0 は、このプリミティブが出力される最小振幅に 感じた程度です。
同じプリミティブの弱いバージョンと強いバージョンを作成する場合は、 1.4 以上の比率で差があることが推奨されます。 簡単に認識できます。3 個を超えるアセットは作成しない 同じプリミティブの輝度レベルが与えられます。これは、 あります。たとえば、0.5、0.7、1.0 のスケールを使用して、低、中、高 高強度バージョンのプリミティブモデルです。
コンポジションでは、連続する時間の間に追加される遅延を指定することもできます。 説明します。この遅延は、 あります。一般的に、2 つのプリミティブの間に 5 ~ 10 ミリ秒のギャップがあると、 短くする必要があります。50 ミリ秒以上のギャップの使用を検討する 2 つのプリミティブの間に識別可能なギャップを作成する場合に使用します。こちらの 遅延のある構成の例:
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());
次の 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. }
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. }
複数のプリミティブを確認して、どのプリミティブを使用するかを決定することもできます。 デバイスのサポートレベルに応じて作成します。
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);
サンプル: 抵抗する(目盛りの小さい)
プリミティブ バイブレーションの振幅を制御することで、 有用なフィードバックを提供できます間隔を空けたスケール値は、 を使って、プリミティブの滑らかなクレッシェンド効果を作り出します。次の遅延は 連続するプリミティブを、ユーザーのリクエストに基づいて動的に設定することもできます。 です。これを、次のビュー アニメーションの例で示します。 ドラッグ操作で制御でき、触覚で拡張されています。
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); } }
サンプル: 拡大(上昇と下降あり)
知覚振動強度を上げるためのプリミティブが 2 つあります。
PRIMITIVE_QUICK_RISE
および
PRIMITIVE_SLOW_RISE
。
どちらも同じターゲットに到達しますが、期間は異なります。1 つのみ
変更する場合は、
PRIMITIVE_QUICK_FALL
。
これらのプリミティブは連携して動作し、
そして消えていきます。スケーリングされたプリミティブを調整して、
振幅がジャンプします。これは、全体的な拡散を
効果時間。人は見たところ、
立ち下がり部分を短くすると
強調を下げている部分に移すために使用します。
これは、この構成を応用して拡張と拡張を実現する例です。 できます。ライズ効果は、 追加します。上昇効果と下降効果を組み合わせることで、 閉じることもできます。
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; } }
サンプル: ぐらつき(スピンあり)
ハプティクスの原則の 1 つは、ユーザーに喜ばれることです。楽しい方法
予想外の振動効果をもたらすには、
PRIMITIVE_SPIN
。
このプリミティブは、複数回呼び出される場合に最も効果的です。複数
回転が連結されると、ぐらつきや不安定な影響が生じることがあります。
各プリミティブにある程度ランダムなスケーリングを適用して、さらに拡張します。マイページ
連続するスピン プリミティブ間のギャップを試すこともできます。2 回回転
ギャップなし(間は 0 ms)は締め付けられた感覚を作り出します。上昇:
スピン間のギャップが 10 ~ 50 ms の場合、回転する感覚が緩くなります。
動画やアニメーションの再生時間に合わせることができます。
100 ms を超える間隔を連続的に使用することはおすすめしません。 スピンがうまく統合されなくなり、個別のエフェクトのように感じられ始めます。
下にドラッグすると弾力性のあるシェイプが跳ね上がる例 リリースされます。アニメーションは 2 つのスピン効果で強化され、再生される バウンド変位に比例して強度が変化します。
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) } }
例: バウンド(音が鳴る)
振動効果の高度な応用として、物理的な振動をシミュレーションする
やり取りできます「
PRIMITIVE_THUD
強い反響効果を生み出すことができます。これは
視覚効果を動画やアニメーションなどで可視化し、
向上させることができます
サウンド効果で強化したシンプルなボールドロップ アニメーションの例 ボールが画面下に跳ねてくるたびに発生します。
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; } } }); } }