Esta página aborda exemplos de como usar diferentes APIs de retorno tátil para criar efeitos personalizados em um aplicativo Android. Como a maioria das informações desta página depende de um bom conhecimento do funcionamento de um atuador de vibração, recomendamos a leitura do manual do atuador de vibração.
Esta página inclui os seguintes exemplos.
- Padrões de vibração personalizados
- Padrão de aceleração: um padrão que começa de forma suave.
- Padrão repetitivo: um padrão sem fim.
- Padrão com substituto: uma demonstração de substituto.
- Composições de vibração
- Resistência: um efeito de arrasto com intensidade dinâmica.
- Expandir: um efeito de aumento e queda.
- Wobble: um efeito de oscilação que usa a primitiva
SPIN
. - Bounce: um efeito de salto usando a primitiva
THUD
.
Para mais exemplos, consulte Adicionar retorno tátil a eventos e siga sempre os princípios de design tátil.
Usar substitutos para lidar com a compatibilidade do dispositivo
Ao implementar qualquer efeito personalizado, considere o seguinte:
- Quais recursos do dispositivo são necessários para o efeito
- O que fazer quando o dispositivo não consegue reproduzir o efeito
A referência da API de retorno tátil do Android fornece detalhes sobre como verificar o suporte a componentes envolvidos em retorno tátil para que o app possa oferecer uma experiência geral consistente.
Dependendo do caso de uso, talvez seja necessário desativar os efeitos personalizados ou oferecer efeitos personalizados alternativos com base em diferentes recursos.
Planeje as seguintes classes de alto nível de recursos do dispositivo:
Se você estiver usando primitivas hápticas: dispositivos que oferecem suporte a essas primitivas necessárias para os efeitos personalizados. Consulte a próxima seção para saber mais sobre primitivas.
Dispositivos com controle de amplitude.
Dispositivos com suporte básico a vibração (ativado/desativado), ou seja, aqueles que não têm controle de amplitude.
Se a escolha de efeitos hápticos do seu app for responsável por essas categorias, a experiência do usuário háptico vai permanecer previsível para qualquer dispositivo.
Uso de primitivas táteis
O Android inclui várias primitivas de retorno tátil que variam em amplitude e frequência. É possível usar uma primitiva sozinha ou várias em combinação para conseguir efeitos hápticos avançados.
- Use atrasos de 50 ms ou mais para intervalos perceptíveis entre duas primitivas, considerando também a duração primitiva se possível.
- Use escalas que diferem em uma proporção de 1,4 ou mais para que a diferença na intensidade seja melhor percebida.
Use escalas de 0,5, 0,7 e 1,0 para criar uma versão de intensidade baixa, média e alta de uma primitiva.
Criar padrões de vibração personalizados
Os padrões de vibração são usados com frequência em hápticas de atenção, como notificações
e toques de chamada. O serviço Vibrator
pode reproduzir padrões de vibração longos que
mudam a amplitude da vibração ao longo do tempo. Esses efeitos são chamados de formas de onda.
Os efeitos de forma de onda podem ser facilmente percebidos, mas vibrações longas e repentinas podem assustar o usuário se tocadas em um ambiente silencioso. Aumentar a amplitude desejada muito rápido também pode produzir ruídos audíveis. A recomendação para projetar padrões de forma de onda é suavizar as transições de amplitude para criar efeitos de aumento e diminuição.
Exemplo: padrão de aceleração
As formas de onda são representadas como VibrationEffect
com três parâmetros:
- Tempos:uma matriz de durações, em milissegundos, para cada segmento de forma de onda.
- Amplitudes:a amplitude de vibração desejada para cada duração especificada no primeiro argumento, representada por um valor inteiro de 0 a 255, sendo que 0 representa o vibrador "desativado" e 255 é a amplitude máxima do dispositivo.
- Índice de repetição:o índice na matriz especificado no primeiro argumento para iniciar a repetição da forma de onda ou -1 se o padrão for reproduzido apenas uma vez.
Confira um exemplo de forma de onda que pulsa duas vezes com uma pausa de 350 ms entre os pulsos. O primeiro pulso é uma rampa suave até a amplitude máxima, e o segundo é uma rampa rápida para manter a amplitude máxima. A parada no final é definida pelo valor negativo do índice de repetição.
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));
Exemplo: padrão repetitivo
As formas de onda também podem ser reproduzidas repetidamente até serem canceladas. Para criar uma forma de onda repetida, defina um parâmetro "repetição" não negativo. Quando você reproduz uma forma de onda repetida, a vibração continua até ser cancelada explicitamente no serviço:
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(); }
Isso é muito útil para eventos intermitentes que exigem uma ação do usuário para ser reconhecido. Exemplos desses eventos incluem ligações recebidas e alarmes acionados.
Exemplo: padrão com substituto
Controlar a amplitude de uma vibração é um recurso dependente de hardware. A reprodução de uma forma de onda em um dispositivo de baixo custo sem esse recurso faz com que ele vibre na amplitude máxima para cada entrada positiva na matriz de amplitude. Se o app precisar acomodar esses dispositivos, recomendamos que o padrão não gere um efeito de zumbido quando reproduzido nessa condição ou projete um padrão LIGADO/DESLIGADO mais simples que possa ser reproduzido como substituto.
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)); }
Criar composições de vibração
Esta seção apresenta maneiras de combiná-los em efeitos personalizados mais longos e complexos, além de explorar haptics avançados usando recursos de hardware mais avançados. É possível usar combinações de efeitos que variam a amplitude e a frequência para criar efeitos táteis mais complexos em dispositivos com atuadores que têm uma largura de banda de frequência mais ampla.
O processo de criação de padrões de vibração personalizados, descrito anteriormente nesta página, explica como controlar a amplitude de vibração para criar efeitos suaves de aumento e diminuição. A haptics avançada melhora esse conceito explorando a gama de frequência mais ampla do vibrador do dispositivo para tornar o efeito ainda mais suave. Essas formas de onda são especialmente eficazes para criar um efeito de crescendo ou diminuir.
As primitivas de composição, descritas anteriormente nesta página, são implementadas pelo fabricante do dispositivo. Eles oferecem uma vibração nítida, curta e agradável que se alinha aos princípios de retorno tátil para retorno tátil claro. Para mais detalhes sobre esses recursos e como eles funcionam, consulte Introdução aos atuadores de vibração.
O Android não oferece substitutos para composições com primitivas sem suporte. Recomendamos que você siga estas etapas:
Antes de ativar a haptics avançada, verifique se um determinado dispositivo oferece suporte a todas as primitivas que você está usando.
Desative o conjunto consistente de experiências sem suporte, não apenas os efeitos que estão sem uma primitiva. Confira abaixo mais informações sobre como verificar o suporte do dispositivo.
É possível criar efeitos de vibração compostos com VibrationEffect.Composition
.
Confira um exemplo de efeito de aumento lento seguido de um efeito de clique nítido:
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());
Uma composição é criada adicionando primitivas para serem reproduzidas em sequência. Cada primitiva também é escalonável, para que você possa controlar a amplitude da vibração gerada por cada uma delas. A escala é definida como um valor entre 0 e 1, em que 0 mapeia para uma amplitude mínima em que essa primitiva pode ser (quase) sentida pelo usuário.
Se você quiser criar uma versão fraca e forte da mesma primitiva, é recomendado que as escalas sejam diferentes em uma proporção de 1,4 ou mais, para que a diferença na intensidade possa ser facilmente percebida. Não tente criar mais de três níveis de intensidade da mesma primitiva, porque eles não são diferenciados. Por exemplo, use escalas de 0,5, 0,7 e 1,0 para criar uma versão de intensidade baixa, média e alta de uma primitiva.
A composição também pode especificar atrasos a serem adicionados entre primitivos consecutivos. Esse atraso é expresso em milissegundos desde o final da primitiva anterior. Em geral, um intervalo de 5 a 10 ms entre duas primitivas é muito curto para ser detectado. Considere usar um intervalo de 50 ms ou mais se você quiser criar um intervalo perceptível entre duas primitivas. Confira um exemplo de composição com atrasos:
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());
As APIs abaixo podem ser usadas para verificar o suporte do dispositivo a primitivas específicas:
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. }
Também é possível verificar várias primitivas e decidir quais delas compor com base no nível de suporte do dispositivo:
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);
Exemplo: Resist (com marcações baixas)
É possível controlar a amplitude da vibração primitiva para transmitir feedback útil a uma ação em andamento. Valores de escala próximos podem ser usados para criar um efeito de crescendo suave de uma primitiva. O atraso entre primitivas consecutivas também pode ser definido dinamicamente com base na interação do usuário. Isso é ilustrado no exemplo a seguir de uma animação de visualização controlada por um gesto de arrasto e aumentada com retorno tátil.

Figura 1. Essa forma de onda representa a aceleração de saída da vibração em um dispositivo.
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); } }
Exemplo: expandir (com aumento e queda)
Há duas primitivas para aumentar a intensidade da vibração percebida: PRIMITIVE_QUICK_RISE
e
PRIMITIVE_SLOW_RISE
.
Ambos atingem o mesmo objetivo, mas com durações diferentes. Há apenas uma
primitiva para reduzir a velocidade, a
PRIMITIVE_QUICK_FALL
.
Essas primitivas funcionam melhor juntas para criar um segmento de forma de onda que aumenta de
intensidade e depois desaparece. Você pode alinhar primitivas dimensionadas para evitar saltos
súbitos na amplitude entre elas, o que também funciona bem para estender a duração
geral do efeito. Perceptualmente, as pessoas sempre notam a parte em ascensão mais do que
a em queda. Portanto, tornar a parte em ascensão mais curta do que a em queda
pode ser usado para mudar a ênfase para a parte em queda.
Confira um exemplo de aplicação dessa composição para expandir e recolher um círculo. O efeito de elevação pode melhorar a sensação de expansão durante a animação. A combinação de efeitos de aumento e diminuição ajuda a enfatizar o recolhimento no final da animação.

Figura 2. Essa forma de onda representa a aceleração de saída da vibração em um dispositivo.
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; } }
Exemplo: oscilação (com giros)
Um dos principais princípios de retorno tátil é encantar os usuários. Uma maneira divertida
de introduzir um efeito de vibração inesperado e agradável é usar o
PRIMITIVE_SPIN
.
Essa primitiva é mais eficaz quando é chamada mais de uma vez. Vários
giros concatenados podem criar um efeito instável e oscilante, que pode ser
melhorado aplicando uma escalação um tanto aleatória em cada primitivo. Você
também pode testar a lacuna entre primitivas de giro sucessivas. Duas rotações
sem intervalo (0 ms entre elas) criam uma sensação de giro apertado. Aumentar
o intervalo entre os giros de 10 para 50 ms leva a uma sensação de giro mais solto e
pode ser usado para corresponder à duração de um vídeo ou animação.
Não recomendamos o uso de uma lacuna maior que 100 ms, porque os giros sucessivos não se integram bem e começam a parecer efeitos individuais.
Confira um exemplo de forma elástica que volta para cima depois de ser arrastada para baixo e depois liberada. A animação é aprimorada com um par de efeitos de giro, reproduzidos com intensidades variáveis proporcionais ao deslocamento de salto.

Figura 3. Essa forma de onda representa a aceleração de saída da vibração em um dispositivo.
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) } }
Exemplo: rejeição (com ruídos)
Outro uso avançado dos efeitos de vibração é simular interações
físicas. O
PRIMITIVE_THUD
pode criar um efeito forte e reverberante, que pode ser combinado com a
visualização de um impacto, em um vídeo ou animação, por exemplo, para aumentar a
experiência geral.
Confira um exemplo de animação simples de queda de bola aprimorada com um efeito de som tocado sempre que a bola quica na parte de baixo da tela:

Figura 4. Essa forma de onda representa a aceleração de saída da vibração em um dispositivo.
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; } } }); } }