Criar efeitos táteis personalizados

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.

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:

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

  1. Antes de ativar o retorno tátil avançado, confira se determinado dispositivo é compatível todos os primitivos que você está usando.

  2. 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
Gráfico 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);
  }
}

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
Gráfico 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: 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
Gráfico 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: 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
Gráfico 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;
          }
        }
      });
  }
}