Criar efeitos táteis personalizados

Esta página aborda exemplos de como usar diferentes APIs de retorno tátil para criar efeitos personalizados em um aplicativo Android. Como a maioria das informações desta página depende de um bom conhecimento do funcionamento de um atuador de vibração, recomendamos a leitura do manual do atuador de vibração.

Esta página inclui os seguintes exemplos.

Para mais exemplos, consulte Adicionar retorno tátil a eventos e siga sempre os princípios de design tátil.

Usar substitutos para lidar com a compatibilidade do dispositivo

Ao implementar qualquer efeito personalizado, considere o seguinte:

  • Quais recursos do dispositivo são necessários para o efeito
  • O que fazer quando o dispositivo não consegue reproduzir o efeito

A referência da API de retorno tátil do Android fornece detalhes sobre como verificar o suporte a componentes envolvidos em retorno tátil para que o app possa oferecer uma experiência geral consistente.

Dependendo do caso de uso, talvez seja necessário desativar os efeitos personalizados ou oferecer efeitos personalizados alternativos com base em diferentes recursos.

Planeje as seguintes classes de alto nível de recursos do dispositivo:

  • Se você estiver usando primitivas hápticas: dispositivos que oferecem suporte a essas primitivas necessárias para os efeitos personalizados. Consulte a próxima seção para saber mais sobre primitivas.

  • Dispositivos com controle de amplitude.

  • Dispositivos com suporte básico a vibração (ativado/desativado), ou seja, aqueles que não têm controle de amplitude.

Se a escolha de efeitos hápticos do seu app for responsável por essas categorias, a experiência do usuário háptico vai permanecer previsível para qualquer dispositivo.

Uso de primitivas táteis

O Android inclui várias primitivas de retorno tátil que variam em amplitude e frequência. É possível usar uma primitiva sozinha ou várias em combinação para conseguir efeitos hápticos avançados.

  • Use atrasos de 50 ms ou mais para intervalos perceptíveis entre duas primitivas, considerando também a duração primitiva se possível.
  • Use escalas que diferem em uma proporção de 1,4 ou mais para que a diferença na intensidade seja melhor percebida.
  • Use escalas de 0,5, 0,7 e 1,0 para criar uma versão de intensidade baixa, média e alta de uma primitiva.

Criar padrões de vibração personalizados

Os padrões de vibração são usados com frequência em hápticas de atenção, como notificações e toques de chamada. O serviço Vibrator pode reproduzir padrões de vibração longos que mudam a amplitude da vibração ao longo do tempo. Esses efeitos são chamados de formas de onda.

Os efeitos de forma de onda podem ser facilmente percebidos, mas vibrações longas e repentinas podem assustar o usuário se tocadas em um ambiente silencioso. Aumentar a amplitude desejada muito rápido também pode produzir ruídos audíveis. A recomendação para projetar padrões de forma de onda é suavizar as transições de amplitude para criar efeitos de aumento e diminuição.

Exemplo: padrão de aceleração

As formas de onda são representadas como VibrationEffect com três parâmetros:

  1. Tempos:uma matriz de durações, em milissegundos, para cada segmento de forma de onda.
  2. Amplitudes:a amplitude de vibração desejada para cada duração especificada no primeiro argumento, representada por um valor inteiro de 0 a 255, sendo que 0 representa o vibrador "desativado" e 255 é a amplitude máxima do dispositivo.
  3. Índice de repetição:o índice na matriz especificado no primeiro argumento para iniciar a repetição da forma de onda ou -1 se o padrão for reproduzido apenas uma vez.

Confira um exemplo de forma de onda que pulsa duas vezes com uma pausa de 350 ms entre os pulsos. O primeiro pulso é uma rampa suave até a amplitude máxima, e o segundo é uma rampa rápida para manter a amplitude máxima. A parada no final é definida pelo valor negativo do índice de repetição.

Kotlin

val timings: LongArray = longArrayOf(50, 50, 50, 50, 50, 100, 350, 25, 25, 25, 25, 200)
val amplitudes: IntArray = intArrayOf(33, 51, 75, 113, 170, 255, 0, 38, 62, 100, 160, 255)
val repeatIndex = -1 // Do not repeat.

vibrator.vibrate(VibrationEffect.createWaveform(timings, amplitudes, repeatIndex))

Java

long[] timings = new long[] { 50, 50, 50, 50, 50, 100, 350, 25, 25, 25, 25, 200 };
int[] amplitudes = new int[] { 33, 51, 75, 113, 170, 255, 0, 38, 62, 100, 160, 255 };
int repeatIndex = -1; // Do not repeat.

vibrator.vibrate(VibrationEffect.createWaveform(timings, amplitudes, repeatIndex));

Exemplo: padrão repetitivo

As formas de onda também podem ser reproduzidas repetidamente até serem canceladas. Para criar uma forma de onda repetida, defina um parâmetro "repetição" não negativo. Quando você reproduz uma forma de onda repetida, a vibração continua até ser cancelada explicitamente no serviço:

Kotlin

void startVibrating() {
  val timings: LongArray = longArrayOf(50, 50, 100, 50, 50)
  val amplitudes: IntArray = intArrayOf(64, 128, 255, 128, 64)
  val repeat = 1 // Repeat from the second entry, index = 1.
  VibrationEffect repeatingEffect = VibrationEffect.createWaveform(timings, amplitudes, repeat)
  // repeatingEffect can be used in multiple places.

  vibrator.vibrate(repeatingEffect)
}

void stopVibrating() {
  vibrator.cancel()
}

Java

void startVibrating() {
  long[] timings = new long[] { 50, 50, 100, 50, 50 };
  int[] amplitudes = new int[] { 64, 128, 255, 128, 64 };
  int repeat = 1; // Repeat from the second entry, index = 1.
  VibrationEffect repeatingEffect = VibrationEffect.createWaveform(timings, amplitudes, repeat);
  // repeatingEffect can be used in multiple places.

  vibrator.vibrate(repeatingEffect);
}

void stopVibrating() {
  vibrator.cancel();
}

Isso é muito útil para eventos intermitentes que exigem uma ação do usuário para ser reconhecido. Exemplos desses eventos incluem ligações recebidas e alarmes acionados.

Exemplo: padrão com substituto

Controlar a amplitude de uma vibração é um recurso dependente de hardware. A reprodução de uma forma de onda em um dispositivo de baixo custo sem esse recurso faz com que ele vibre na amplitude máxima para cada entrada positiva na matriz de amplitude. Se o app precisar acomodar esses dispositivos, recomendamos que o padrão não gere um efeito de zumbido quando reproduzido nessa condição ou projete um padrão LIGADO/DESLIGADO mais simples que possa ser reproduzido como substituto.

Kotlin

if (vibrator.hasAmplitudeControl()) {
  vibrator.vibrate(VibrationEffect.createWaveform(smoothTimings, amplitudes, smoothRepeatIdx))
} else {
  vibrator.vibrate(VibrationEffect.createWaveform(onOffTimings, onOffRepeatIdx))
}

Java

if (vibrator.hasAmplitudeControl()) {
  vibrator.vibrate(VibrationEffect.createWaveform(smoothTimings, amplitudes, smoothRepeatIdx));
} else {
  vibrator.vibrate(VibrationEffect.createWaveform(onOffTimings, onOffRepeatIdx));
}

Criar composições de vibração

Esta seção apresenta maneiras de combiná-los em efeitos personalizados mais longos e complexos, além de explorar haptics avançados usando recursos de hardware mais avançados. É possível usar combinações de efeitos que variam a amplitude e a frequência para criar efeitos táteis mais complexos em dispositivos com atuadores que têm uma largura de banda de frequência mais ampla.

O processo de criação de padrões de vibração personalizados, descrito anteriormente nesta página, explica como controlar a amplitude de vibração para criar efeitos suaves de aumento e diminuição. A haptics avançada melhora esse conceito explorando a gama de frequência mais ampla do vibrador do dispositivo para tornar o efeito ainda mais suave. Essas formas de onda são especialmente eficazes para criar um efeito de crescendo ou diminuir.

As primitivas de composição, descritas anteriormente nesta página, são implementadas pelo fabricante do dispositivo. Eles oferecem uma vibração nítida, curta e agradável que se alinha aos princípios de retorno tátil para retorno tátil claro. Para mais detalhes sobre esses recursos e como eles funcionam, consulte Introdução aos atuadores de vibração.

O Android não oferece substitutos para composições com primitivas sem suporte. Recomendamos que você siga estas etapas:

  1. Antes de ativar a haptics avançada, verifique se um determinado dispositivo oferece suporte a todas as primitivas que você está usando.

  2. Desative o conjunto consistente de experiências sem suporte, não apenas os efeitos que estão sem uma primitiva. Confira abaixo mais informações sobre como verificar o suporte do dispositivo.

É possível criar efeitos de vibração compostos com VibrationEffect.Composition. Confira um exemplo de efeito de aumento lento seguido de um efeito de clique nítido:

Kotlin

vibrator.vibrate(
    VibrationEffect.startComposition().addPrimitive(
      VibrationEffect.Composition.PRIMITIVE_SLOW_RISE
    ).addPrimitive(
      VibrationEffect.Composition.PRIMITIVE_CLICK
    ).compose()
  )

Java

vibrator.vibrate(
    VibrationEffect.startComposition()
        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_SLOW_RISE)
        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
        .compose());

Uma composição é criada adicionando primitivas para serem reproduzidas em sequência. Cada primitiva também é escalonável, para que você possa controlar a amplitude da vibração gerada por cada uma delas. A escala é definida como um valor entre 0 e 1, em que 0 mapeia para uma amplitude mínima em que essa primitiva pode ser (quase) sentida pelo usuário.

Se você quiser criar uma versão fraca e forte da mesma primitiva, é recomendado que as escalas sejam diferentes em uma proporção de 1,4 ou mais, para que a diferença na intensidade possa ser facilmente percebida. Não tente criar mais de três níveis de intensidade da mesma primitiva, porque eles não são diferenciados. Por exemplo, use escalas de 0,5, 0,7 e 1,0 para criar uma versão de intensidade baixa, média e alta de uma primitiva.

A composição também pode especificar atrasos a serem adicionados entre primitivos consecutivos. Esse atraso é expresso em milissegundos desde o final da primitiva anterior. Em geral, um intervalo de 5 a 10 ms entre duas primitivas é muito curto para ser detectado. Considere usar um intervalo de 50 ms ou mais se você quiser criar um intervalo perceptível entre duas primitivas. Confira um exemplo de composição com atrasos:

Kotlin

val delayMs = 100
vibrator.vibrate(
    VibrationEffect.startComposition().addPrimitive(
      VibrationEffect.Composition.PRIMITIVE_SPIN, 0.8f
    ).addPrimitive(
      VibrationEffect.Composition.PRIMITIVE_SPIN, 0.6f
    ).addPrimitive(
      VibrationEffect.Composition.PRIMITIVE_THUD, 1.0f, delayMs
    ).compose()
  )

Java

int delayMs = 100;
vibrator.vibrate(
    VibrationEffect.startComposition()
        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_SPIN, 0.8f)
        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_SPIN, 0.6f)
        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD, 1.0f, delayMs)
        .compose());

As APIs abaixo podem ser usadas para verificar o suporte do dispositivo a primitivas específicas:

Kotlin

val primitive = VibrationEffect.Composition.PRIMITIVE_LOW_TICK

if (vibrator.areAllPrimitivesSupported(primitive)) {
  vibrator.vibrate(VibrationEffect.startComposition().addPrimitive(primitive).compose())
} else {
  // Play a predefined effect or custom pattern as a fallback.
}

Java

int primitive = VibrationEffect.Composition.PRIMITIVE_LOW_TICK;

if (vibrator.areAllPrimitivesSupported(primitive)) {
  vibrator.vibrate(VibrationEffect.startComposition().addPrimitive(primitive).compose());
} else {
  // Play a predefined effect or custom pattern as a fallback.
}

Também é possível verificar várias primitivas e decidir quais delas compor com base no nível de suporte do dispositivo:

Kotlin

val effects: IntArray = intArrayOf(
  VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
  VibrationEffect.Composition.PRIMITIVE_TICK,
  VibrationEffect.Composition.PRIMITIVE_CLICK
)
val supported: BooleanArray = vibrator.arePrimitivesSupported(primitives);

Java

int[] primitives = new int[] {
  VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
  VibrationEffect.Composition.PRIMITIVE_TICK,
  VibrationEffect.Composition.PRIMITIVE_CLICK
};
boolean[] supported = vibrator.arePrimitivesSupported(effects);

Exemplo: Resist (com marcações baixas)

É possível controlar a amplitude da vibração primitiva para transmitir feedback útil a uma ação em andamento. Valores de escala próximos podem ser usados para criar um efeito de crescendo suave de uma primitiva. O atraso entre primitivas consecutivas também pode ser definido dinamicamente com base na interação do usuário. Isso é ilustrado no exemplo a seguir de uma animação de visualização controlada por um gesto de arrasto e aumentada com retorno tátil.

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

Figura 1. Essa forma de onda representa a aceleração de saída da vibração em um dispositivo.

Kotlin

@Composable
fun ResistScreen() {
  // Control variables for the dragging of the indicator.
  var isDragging by remember { mutableStateOf(false) }
  var dragOffset by remember { mutableStateOf(0f) }

  // Only vibrates while the user is dragging
  if (isDragging) {
    LaunchedEffect(Unit) {
      // Continuously run the effect for vibration to occur even when the view
      // is not being drawn, when user stops dragging midway through gesture.
      while (true) {
        // Calculate the interval inversely proportional to the drag offset.
        val vibrationInterval = calculateVibrationInterval(dragOffset)
        // Calculate the scale directly proportional to the drag offset.
        val vibrationScale = calculateVibrationScale(dragOffset)

        delay(vibrationInterval)
        vibrator.vibrate(
          VibrationEffect.startComposition().addPrimitive(
            VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
            vibrationScale
          ).compose()
        )
      }
    }
  }

  Screen() {
    Column(
      Modifier
        .draggable(
          orientation = Orientation.Vertical,
          onDragStarted = {
            isDragging = true
          },
          onDragStopped = {
            isDragging = false
          },
          state = rememberDraggableState { delta ->
            dragOffset += delta
          }
        )
    ) {
      // Build the indicator UI based on how much the user has dragged it.
      ResistIndicator(dragOffset)
    }
  }
}

Java

class DragListener implements View.OnTouchListener {
  // Control variables for the dragging of the indicator.
  private int startY;
  private int vibrationInterval;
  private float vibrationScale;

  @Override
  public boolean onTouch(View view, MotionEvent event) {
    switch (event.getAction()) {
      case MotionEvent.ACTION_DOWN:
        startY = event.getRawY();
        vibrationInterval = calculateVibrationInterval(0);
        vibrationScale = calculateVibrationScale(0);
        startVibration();
        break;
      case MotionEvent.ACTION_MOVE:
        float dragOffset = event.getRawY() - startY;
        // Calculate the interval inversely proportional to the drag offset.
        vibrationInterval = calculateVibrationInterval(dragOffset);
        // Calculate the scale directly proportional to the drag offset.
        vibrationScale = calculateVibrationScale(dragOffset);
        // Build the indicator UI based on how much the user has dragged it.
        updateIndicator(dragOffset);
        break;
      case MotionEvent.ACTION_CANCEL:
      case MotionEvent.ACTION_UP:
        // Only vibrates while the user is dragging
        cancelVibration();
        break;
    }
    return true;
  }

  private void startVibration() {
    vibrator.vibrate(
          VibrationEffect.startComposition()
            .addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK, vibrationScale)
            .compose());

    // Continuously run the effect for vibration to occur even when the view
    // is not being drawn, when user stops dragging midway through gesture.
    handler.postDelayed(this::startVibration, vibrationInterval);
  }

  private void cancelVibration() {
    handler.removeCallbacksAndMessages(null);
  }
}

Exemplo: expandir (com aumento e queda)

Há duas primitivas para aumentar a intensidade da vibração percebida: PRIMITIVE_QUICK_RISE e PRIMITIVE_SLOW_RISE. Ambos atingem o mesmo objetivo, mas com durações diferentes. Há apenas uma primitiva para reduzir a velocidade, a PRIMITIVE_QUICK_FALL. Essas primitivas funcionam melhor juntas para criar um segmento de forma de onda que aumenta de intensidade e depois desaparece. Você pode alinhar primitivas dimensionadas para evitar saltos súbitos na amplitude entre elas, o que também funciona bem para estender a duração geral do efeito. Perceptualmente, as pessoas sempre notam a parte em ascensão mais do que a em queda. Portanto, tornar a parte em ascensão mais curta do que a em queda pode ser usado para mudar a ênfase para a parte em queda.

Confira um exemplo de aplicação dessa composição para expandir e recolher um círculo. O efeito de elevação pode melhorar a sensação de expansão durante a animação. A combinação de efeitos de aumento e diminuição ajuda a enfatizar o recolhimento no final da animação.

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

Figura 2. Essa forma de onda representa a aceleração de saída da vibração em um dispositivo.

Kotlin

enum class ExpandShapeState {
    Collapsed,
    Expanded
}

@Composable
fun ExpandScreen() {
  // Control variable for the state of the indicator.
  var currentState by remember { mutableStateOf(ExpandShapeState.Collapsed) }

  // Animation between expanded and collapsed states.
  val transitionData = updateTransitionData(currentState)

  Screen() {
    Column(
      Modifier
        .clickable(
          {
            if (currentState == ExpandShapeState.Collapsed) {
              currentState = ExpandShapeState.Expanded
              vibrator.vibrate(
                VibrationEffect.startComposition().addPrimitive(
                  VibrationEffect.Composition.PRIMITIVE_SLOW_RISE,
                  0.3f
                ).addPrimitive(
                  VibrationEffect.Composition.PRIMITIVE_QUICK_FALL,
                  0.3f
                ).compose()
              )
            } else {
              currentState = ExpandShapeState.Collapsed
              vibrator.vibrate(
                VibrationEffect.startComposition().addPrimitive(
                  VibrationEffect.Composition.PRIMITIVE_SLOW_RISE
                ).compose()
              )
          }
        )
    ) {
      // Build the indicator UI based on the current state.
      ExpandIndicator(transitionData)
    }
  }
}

Java

class ClickListener implements View.OnClickListener {
  private final Animation expandAnimation;
  private final Animation collapseAnimation;
  private boolean isExpanded;

  ClickListener(Context context) {
    expandAnimation = AnimationUtils.loadAnimation(context, R.anim.expand);
    expandAnimation.setAnimationListener(new Animation.AnimationListener() {

      @Override
      public void onAnimationStart(Animation animation) {
        vibrator.vibrate(
          VibrationEffect.startComposition()
            .addPrimitive(VibrationEffect.Composition.PRIMITIVE_SLOW_RISE, 0.3f)
            .addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_FALL, 0.3f)
            .compose());
      }
    });

    collapseAnimation = AnimationUtils.loadAnimation(context, R.anim.collapse);
    collapseAnimation.setAnimationListener(new Animation.AnimationListener() {

      @Override
      public void onAnimationStart(Animation animation) {
        vibrator.vibrate(
          VibrationEffect.startComposition()
            .addPrimitive(VibrationEffect.Composition.PRIMITIVE_SLOW_RISE)
            .compose());
      }
    });
  }

  @Override
  public void onClick(View view) {
    view.startAnimation(isExpanded ? collapseAnimation : expandAnimation);
    isExpanded = !isExpanded;
  }
}

Exemplo: oscilação (com giros)

Um dos principais princípios de retorno tátil é encantar os usuários. Uma maneira divertida de introduzir um efeito de vibração inesperado e agradável é usar o PRIMITIVE_SPIN. Essa primitiva é mais eficaz quando é chamada mais de uma vez. Vários giros concatenados podem criar um efeito instável e oscilante, que pode ser melhorado aplicando uma escalação um tanto aleatória em cada primitivo. Você também pode testar a lacuna entre primitivas de giro sucessivas. Duas rotações sem intervalo (0 ms entre elas) criam uma sensação de giro apertado. Aumentar o intervalo entre os giros de 10 para 50 ms leva a uma sensação de giro mais solto e pode ser usado para corresponder à duração de um vídeo ou animação.

Não recomendamos o uso de uma lacuna maior que 100 ms, porque os giros sucessivos não se integram bem e começam a parecer efeitos individuais.

Confira um exemplo de forma elástica que volta para cima depois de ser arrastada para baixo e depois liberada. A animação é aprimorada com um par de efeitos de giro, reproduzidos com intensidades variáveis proporcionais ao deslocamento de salto.

Animação de uma forma elástica que salta
Gráfico da forma de onda da vibração de entrada

Figura 3. Essa forma de onda representa a aceleração de saída da vibração em um dispositivo.

Kotlin

@Composable
fun WobbleScreen() {
    // Control variables for the dragging and animating state of the elastic.
    var dragDistance by remember { mutableStateOf(0f) }
    var isWobbling by remember { mutableStateOf(false) }
 
    // Use drag distance to create an animated float value behaving like a spring.
    val dragDistanceAnimated by animateFloatAsState(
        targetValue = if (dragDistance > 0f) dragDistance else 0f,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioHighBouncy,
            stiffness = Spring.StiffnessMedium
        ),
    )
 
    if (isWobbling) {
        LaunchedEffect(Unit) {
            while (true) {
                val displacement = dragDistanceAnimated / MAX_DRAG_DISTANCE
                // Use some sort of minimum displacement so the final few frames
                // of animation don't generate a vibration.
                if (displacement > SPIN_MIN_DISPLACEMENT) {
                    vibrator.vibrate(
                        VibrationEffect.startComposition().addPrimitive(
                            VibrationEffect.Composition.PRIMITIVE_SPIN,
                            nextSpinScale(displacement)
                        ).addPrimitive(
                          VibrationEffect.Composition.PRIMITIVE_SPIN,
                          nextSpinScale(displacement)
                        ).compose()
                    )
                }
                // Delay the next check for a sufficient duration until the current
                // composition finishes. Note that you can use
                // Vibrator.getPrimitiveDurations API to calculcate the delay.
                delay(VIBRATION_DURATION)
            }
        }
    }
 
    Box(
        Modifier
            .fillMaxSize()
            .draggable(
                onDragStopped = {
                    isWobbling = true
                    dragDistance = 0f
                },
                orientation = Orientation.Vertical,
                state = rememberDraggableState { delta ->
                    isWobbling = false
                    dragDistance += delta
                }
            )
    ) {
        // Draw the wobbling shape using the animated spring-like value.
        WobbleShape(dragDistanceAnimated)
    }
}

// Calculate a random scale for each spin to vary the full effect.
fun nextSpinScale(displacement: Float): Float {
  // Generate a random offset in [-0.1,+0.1] to be added to the vibration
  // scale so the spin effects have slightly different values.
  val randomOffset: Float = Random.Default.nextFloat() * 0.2f - 0.1f
  return (displacement + randomOffset).absoluteValue.coerceIn(0f, 1f)
}

Java

class AnimationListener implements DynamicAnimation.OnAnimationUpdateListener {
  private final Random vibrationRandom = new Random(seed);
  private final long lastVibrationUptime;

  @Override
  public void onAnimationUpdate(DynamicAnimation animation, float value, float velocity) {
    // Delay the next check for a sufficient duration until the current
    // composition finishes. Note that you can use
    // Vibrator.getPrimitiveDurations API to calculcate the delay.
    if (SystemClock.uptimeMillis() - lastVibrationUptime < VIBRATION_DURATION) {
      return;
    }

    float displacement = calculateRelativeDisplacement(value);

    // Use some sort of minimum displacement so the final few frames
    // of animation don't generate a vibration.
    if (displacement < SPIN_MIN_DISPLACEMENT) {
      return;
    }

    lastVibrationUptime = SystemClock.uptimeMillis();
    vibrator.vibrate(
      VibrationEffect.startComposition()
        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_SPIN, nextSpinScale(displacement))
        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_SPIN, nextSpinScale(displacement))
        .compose());
  }

  // Calculate a random scale for each spin to vary the full effect.
  float nextSpinScale(float displacement) {
    // Generate a random offset in [-0.1,+0.1] to be added to the vibration
    // scale so the spin effects have slightly different values.
    float randomOffset = vibrationRandom.nextFloat() * 0.2f - 0.1f
    return MathUtils.clamp(displacement + randomOffset, 0f, 1f)
  }
}

Exemplo: rejeição (com ruídos)

Outro uso avançado dos efeitos de vibração é simular interações físicas. O PRIMITIVE_THUD pode criar um efeito forte e reverberante, que pode ser combinado com a visualização de um impacto, em um vídeo ou animação, por exemplo, para aumentar a experiência geral.

Confira um exemplo de animação simples de queda de bola aprimorada com um efeito de som tocado sempre que a bola quica na parte de baixo da tela:

Animação de uma bola caindo e quicando na parte de baixo da tela
Gráfico da forma de onda da vibração de entrada

Figura 4. Essa forma de onda representa a aceleração de saída da vibração em um dispositivo.

Kotlin

enum class BallPosition {
    Start,
    End
}

@Composable
fun BounceScreen() {
  // Control variable for the state of the ball.
  var ballPosition by remember { mutableStateOf(BallPosition.Start) }
  var bounceCount by remember { mutableStateOf(0) }

  // Animation for the bouncing ball.
  var transitionData = updateTransitionData(ballPosition)
  val collisionData = updateCollisionData(transitionData)

  // Ball is about to contact floor, only vibrating once per collision.
  var hasVibratedForBallContact by remember { mutableStateOf(false) }
  if (collisionData.collisionWithFloor) {
    if (!hasVibratedForBallContact) {
      val vibrationScale = 0.7.pow(bounceCount++).toFloat()
      vibrator.vibrate(
        VibrationEffect.startComposition().addPrimitive(
          VibrationEffect.Composition.PRIMITIVE_THUD,
          vibrationScale
        ).compose()
      )
      hasVibratedForBallContact = true
    }
  } else {
    // Reset for next contact with floor.
    hasVibratedForBallContact = false
  }

  Screen() {
    Box(
      Modifier
        .fillMaxSize()
        .clickable {
          if (transitionData.isAtStart) {
            ballPosition = BallPosition.End
          } else {
            ballPosition = BallPosition.Start
            bounceCount = 0
          }
        },
    ) {
      // Build the ball UI based on the current state.
      BouncingBall(transitionData)
    }
  }
}

Java

class ClickListener implements View.OnClickListener {
  @Override
  public void onClick(View view) {
    view.animate()
      .translationY(targetY)
      .setDuration(3000)
      .setInterpolator(new BounceInterpolator())
      .setUpdateListener(new AnimatorUpdateListener() {

        boolean hasVibratedForBallContact = false;
        int bounceCount = 0;

        @Override
        public void onAnimationUpdate(ValueAnimator animator) {
          boolean valueBeyondThreshold = (float) animator.getAnimatedValue() > 0.98;
          if (valueBeyondThreshold) {
            if (!hasVibratedForBallContact) {
              float vibrationScale = (float) Math.pow(0.7, bounceCount++);
              vibrator.vibrate(
                VibrationEffect.startComposition()
                  .addPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD, vibrationScale)
                  .compose());
              hasVibratedForBallContact = true;
            }
          } else {
            // Reset for next contact with floor.
            hasVibratedForBallContact = false;
          }
        }
      });
  }
}