Esta página traz exemplos de como usar diferentes APIs de retorno tátil para criar efeitos personalizados em um app Android. A maior parte das informações sobre esta página depende de um bom conhecimento do funcionamento de um atuador de vibração, recomendamos a leitura do Guia do atuador de vibração.
Esta página inclui os exemplos a seguir.
- Padrões de vibração personalizados
- Padrão de aceleração: um padrão que começa suavemente.
- Padrão de repetição: um padrão sem fim.
- Padrão com substituto: um substituto demonstração.
- Composições de vibração
Para mais exemplos, consulte Adicionar retorno tátil a eventos e sempre sigam os princípios de design de retorno tátil.
Usar substitutos para lidar com a compatibilidade de dispositivos
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 tátil do Android fornece detalhes sobre como verificar suporte a componentes envolvidos no retorno tátil, para que seu aplicativo possa fornecer uma uma experiência geral consistente.
Dependendo do caso de uso, convém desativar os efeitos personalizados ou fornecem efeitos personalizados alternativos com base em diferentes recursos potenciais.
Planeje as seguintes classes de alto nível de capacidade do dispositivo:
Se você usa primitivos táteis: os dispositivos compatíveis com esses primitivos necessárias para os efeitos personalizados. Consulte a próxima seção para mais detalhes primitivas.
Dispositivos com controle de amplitude.
Dispositivos com suporte a vibração básico (ativado ou desativado), ou seja, sem controle de amplitude.
Se a opção de efeito tátil do seu app considerar essas categorias, seu a experiência tátil do usuário deve permanecer previsível para qualquer dispositivo individual.
Uso de primitivos táteis
O Android inclui várias primitivas de retorno tátil que variam em amplitude e frequência. É possível usar um primitivo sozinho ou vários primitivos em combinação para obter efeitos táteis avançados.
- Use atrasos de 50 ms ou mais para lacunas perceptíveis entre dois primitivos, considerando também a classe primitiva duração se possível.
- Use escalas que diferem em uma proporção de 1,4 ou mais para que a diferença em e intensidade é melhor percebida.
Use escalas de 0,5, 0,7 e 1,0 para criar uma escala baixa, média e alta. de intensidade de um primitivo.
Criar padrões de vibração personalizados
Os padrões de vibração são
frequentemente usados no retorno tátil de atenção, como notificações
e toques. O serviço do Vibrator
pode reproduzir padrões longos de vibração que
mudar a amplitude de vibração ao longo do tempo. Esses efeitos são chamados de formas de onda.
Os efeitos da forma de onda são perceptíveis com facilidade, mas vibrações longas e repentinas podem surpreender o usuário se ele for tocado em um ambiente silencioso. Aumento para uma amplitude desejada muito rápido também pode produzir zumbidos audíveis. A recomendação para projetar padrões de forma de onda é suavizar as transições de amplitude para criar os efeitos de aumento e redução.
Amostra: padrão de ampliaçã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 forma de onda. um segmento de público-alvo.
- Amplitudes:a amplitude de vibração desejada para cada duração especificada. no primeiro argumento, representado por um valor inteiro de 0 a 255, com 0 que representa a vibração "desativada" e 255 é o limite máximo amplitude.
- Repetição de índice: o índice na matriz especificada no primeiro argumento para comece a repetir a forma de onda, ou -1 se deve tocar o padrão apenas uma vez.
Aqui está um exemplo de forma de onda que pulsa duas vezes com uma pausa de 350 ms entre pulsos O primeiro pulso é uma rampa suave até a amplitude máxima, e a o segundo é uma rampa rápida para manter a amplitude máxima. É definida uma parada no final pelo valor do índice de repetição negativa.
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));
Amostra: padrão repetido
As formas de onda também podem ser reproduzidas repetidamente até que sejam canceladas. A maneira de criar a repetição da forma de onda é definir um parâmetro "repeat" não negativo. Quando você joga que se repete, a vibração continua até ser explicitamente cancelada 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 ação do usuário para reconhecê-lo. Exemplos de tais eventos incluem chamadas telefônicas recebidas e alarmes acionados.
Amostra: padrão com substituto
Controlar a amplitude de uma vibração é dependente de hardware. Reproduzir uma forma de onda em um um dispositivo mais simples sem esse recurso faz com que ele vibre no máximo amplitude para cada entrada positiva na matriz de amplitude. Caso seu app precise acomodar esses dispositivos, a recomendação é garantir não gera um efeito de zumbido quando tocado nesta condição, ou para crie 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 compô-los em efeitos personalizados mais longos e complexos, e vai além para explorar ricos o retorno tátil usando recursos de hardware mais avançados. Você pode usar combinações de efeitos que variam amplitude e frequência para criar efeitos táteis mais complexos em dispositivos com atuadores táteis que têm uma largura de banda de frequência mais ampla.
O processo para criar vibrações personalizadas padrões, descritos anteriormente nesta página, explica como controlar a amplitude de vibração para criar efeitos suaves de aumentando e diminuindo. O retorno tátil avançado melhora esse conceito explorando as maior intervalo de frequência da vibração do dispositivo para tornar o efeito ainda mais suave. Essas formas de onda são especialmente eficazes para criar um crescendo ou diminuendo efeito
Os primitivos de composição, descritos anteriormente nesta página, são implementados por do fabricante do dispositivo. Eles fornecem uma vibração nítida, curta e agradável alinhada aos princípios táteis para um retorno tátil claro. Para mais detalhes sobre esses recursos e como eles funcionam, consulte Atuadores de vibração introdução.
O Android não oferece substitutos para composições com suporte primitivas. Recomendamos que você siga estas etapas:
Antes de ativar o retorno tátil avançado, confira se determinado dispositivo é compatível todos os primitivos que você está usando.
Desative o conjunto consistente de experiências incompatíveis, não apenas os efeitos sem uma primitiva. Mais informações sobre como verificar a o suporte do dispositivo é mostrado a seguir.
Você pode criar efeitos compostos de vibração com o VibrationEffect.Composition
.
Veja um exemplo de um efeito de ascensão lenta 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 primitivos para serem tocados em sequência. Cada também é escalonável, o que permite controlar a amplitude da vibração geradas por cada um deles. A escala é definida como um valor entre 0 e 1, em que 0 mapeia para uma amplitude mínima na qual esse primitivo pode ser senti (quase) pelo usuário.
Se você quiser criar uma versão fraca e forte do mesmo primitivo, é recomendado que as escalas diferem em uma proporção de 1,4 ou mais, de modo que a diferença e intensidade pode ser facilmente percebido. Não tente criar mais de três níveis de intensidade do mesmo primitivo, porque eles não são perceptivamente distintos. Por exemplo, use escalas de 0,5, 0,7 e 1,0 para criar uma escala baixa, média e de alta intensidade de um primitivo.
A composição também pode especificar atrasos a serem adicionados entre primitivas. Esse atraso é expresso em milissegundos desde o fim do primitivo anterior. Em geral, uma lacuna de 5 a 10 ms entre dois primitivos é muito curtas para serem detectáveis. Use um intervalo de cerca de 50 ms ou mais. se você quiser criar uma lacuna perceptível entre dois primitivos. Veja um exemplo de uma 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 seguintes APIs podem ser usadas para verificar o suporte do dispositivo para certos primitivos:
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ários primitivos e depois decidir quais escrever 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);
Amostra: Resistência (com marcas baixas)
É possível controlar a amplitude da vibração primitiva para transmitir feedback útil para uma ação em andamento. Valores de escala com espaçamento próximo podem ser usada para criar um efeito de crescendo suave de um primitivo. O atraso entre primitivos consecutivos também podem ser definidos dinamicamente com base no usuário interação. Isso é ilustrado no exemplo a seguir de uma animação de visualização. controlado por um gesto de arrastar e aumentado com retorno tátil.
![Animação de um círculo sendo arrastado para baixo](https://developer.android.google.cn/static/develop/ui/views/haptics/images/resist-demo.gif?authuser=19&hl=pt)
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); } }
Amostra: expandir (com aumento e queda)
Há dois primitivos para aumentar a intensidade de vibração percebida: o
PRIMITIVE_QUICK_RISE
e
PRIMITIVE_SLOW_RISE
.
Ambos atingem a mesma meta, mas com durações diferentes. Há apenas um
primitivos para reduzir, o
PRIMITIVE_QUICK_FALL
Esses primitivos funcionam melhor juntos para criar um segmento em forma de onda que cresce em
e, em seguida, desaparece. É possível alinhar primitivos dimensionados para evitar
saltos de amplitude entre eles, o que também funciona bem para estender
duração do efeito. As pessoas sempre percebem a parte crescente mais do que
a porção decrescente, portanto, tornar a parte crescente mais curta do que a que está pode
ser usada para mudar a ênfase para a parte decrescente.
Aqui está um exemplo de aplicação dessa composição para expandir e recolher um círculo. O efeito de ascensão pode aumentar a sensação de expansão durante a animação. A combinação dos efeitos de ascensão e queda ajuda a enfatizar recolhendo no final da animação.
![Animação de um círculo expansível](https://developer.android.google.cn/static/develop/ui/views/haptics/images/expand-demo.gif?authuser=19&hl=pt)
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; } }
Amostra: Wobble (com giros)
Um dos principais princípios de retorno tátil é agradar os usuários. Um jeito divertido
introduzir um efeito de vibração inesperado agradável é usar o
PRIMITIVE_SPIN
Esse primitivo é mais eficaz quando é chamado mais de uma vez. Vários status
giros concatenados pode criar um efeito instável e instável,
ainda mais aprimorado com a aplicação de um escalonamento um pouco aleatório em cada primitivo. Você
também pode testar a lacuna entre primitivos de rotação sucessivos. Duas giros
sem qualquer lacuna (0 ms entre eles) cria uma sensação de tensão. Aumentando
o intervalo entre giros de 10 a 50 ms leva a uma sensação de spinning mais solto, e
pode ser usada para corresponder à duração de um vídeo ou animação.
Não recomendamos o uso de um intervalo maior que 100 ms, porque as sequências Os giros não se integram mais e começam a parecer efeitos individuais.
Aqui está um exemplo de uma forma elástica que quica depois de ser arrastada para baixo e liberados. A animação é aprimorada com um par de efeitos de rotação, reproduzidos com intensidades variáveis e proporcionais ao deslocamento do salto.
![Animação de uma forma elástica saltando](https://developer.android.google.cn/static/develop/ui/views/haptics/images/wobble-demo.gif?authuser=19&hl=pt)
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) } }
Amostra: saltar (com batidas)
Outra aplicação avançada de efeitos de vibração é a simulação
e interações. A
PRIMITIVE_THUD
pode criar um efeito forte e retumbante, que pode ser combinado com o
visualização de um impacto, em um vídeo ou uma animação, por exemplo, para aumentar
experiência geral.
Aqui está um exemplo de uma animação de queda de bola simples aprimorada com um efeito de batida jogado cada vez que a bola quica na parte inferior da tela:
![Animação de uma bola quicando na parte de baixo da tela](https://developer.android.google.cn/static/develop/ui/views/haptics/images/bounce-demo.gif?authuser=19&hl=pt)
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; } } }); } }