建立自訂觸覺技術效果

本頁面將舉例說明如何使用不同的觸覺技術 API 在 Android 應用程式中建立自訂特效。這裡有很多 必須充分瞭解震動致動器的運作方式 建議你參閱震動致動器入門

本頁麵包含下列範例。

如需其他範例,請參閱為活動新增觸覺回饋一文,以及 一律遵守觸覺技術設計原則

使用備用機制處理裝置相容性

實作自訂效果時,請考量以下幾點:

  • 效果所需的裝置功能
  • 裝置無法播放效果時的處理方式

Android haptics API 參考資料詳細說明如何檢查 對觸覺技術相關元件提供支援,讓應用程式能夠提供 一致的整體體驗

視用途而定,建議您停用自訂效果 並根據不同潛在功能提供替代自訂特效。

規劃下列高階裝置功能類別:

  • 如果你使用觸覺基元:支援這些基元的裝置 自訂效果所需的圖片(詳情請參閱下節)。 一些基本內容)

  • 具有振幅控制的裝置。

  • 支援「基本」震動功能的裝置 (開啟/關閉),也就是 缺乏強力控制

如果應用程式的觸覺效應選擇符合上述類別, 觸覺使用者體驗應可預測,適用於所有個別裝置。

使用觸覺基元

Android 提供數種觸覺回饋基元,各種振幅和 頻率。您可以使用單一原始版本,也可以將多個基元搭配運用 能帶來豐富的觸覺回饋

  • 誤點 50 毫秒或更久,以利辨別間隔 但也考量到 時間長度 可以的話
  • 使用比例差為 1.4 以上的量表, 強度較容易察覺。
  • 以 0.5、0.7 和 1.0 分段建立低、中、高 原始的強度版本

建立自訂震動模式

震動模式通常用於注意力觸覺回饋 (例如通知) 和鈴聲。Vibrator 服務可播放較長的震動模式, 隨時間調整振動振幅。這類效果的名稱為波形。

波形效果很容易察覺,但如果震動過長,就會變得難以察覺 在安靜的環境中播放時可啟動使用者。逐漸達到目標振幅 速度過快也可能會產生嗡嗡聲。建議的 打造波形模式的目的,是讓振幅轉場順暢出現 增加與減少效應的影響

範例:效能提升模式

波形會以 VibrationEffect 表示,其中包含三個參數:

  1. 時機:每個波形的持續時間陣列 (以毫秒為單位)。 區隔
  2. 音量:指定各指定時間長度的震動振幅 填入第一個引數,以 0 到 255 之間的整數值表示,其中 0 代表震動器「關閉」最大是 255 振幅
  3. 重複索引:在第一個引數中指定的陣列中,為 開始重複波形,如果應該只播放模式一次,則按 -1。

以下的波形範例會跳動兩次,每次間隔約 350 毫秒 脈衝。第一脈衝刺得達到最大振幅, 第二步則是快速吸收最大振幅的快速坡道。「在結尾處停止」的定義 乘以負重複索引值

KotlinJava
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));

範例:重複模式

也可以反覆播放波形,直到取消為止。建立 Deployment 波形是指設定非負數的「repeat」參數當你在 一直重複波形,振動會持續,直到在 服務:

KotlinJava
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();
}

若是需要使用者採取行動的間歇事件,這個做法就非常實用 我知道了。這類事件包括來電和 觸發的鬧鐘

範例:包含備用的模式

控制振動振幅是 硬體相關功能。改用 如果不具備這項功能的低階裝置,將以最大震動的方式震動 振幅陣列中每個陽性項目的振幅。如果您的應用程式需要 我們就建議您確保 在該條件下播放時,模式不會發出嗡嗡聲,或是 設計一個較簡單的開啟/關閉模式,並做為備用的播放方式。

KotlinJava
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));
}

建立震動組合

本節說明幾種撰寫方式 以及更複雜的自訂效果 強化觸覺回饋您可以將 增加振幅和頻率的效果,創造出更複雜的觸覺效果 裝置會在頻率頻寬較高的裝置上,顯示觸覺技術致動器。

建立自訂震動程序 本頁內容 說明如何控制振動振動,以產生流暢的 不斷成長豐富的觸覺回饋改良了這個概念 裝置振動頻率範圍越大,效果就更加平靜。 這些波形在建立漸強或漸弱時特別有效 效果。

本頁前述的樂曲「基本」實作方式是由 從裝置製造商開始手錶能夠提供鮮明清晰的震動畫面 。如要 如要進一步瞭解這些功能及其運作方式,請參閱震動致動器 primer

如果組合不受支援,Android 無法提供備用選項 以及一些基本問題建議您執行下列步驟:

  1. 啟用進階觸覺回饋前,請先確認指定裝置是否支援 您使用的所有基本功能

  2. 停用一致的體驗組合,而非只有 缺少基本效果如要進一步瞭解如何查看 支援的裝置如下:

你可以使用 VibrationEffect.Composition 建立組成的震動效果。 以下是緩慢上升效果加上銳利點擊效果的例子:

KotlinJava
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 至 10 毫秒的差距 容易偵測建議以 50 毫秒或更長時間的順序使用間隔 如果您想在兩個基元之間建立明顯的差距以下是 延遲的組合範例:

KotlinJava
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 可用於驗證特定裝置對裝置的支援情形 基元:

KotlinJava
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.
}

你也可以檢查多個基元,然後決定要採用哪些基元 根據裝置支援等級撰寫:

KotlinJava
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);

範例:反抗 (低滴答)

你可以控制用於傳輸的原始震動振幅 與執行中動作相關的實用意見回饋。鄰近間距的縮放值 可以創造出原始的流暢漸強效果介於 而連續的基元也可供根據使用者動態設定 互動。如以下視圖動畫範例所示 以拖曳手勢控制,並利用觸覺技術擴增。

向下拖曳圓圈的動畫
輸入震動波形圖
KotlinJava
@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。 這些基元相輔相成,創造出長片 強度然後損失您可以對齊縮放基元,避免突然移動 而且只是大幅拓展 效果時間長度。可想而知,使用者通常會發現 因此,讓出自下滑的部分更短 用來將強調效果移至下降部分

以下是此組合的應用範例 收合圓形。上升效果可在 動畫。結合升降和跌倒效果可以強調 在動畫結束時收合。

展開式圓形的動畫
輸入震動波形圖
KotlinJava
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。 如果多次呼叫此基元,這個基元效果最佳。多個 將轉場串連起來,可能會造成晃動且不穩定, 來進一步強化。個人中心 也可以嘗試各種衍生基元之間的差距。兩輪旋轉式 沒有空隙 (介於 0 毫秒) 會產生緊張刺激的感覺。增加 間歇性陷阱從 10 到 50 毫秒的空缺就會引發如火如荼的感覺, 可用來比對影片或動畫的時間長度。

我們不建議使用超過 100 毫秒的間隔,做為連續的 讓旋律不再完美整合,看起來會像個別特效。

以下是在向下拖曳後彈回的彈性形狀範例 然後再放開。使用一組旋轉特效讓動畫更美觀, 並點出與彈跳位成比例差不多的

彈性形狀彈跳的動畫
輸入震動波形圖
KotlinJava
@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敬上 就能建立強而有力的效果 在影片或動畫中以視覺化方式呈現影響力 以及整體體驗

以下範例是運用黏合效果強化的簡易球投動畫 每當球從畫面底部彈跳時播放:

掉落球從畫面底部彈跳的動畫
輸入震動波形圖
KotlinJava
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)
   
}
 
}
}
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;
         
}
       
}
     
});
 
}
}