このページでは、さまざまな ハプティクス API を使用して、Android アプリで標準のバイブレーション ウェーブフォームを超えるカスタム エフェクトを作成する方法の例について説明します。
このページでは、次の例を説明します。
- カスタム バイブレーション パターン
- 増加パターン: スムーズに開始されるパターン。
- 繰り返しパターン: 終わりのないパターン。
- フォールバックありのパターン: フォールバックのデモ。
- バイブレーションの構成
その他の例については、イベントに触覚フィードバックを追加するをご覧ください。また、常に触覚設計の原則に従ってください。
デバイスの互換性を処理するためにフォールバックを使用する
カスタム エフェクトを実装する際は、次の点を考慮してください。
- エフェクトに必要なデバイスの機能
- デバイスでエフェクトを再生できない場合の対応
Android ハプティクス API リファレンスでは、アプリで全体的な一貫したエクスペリエンスを提供できるように、ハプティクスに関連するコンポーネントのサポートを確認する方法について詳しく説明しています。
ユースケースに応じて、カスタム エフェクトを無効にしたり、さまざまな潜在的な機能に基づいて代替のカスタム エフェクトを提供したりできます。
デバイス機能の次の大まかなクラスを計画します。
ハプティクスのプリミティブを使用している場合: カスタム エフェクトに必要なプリミティブをサポートしているデバイス。(プリミティブの詳細については、次のセクションをご覧ください)。
振幅制御機能付きのデバイス。
バイブレーションの基本的なサポート(オン/オフ)が可能なデバイス(振幅の調整が不可能なデバイス)。
アプリのハプティクス エフェクトの選択でこれらのカテゴリを考慮すると、個々のデバイスでハプティクス ユーザー エクスペリエンスが予測可能になります。
ハプティクス プリミティブの使用
Android には、振幅と周波数の両方が異なる複数のハプティクス プリミティブが含まれています。1 つのプリミティブを単独で使用することも、複数のプリミティブを組み合わせて使用することもできます。これにより、豊かなハプティクス効果を実現できます。
- 2 つのプリミティブの間に認識できるギャップがある場合は、50 ミリ秒以上の遅延を使用します。可能であれば、プリミティブの持続時間も考慮してください。
- 強度の違いを認識しやすいように、比率が 1.4 以上のスケールを使用します。
スケールを 0.5、0.7、1.0 を使用して、プリミティブの低、中、高の強度バージョンを作成します。
カスタムのバイブレーション パターンを作成する
バイブレーション パターンは、通知や着信音などの注意喚起ハプティクスでよく使用されます。Vibrator
サービスは、時間の経過とともに振動振幅が変化する長い振動パターンを再生できます。このような効果を波形と呼びます。
通常、波形効果は認識できますが、静かな環境で再生すると、突然のロング バイブレーションでユーザーが驚く可能性があります。ターゲット振幅に急速に増加すると、耳障りなブーンというノイズが発生することもあります。振幅の遷移をスムーズにして、増加と減少の効果を作成するように波形パターンを設計します。
バイブレーション パターンの例
以降のセクションでは、バイブレーション パターンの例をいくつか示します。
ランプアップ パターン
波形は、3 つのパラメータを持つ VibrationEffect
として表されます。
- タイミング: 各波形セグメントの再生時間(ミリ秒単位)の配列。
- 振幅: 最初の引数で指定された各時間の所望の振幅。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 // 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));
繰り返しパターン
波形は、キャンセルされるまで繰り返し再生することもできます。繰り返し波形を作成するには、正の 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()
}
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();
}
これは、ユーザーの操作で確認が必要な断続的なイベントに非常に便利です。このようなイベントの例としては、着信やアラームのトリガーなどがあります。
フォールバックありのパターン
バイブレーションの振幅を制御する機能は、ハードウェアに依存する機能です。この機能のないローエンド デバイスで波形を再生すると、振幅配列内の正のエントリごとに最大振幅でデバイスが振動します。アプリでこのようなデバイスに対応する必要がある場合は、そのような状態で再生したときにハム音効果が発生しないパターンを使用するか、代わりにフォールバックとして再生できるシンプルなオン/オフ パターンを設計します。
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));
}
バイブレーションの構成を作成する
このセクションでは、バイブレーションを長く複雑なカスタム エフェクトに構成する方法について説明します。さらに、より高度なハードウェア機能を使用して豊かなハプティクスを探ります。振幅と周波数を変える効果を組み合わせて使用すると、周波数帯域が広いハプティクス アクチュエータ搭載デバイスで、より複雑な触覚効果を作成できます。
このページで説明したカスタム バイブレーション パターンを作成するプロセスでは、バイブレーション振幅を制御して、スムーズな増減効果を作成する方法を説明しています。リッチ ハプティクスは、デバイスのバイブレーターの周波数範囲を広げて効果をよりスムーズにすることで、このコンセプトを改善しています。これらの波形は、クレッシェンドやディミヌエンドの効果を作成する場合に特に効果的です。
このページの前半で説明したコンポジション プリミティブは、デバイス メーカーによって実装されます。クリア ハプティクスのハプティクスの原則に沿って、鮮明で短く心地よい振動を提供します。これらの機能とその仕組みの詳細については、振動アクチュエータの概要をご覧ください。
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 ミリ秒のギャップは短すぎて検出できません。2 つのプリミティブの間に認識できるギャップを作成する場合は、50 ms 以上のギャップを使用します。遅延のあるコンポジションの例を次に示します。
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);
バイブレーションの構成例
以降のセクションでは、GitHub のハプティクス サンプルアプリから取得したバイブレーションの構成例をいくつか示します。
抵抗(低いティック数)
プリミティブ バイブレーションの振幅を制御して、進行中のアクションに有用なフィードバックを伝達できます。スケール値を狭くすると、プリミティブの滑らかなクレッシェンド効果を作成できます。連続するプリミティブ間の遅延は、ユーザー操作に基づいて動的に設定することもできます。次の例は、ドラッグ ジェスチャーで制御され、ハプティクスで拡張されたビュー アニメーションを示しています。

図 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)
}
}
}
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);
}
}
拡大(上昇と下降)
知覚される振動強度を段階的に増やすためのプリミティブは、PRIMITIVE_QUICK_RISE
と PRIMITIVE_SLOW_RISE
の 2 つです。どちらも同じターゲットにリーチしますが、有効期間が異なります。減速プリミティブは 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)
}
}
}
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 ms から 50 ms に増やすと、スピンの感覚が緩和されます。動画やアニメーションの長さに合うように使用できます。
100 ミリ秒を超えるギャップは使用しないでください。連続するスピンの効果が統合されなくなり、個々のエフェクトのように見えます。
以下は、下にドラッグして放した後に跳ね返る弾性シェイプの例です。アニメーションは、バウンドの移動距離に比例するさまざまな強度で再生される 2 つの回転効果で強化されます。

図 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 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)
}
}
バウンス(ドンドンという音)
バイブレーション効果の高度な応用として、物理的な操作をシミュレートすることもできます。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;
}
}
});
}
}