Criar efeitos táteis personalizados

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.

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:

  1. Timings:uma matriz de durações, em milissegundos, para cada segmento de forma de onda.
  2. 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.
  3. 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:

  1. Antes de ativar o retorno tátil avançado, verifique se um determinado dispositivo oferece suporte a todos os primitivos que você está usando.

  2. 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.

Animação de um círculo sendo arrastado para baixo
Representação da forma de onda de vibração de entrada

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.

Animação de um círculo em expansão
Representação da forma de onda de vibração de entrada

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.

Animação de uma forma elástica saltando
Representação da forma de onda de vibração de entrada

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:

Animação de uma bola caindo saltando da parte de baixo da tela
Representação da forma de onda de vibração de entrada

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