Criar efeitos táteis personalizados

Esta página aborda os exemplos de como usar diferentes APIs de háptica para criar efeitos personalizados além das formas de onda de vibração padrão em um app Android.

Esta página inclui os seguintes exemplos:

Para conferir outros 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 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 haptics do Android fornece detalhes sobre como verificar o suporte a componentes envolvidos nas haptics 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 continuar 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 da 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 geralmente são perceptíveis, mas vibrações longas e repentinas podem assustar o usuário se tocadas em um ambiente silencioso. Aumentar a amplitude de destino muito rápido também pode produzir ruídos audíveis. Crie padrões de forma de onda para suavizar as transições de amplitude e criar efeitos de aumento e diminuição.

Exemplos de padrões de vibração

As seções a seguir apresentam vários exemplos de padrões de vibração:

Padrão de otimizaçã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 0 representa o "estado desligado" do vibrador 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 // Don't 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; // Don't repeat.

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

Padrão repetitivo

As formas de onda também podem ser reproduzidas repetidamente até serem canceladas. A maneira de criar uma forma de onda repetida é definir um parâmetro repeat 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.

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 o dispositivo vibre na amplitude máxima para cada entrada positiva na matriz de amplitude. Se o app precisar acomodar esses dispositivos, use um padrão que não gere um efeito de zumbido quando reproduzido nessa condição ou crie um padrão LIGADO/DESLIGADO mais simples que possa ser reproduzido como fallback.

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 compor vibrações em efeitos personalizados mais longos e complexos e vai além para explorar a haptics avançada 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 hápticos mais complexos em dispositivos com acionadores hápticos 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 aproveitando a faixa 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 diminuendo.

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 Noções básicas sobre atuadores de vibração.

O Android não oferece substitutos para composições com primitivas sem suporte. Portanto, 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.

Mais informações sobre como verificar o suporte do dispositivo são mostradas nas seções a seguir.

Criar efeitos de vibração compostos

É possível criar efeitos de vibração compostos com VibrationEffect.Composition. Confira um exemplo de efeito de aumento lento seguido por 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.

Criar variantes em primitivas de vibração

Se você quiser criar uma versão fraca e forte da mesma primitiva, crie razões de força 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 da mesma primitiva, porque elas não são perceptivamente distintas. Por exemplo, use escalas de 0,5, 0,7 e 1,0 para criar versões de intensidade baixa, média e alta de uma primitiva.

Adicionar intervalos entre primitivas de vibração

A composição também pode especificar atrasos a serem adicionados entre primitivos consecutivos. Esse atraso é expresso em milissegundos desde o fim da primitiva anterior. Em geral, um intervalo de 5 a 10 ms entre duas primitivas é muito curto para ser detectado. Use uma lacuna de 50 ms ou mais se quiser criar uma lacuna 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());

Verificar quais primitivas têm suporte

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

Exemplos de composições de vibração

As seções a seguir fornecem vários exemplos de composições de vibração, retirados do app de amostra de haptics (link em inglês) no GitHub.

Resistir (com tiques baixos)

É possível controlar a amplitude da vibração primitiva para transmitir um 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 arrastar 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);
    }
}

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 a mesma meta, mas com durações diferentes. Há apenas uma primitiva para diminuir, PRIMITIVE_QUICK_FALL. Essas primitivas funcionam melhor em conjunto para criar um segmento de forma de onda que cresce em intensidade e depois desaparece. É possível alinhar primitivas dimensionadas para evitar saltos repentinos 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 subida e descida 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.Esta 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;
    }
}

Equilíbrio (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 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 aprimorado aplicando uma escalação um tanto aleatória em cada primitiva. 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 rotação apertada. Aumentar o intervalo entre as rotações de 10 para 50 ms gera uma sensação de rotação mais solta e pode ser usado para corresponder à duração de um vídeo ou animação.

Não use um intervalo maior que 100 ms, porque as rotações sucessivas 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 the range [-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 the range [-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)
    }
}

Ressalto (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 melhorar a experiência geral.

Confira um exemplo de animação de queda de bola aprimorada com um efeito de som tocado sempre que a bola bate 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;
            }
            }
        });
    }
}