Esta página aborda os exemplos de como usar diferentes APIs táteis para criar efeitos personalizados em um app Android. Como grande parte das informações desta página depende do bom conhecimento do funcionamento de um atuador de vibração, recomendamos a leitura do Primer do atuador de vibração (link em inglês).
Esta página inclui os exemplos a seguir.
- Padrões de vibração personalizados
- Padrão de aumento: um padrão que começa suavemente.
- Padrão de repetição: um padrão sem fim.
- Padrão com substituto: uma demonstração de substituição.
- Composições de vibração
- Resistente: um efeito de arrastar com intensidade dinâmica.
- Expandir: efeito de aumento e queda.
- Wobble: um efeito de instabilidade que usa o primitivo
SPIN
. - Rejeição: um efeito de salto que usa o primitivo
THUD
.
Para ver mais exemplos, consulte Adicionar retorno tátil a eventos e sempre siga os princípios de design 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 for capaz de reproduzir o efeito
A referência da API Android haptics fornece detalhes sobre como verificar o suporte a componentes envolvidos no retorno tátil para que seu app possa oferecer uma experiência geral consistente.
Dependendo do caso de uso, convém desativar os efeitos personalizados ou fornecer efeitos personalizados alternativos com base nos diferentes recursos em potencial.
Planeje as seguintes classes de alto nível de recursos do dispositivo:
Se você estiver usando primitivos táteis: dispositivos compatíveis com os primitivos necessários para os efeitos personalizados. Consulte a próxima seção para ver detalhes sobre primitivos.
Dispositivos com controle de amplitude.
Dispositivos com suporte a vibração básico (ligado/desligado), ou seja, aqueles sem controle de amplitude.
Se a escolha de efeito tátil do app considerar essas categorias, a experiência tátil do usuário vai ser previsível para qualquer dispositivo individual.
Uso de primitivos táteis
O Android inclui vários primitivos táteis que variam em amplitude e frequência. É possível usar um ou vários primitivos combinados para conseguir efeitos táteis avançados.
- Use atrasos de 50 ms ou mais para intervalos visíveis entre dois primitivos, considerando também a duração do primitivo, se possível.
- Use escalas com uma proporção de 1,4 ou mais para que a diferença de intensidade seja melhor percebida.
Use escalas de 0,5, 0,7 e 1,0 para criar uma versão de baixa, média e alta intensidade de um primitivo.
Crie padrões de vibração personalizados
Padrões de vibração são frequentemente usados no retorno tátil de atenção, como notificações
e toques. O serviço Vibrator
pode reproduzir padrões de vibração longos que
mudam a amplitude de vibração ao longo do tempo. Esses efeitos são chamados de formas de onda.
Efeitos de forma de onda podem ser facilmente perceptíveis, mas vibrações longas e repentinas podem assustar o usuário em um ambiente silencioso. Aumentar a uma amplitude desejada muito rápido também pode produzir ruídos de zumbido audíveis. A recomendação para projetar padrões de forma de onda é suavizar as transições de amplitude e criar efeitos de ampliação.
Amostra: padrão de ampliação
As formas de onda são representadas como VibrationEffect
com três parâmetros:
- Timings:uma matriz de durações, em milissegundos, para cada segmento de forma de onda.
- Amplitude:a amplitude de vibração desejada para cada duração especificada no primeiro argumento, representada por um valor inteiro de 0 a 255, em que 0 representa a amplitude de vibração "desativada" e 255 é a amplitude máxima do dispositivo.
- Repetição de índice:o índice na matriz especificada no primeiro argumento para começar a repetir a forma de onda ou -1 se ela reproduzir o padrão apenas uma vez.
Veja um exemplo de forma de onda que pisca duas vezes com uma pausa de 350 ms entre os pulsamentos. O primeiro pulso é uma ampliação suave até a amplitude máxima, e o segundo é uma ampliação 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));
Amostra: padrão de repetição
As formas de onda também podem ser reproduzidas repetidamente até serem canceladas. Para criar uma forma de onda repetida, defina um parâmetro "repeat" não negativo. Quando você reproduz uma forma de onda repetida, a vibração continua até que seja explicitamente cancelada 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 ação do usuário para confirmá-lo. Exemplos desses eventos incluem chamadas telefônicas recebidas e alarmes acionados.
Amostra: padrão com substituto
Controlar a amplitude de uma vibração é um recurso que depende do hardware. A reprodução de uma forma de onda em um dispositivo simples sem esse recurso faz com que ela vibre na amplitude máxima para cada entrada positiva na matriz de amplitude. Caso seu app precise acomodar esses dispositivos, a recomendação é garantir que o padrão não gere um efeito de vibração quando reproduzido nessa condição ou projetar um padrão ATIVADO/DESATIVADO mais simples que possa ser reproduzido como um 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 e vai além disso para explorar recursos táteis avançados usando recursos de hardware mais avançados. É possível 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 tenham uma largura de banda de frequência mais ampla.
O processo para criar padrões de vibração personalizados, descrito anteriormente nesta página, explica como controlar a amplitude de vibração para criar efeitos suaves de variação de movimentos para cima e para baixo. O retorno tátil avançado melhora esse conceito explorando a ampla faixa 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 efeito de diminuendo.
Os primitivos de composição, descritos anteriormente nesta página, são implementados pelo fabricante do dispositivo. Eles fornecem uma vibração nítida, curta e agradável que se alinha aos princípios táteis para proporcionar um 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 fornece substitutos para composições com primitivos sem suporte. Recomendamos que você siga estas etapas:
Antes de ativar o retorno tátil avançado, verifique se um determinado dispositivo oferece suporte a todos os primitivos que você está usando.
Desative o conjunto consistente de experiências sem suporte, não apenas os efeitos sem um primitivo. Confira a seguir mais informações sobre como conferir o suporte do dispositivo.
Você pode criar efeitos de vibração compostos com o VibrationEffect.Composition
.
Veja um exemplo de um efeito que cresce lentamente seguido de um efeito de clique forte:
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 reproduzidos em sequência. Cada primário também é escalonável, para que você possa controlar a amplitude da vibração gerada por cada um deles. A escala é definida como um valor entre 0 e 1, em que 0 realmente corresponde a uma amplitude mínima em que esse primitivo pode ser (quase) sentido pelo usuário.
Se você quiser criar uma versão fraca e forte do mesmo primitivo, é recomendável que as escalas sejam diferentes em uma proporção de 1,4 ou mais, para que a diferença de intensidade possa ser facilmente percebida. 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 versão de baixa, média e alta intensidade de um primitivo.
A composição também pode especificar atrasos a serem adicionados entre primitivos consecutivos. Esse atraso é expresso em milissegundos desde o fim do primitivo anterior. Em geral, um intervalo de 5 a 10 ms entre dois primitivos é muito curto para ser detectado. Considere usar um intervalo de 50 ms ou mais se quiser criar um intervalo compreensível entre dois primitivos. Confira 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 APIs abaixo podem ser usadas para verificar a compatibilidade do dispositivo com primitivos específicos:
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 decidir quais 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);
Amostra: resistir (com marcações baixas)
Você pode controlar a amplitude da vibração primitiva para transmitir feedback útil para uma ação em andamento. Valores de escala espaçados podem ser usados para criar um efeito de crescendo suave de um primitivo. O atraso entre primitivos consecutivos 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 arrastar e aumentada com retorno tátil.
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: expansão (com ascensão e queda)
Há dois primitivos para aumentar a intensidade de vibração percebida: PRIMITIVE_QUICK_RISE
e
PRIMITIVE_SLOW_RISE
.
Ambas atingem a mesma meta, mas com durações diferentes. Há apenas um
primário para redução, o
PRIMITIVE_QUICK_FALL
.
Esses primitivos trabalham melhor juntos para criar um segmento de forma de onda que aumenta em
intensidade e depois desaparece. É possível alinhar os primitivos dimensionados para evitar saltos
repentinos de amplitude entre eles, o que também funciona bem para ampliar a duração
geral do efeito. Perceptivamente, as pessoas sempre percebem a parte crescente do que a parte em queda. Portanto, tornar a parte crescente mais curta do que a queda pode
ser usado para mudar a ênfase para a parte que cai.
Este é um exemplo de uma aplicação dessa composição para expandir e recolher um círculo. O efeito de ascensão pode melhorar a sensação de expansão durante a animação. A combinação de efeitos de ascensão e queda ajuda a enfatizar o recolhimento no final da animação.
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: oscilação (com giros)
Um dos principais princípios táteis é encantar os usuários. Uma maneira divertida
de introduzir um efeito de vibração agradável e inesperado é usar a
PRIMITIVE_SPIN
.
Este primitivo é mais eficaz quando é chamado mais de uma vez. Vários
giros concatenados podem criar um efeito instável e instável, que pode ser
aprimorado ainda mais com a aplicação de um dimensionamento um pouco aleatório em cada primitivo. Você também pode testar a lacuna entre primitivos de rotação sucessivos. Duas voltas
sem qualquer lacuna (0 ms entre elas) criam uma sensação incrível. O aumento
do intervalo entre rotação de 10 para 50 ms causa uma sensação mais lenta e
pode ser usado para corresponder a duração de um vídeo ou animação.
Não recomendamos o uso de um intervalo maior que 100 ms, porque os movimentos sucessivos não se integram bem e começam a parecer efeitos individuais.
Veja um exemplo de uma forma elástica que se recupera depois de ser arrastada para baixo e, em seguida, soltá-la. A animação é aprimorada com um par de efeitos de Rotação, reproduzidos com intensidades variadas que são proporcionais ao deslocamento do quilômetro.
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: rejeições (com batidas)
Outra aplicação avançada dos efeitos de vibração é a simulação de interações
físicas. O
PRIMITIVE_THUD
pode criar um efeito forte e de reverberação, que pode ser combinado com a
visualização de um impacto, em um vídeo ou animação, por exemplo, para melhorar a
experiência geral.
Este é um exemplo de uma animação simples de queda de bola aprimorada com um efeito de balanço reproduzido sempre que a bola salta da parte de baixo da tela:
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; } } }); } }