创建自定义触感反馈效果

本页面将举例说明如何使用不同的触感反馈 API 在 Android 应用中创建自定义效果。我们网站上提供的大部分信息 本页依靠的是对振动致动器工作方式的深入了解, 我们建议您阅读振动致动器入门指南

本页面包含以下示例。

如需查看更多示例,请参阅为事件添加触感反馈。 始终遵循触感反馈设计原则

使用回退机制处理设备兼容性

在实现任何自定义效果时,请考虑以下事项:

  • 呈现效果所需的设备功能
  • 当设备无法播放该特效时该怎么做

Android 触感反馈 API 参考文档详细介绍了如何检查 支持触感反馈中涉及的组件,以便您的应用可以提供 提供一致的整体体验

根据您的用例,您可能需要停用自定义效果或 根据不同潜在功能提供替代的自定义效果。

针对以下高级类别的设备功能进行规划:

  • 如果您使用的是触感反馈基元:设备支持这些基元 自定义效果所需的全部尺寸(如需详细了解 基元。)

  • 具有振幅控制功能的设备。

  • 具有基本振动支持(开/关)的设备,换言之, 缺少振幅控制。

如果应用的触感反馈效果选择考虑了这些类别,那么 任何单个设备的触感反馈用户体验都应保持可预测。

触感反馈基元的使用

Android 包含几个在幅度和大小方面有所不同的触感反馈基元 频率。您可以单独使用一个基元,也可以组合使用多个基元 实现丰富的触感反馈效果。

  • 采用 50 毫秒或更长的延迟时间,确保两个视频之间的可辨别间隙 基元,同时要考虑到 基元 时长
  • 请使用比率差异为 1.4 或更大的比例,这样差异 并且感觉强度更好
  • 使用范围 0.5、0.7 和 1.0 创建低、中、高 强度版本。

创建自定义振动模式

振动模式通常用于注意力触感反馈,例如通知 和铃声。Vibrator 服务可以播放长时间振动模式, 改变振动幅度。此类效应被命名为波形。

波形效应很容易察觉,但突然的长时间振动 如果在安静的环境中播放,会让用户受到惊吓。推进到目标振幅 过快也可能会产生嗡嗡声。对于 设计波形模式是让振幅过渡平滑, 渐弱效果

示例:公开范围渐增模式

波形表示为带有三个参数的 VibrationEffect

  1. Timings:每个波形的时长数组(以毫秒为单位) 细分。
  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));

示例:重复模式

波形也可以重复播放,直到取消为止。创建 设置一个非负“重复”参数。当你播放 重复波形,振动会一直持续,直到在 服务:

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

这对于需要用户执行操作的间歇性事件非常有用 确认这一点此类事件的示例包括来电和 已触发的闹钟。

示例:包含后备信息的模式

控制振动幅度是 依赖于硬件的功能:在 不具备此功能的低端设备在达到最大振动值时振动 amplitude 数组中每个正条目的 amplitude。如果您的应用需要 那么建议您确保 在这种情况下,这种模式就不会产生嗡嗡声,或者 设计一个更简单的 ON/OFF 模式,该模式可以作为后备广告来播放。

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

创建振动乐曲

本部分介绍了将它们组成 更长、更复杂的自定义效果, 以及使用更高级硬件功能的触感反馈您可以将 可调整振幅和频率的音效,从而产生更复杂的触感反馈效果 在配备具有更宽频率带宽的触感致动器的设备上。

创建自定义振动设置的流程 (如本页前所述), 解释了如何控制振动振幅来产生平滑效果, 不断增长和下降。丰富的触感反馈改进了这一概念,具体方法是探索 扩大设备振动器的频率范围,以使效果更平滑。 这些波形在使声音渐强或渐弱方面特别有效 效果。

本页前面所述的组合基元由 设备制造商它们能发出清脆、短暂而愉悦的振动 符合触感反馈原则,实现清晰触感反馈。有关 有关这些功能及其工作原理的详细信息,请参阅振动致动器 入门指南

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