Cómo crear efectos táctiles personalizados

En esta página, se incluyen ejemplos de cómo usar diferentes APIs de tecnología táctil para crear efectos personalizados en una aplicación para Android. Dado que gran parte de la información de esta página se basa en buenos conocimientos sobre el funcionamiento de un accionador de vibración, te recomendamos que leas el Manual del accionador de vibraciones.

En esta página, se incluyen los siguientes ejemplos.

Para ver ejemplos adicionales, consulta Cómo agregar respuestas táctiles a eventos y sigue siempre los principios de diseño de tecnología táctil.

Usa resguardos para controlar la compatibilidad de dispositivos

Cuando implementes algún efecto personalizado, ten en cuenta lo siguiente:

  • Qué funciones del dispositivo se requieren para el efecto
  • Qué hacer cuando el dispositivo no puede reproducir el efecto

En la referencia de la API de tecnología táctil de Android, se proporcionan detalles para comprobar la compatibilidad de los componentes involucrados en la tecnología táctil de Android, de modo que tu app pueda brindar una experiencia general coherente.

Según tu caso de uso, es posible que desees inhabilitar los efectos personalizados o proporcionar otros alternativos en función de diferentes capacidades potenciales.

Planifica las siguientes clases de alto nivel de capacidad del dispositivo:

  • Si usas primitivas táctiles: dispositivos que admiten las primitivas que necesitan los efectos personalizados. (Consulta la siguiente sección para obtener detalles sobre las primitivas).

  • Dispositivos con control de amplitud

  • Dispositivos con compatibilidad básica de vibración (encendido/apagado), es decir, aquellos que no tienen control de amplitud.

Si la elección de efectos táctiles de tu app contempla estas categorías, la experiencia del usuario táctil debe seguir siendo predecible para cualquier dispositivo individual.

Uso de primitivas táctiles

Android incluye varias primitivas de tecnología táctil que varían en amplitud y frecuencia. Puedes usar solo una primitiva o varias primitivas combinadas para lograr efectos táctiles enriquecidos.

  • Usa retrasos de 50 ms o más para intervalos perceptibles entre dos primitivas, teniendo en cuenta también la duración de la primitiva, si es posible.
  • Usa escalas que difieran en una proporción de 1.4 o más para que la diferencia en la intensidad se perciba mejor.
  • Usa escalas de 0.5, 0.7 y 1.0 para crear una versión de intensidad baja, media y alta de un elemento primitivo.

Crea patrones de vibración personalizados

Los patrones de vibración suelen usarse en la tecnología háptica de atención, como las notificaciones y los tonos. El servicio Vibrator puede reproducir patrones de vibración largos que cambian la amplitud de la vibración con el tiempo. Esos efectos se denominan formas de onda.

Los efectos de forma de onda son fáciles de percibir, pero las vibraciones largas y repentinas pueden alterar el usuario si se reproducen en un entorno silencioso. Aumentar la velocidad a una amplitud objetivo demasiado rápida también puede producir zumbidos audibles. Para diseñar patrones de forma de onda, se recomienda suavizar las transiciones de amplitud a fin de crear efectos de aumento y reducción de aumento.

Muestra: Patrón de aumento

Las formas de onda se representan como VibrationEffect con tres parámetros:

  1. Timing: un array de duraciones, en milisegundos, para cada segmento de forma de onda.
  2. Amplitudes: Es la amplitud de vibración deseada para cada duración especificada en el primer argumento, representada por un valor entero de 0 a 255, donde 0 representa la opción de apagado del vibrador y 255 es la amplitud máxima del dispositivo.
  3. Repetir índice: Es el índice del array especificado en el primer argumento para comenzar a repetir la forma de onda, o -1 si debe reproducir el patrón solo una vez.

Este es un ejemplo de una forma de onda que pulsa dos veces, con una pausa de 350 ms entre ellos. El primer pulso es un aumento suave hasta la amplitud máxima y el segundo es un aumento rápido para mantener la amplitud máxima. El valor del índice de repetición negativo define la detención al final.

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

Muestra: Patrón de repetición

Las formas de onda también se pueden reproducir reiteradamente hasta que se cancelen. La forma de crear una forma de onda repetitiva es establecer un parámetro "repetir" no negativo. Cuando reproduces una forma de onda repetida, la vibración continúa hasta que se cancela explícitamente en el servicio:

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

Esto es muy útil para los eventos intermitentes que requieren la acción del usuario para confirmarlos. Algunos ejemplos de estos eventos incluyen las llamadas telefónicas entrantes y las alarmas activadas.

Muestra: Patrón con resguardo

El control de la amplitud de una vibración es una capacidad que depende del hardware. Reproducir una forma de onda en un dispositivo de gama baja sin esta capacidad hace que vibre a la amplitud máxima para cada entrada positiva en el array de amplitud. Si tu app necesita admitir esos dispositivos, te recomendamos que te asegures de que el patrón no genere un efecto de zumbido cuando se reproduzca en esa condición, o bien que diseñes un patrón de encendido y apagado más simple que se pueda reproducir como resguardo.

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

Cómo crear composiciones de vibración

En esta sección, se presentan formas de componerlos en efectos personalizados más largos y complejos, y se va más allá de eso para explorar la tecnología táctil enriquecida con capacidades de hardware más avanzadas. Puedes usar combinaciones de efectos que varían de amplitud y frecuencia para crear efectos táctiles más complejos en dispositivos con accionadores táctiles que tienen un ancho de banda de frecuencia más amplio.

En el proceso de creación de patrones de vibración personalizados, que se describió anteriormente en esta página, se explica cómo controlar la amplitud de la vibración para crear efectos fluidos de aumento y disminución. La tecnología táctil enriquecida mejora este concepto mediante la exploración del rango de frecuencia más amplio del vibrador del dispositivo para que el efecto sea aún más fluido. Estas formas de onda son especialmente eficaces para crear un efecto de crescendo o diminuendo.

El fabricante del dispositivo implementa las primitivas de composición, descritas anteriormente en esta página. Proporcionan una vibración nítida, corta y agradable que se alinea con los principios de la tecnología táctil para una tecnología táctil clara. Para obtener más detalles sobre estas funciones y cómo funcionan, consulta Conceptos básicos sobre los accionadores de vibración.

Android no proporciona resguardos para composiciones con primitivas no compatibles. Te recomendamos que sigas estos pasos:

  1. Antes de activar la tecnología táctil avanzada, comprueba que un dispositivo determinado admita todas las primitivas que usas.

  2. Inhabilita el conjunto coherente de experiencias que no son compatibles, no solo los efectos a los que les falta una primitiva. A continuación, se muestra más información para verificar la compatibilidad del dispositivo.

Puedes crear efectos de vibración con VibrationEffect.Composition. El siguiente es un ejemplo de un efecto que aumenta lentamente seguido de un efecto de clic agudo:

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

Para crear una composición, se agregan primitivas que se reproducirán en secuencia. Cada primitiva también es escalable, de modo que puedes controlar la amplitud de la vibración que genera cada una. La escala se define como un valor entre 0 y 1, en el que 0 se asigna en realidad a una amplitud mínima a la que el usuario puede sentir (apenas) esta primitiva.

Si quieres crear una versión débil y sólida de la misma primitiva, se recomienda que las escalas difieran en una proporción de 1.4 o más, para que la diferencia en la intensidad se pueda percibir con facilidad. No intentes crear más de tres niveles de intensidad del mismo tipo primitivo, ya que no se distinguen en la percepción. Por ejemplo, usa escalas de 0.5, 0.7 y 1.0 para crear una versión de intensidad baja, media y alta de una primitiva.

La composición también puede especificar demoras que se deben agregar entre primitivas consecutivas. Este retraso se expresa en milisegundos desde el final de la primitiva anterior. En general, un intervalo de 5 a 10 ms entre dos primitivas es demasiado corto para ser detectable. Considera usar un espacio de 50 ms o más si quieres crear un espacio distinguible entre dos primitivas. A continuación, se muestra un ejemplo de una composición con retrasos:

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

Las siguientes APIs se pueden usar para verificar la compatibilidad del dispositivo con 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.
}

También es posible verificar varias primitivas y, luego, decidir cuáles componer según el nivel de compatibilidad del 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);

Muestra: Resistir (con marcas bajas)

Puedes controlar la amplitud de la vibración básica para transmitir comentarios útiles a una acción en curso. Se pueden usar valores de escala muy espaciados para crear un efecto de crescendo suave de una primitiva. El retraso entre primitivas consecutivas también se puede establecer de forma dinámica según la interacción del usuario. Esto se ilustra en el siguiente ejemplo de una animación de vista controlada por un gesto de arrastre y aumentada con tecnología táctil.

Animación de un círculo que se arrastra hacia abajo
Gráfico de la forma de onda de la vibración 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);
  }
}

Muestra: Expandir (con ascenso y descenso)

Hay dos primitivas para aumentar la intensidad de la vibración percibida: PRIMITIVE_QUICK_RISE y PRIMITIVE_SLOW_RISE. Ambos alcanzan el mismo objetivo, pero con duraciones distintas. Solo hay una primitiva para la expansión, PRIMITIVE_QUICK_FALL. Estas primitivas funcionan mejor juntas para crear un segmento de forma de onda que aumenta su intensidad y luego desaparece. Puedes alinear las primitivas escaladas para evitar saltos repentinos de amplitud entre ellos, lo que también funciona bien para extender la duración general del efecto. Desde el punto de vista perceptual, las personas siempre ven la parte ascendente más que la parte descendente, por lo que se puede usar la porción ascendente más corta que la parte que cae para cambiar el énfasis hacia la parte descendente.

A continuación, se muestra un ejemplo de una aplicación de esta composición para expandir y contraer un círculo. El efecto aumento puede mejorar la sensación de expansión durante la animación. La combinación de efectos de elevación y caída ayuda a enfatizar el colapso al final de la animación.

Animación de un círculo que se expande
Gráfico de la forma de onda de la vibración 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;
  }
}

Muestra: Wobble (con giros)

Uno de los principios de la tecnología táctil clave es deleitar a los usuarios. Una manera divertida de introducir un efecto de vibración inesperado y agradable es usar PRIMITIVE_SPIN. Esta primitiva es más eficaz cuando se la llama más de una vez. Varios giros concatenados pueden crear un efecto inestable y tambaleante, que se puede mejorar aún más si se aplica un escalamiento aleatorio en cada primitiva. También puedes experimentar con el espacio entre primitivas de giro sucesivo. Dos giros sin ningún espacio (0 ms intermedios) crean una sensación de giro intenso. Aumentar la brecha entre los giros de 10 ms a 50 ms genera una sensación de rotación más flexible y se puede usar para igualar la duración de un video o una animación.

No se recomienda usar un intervalo de más de 100 ms, ya que los giros sucesivos ya no se integran bien y comienzan a parecer efectos individuales.

A continuación, se muestra un ejemplo de una forma elástica que rebota después de que se arrastra hacia abajo y luego se suelta. La animación se mejora con un par de efectos de giro, reproducidos con varias intensidades que son proporcionales al desplazamiento del rebote.

Animación de una forma elástica que rebota
Gráfico de la forma de onda de la vibración 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)
  }
}

Ejemplo: Rebote (con golpes)

Otra aplicación avanzada de los efectos de vibración es la simulación de interacciones físicas. El objeto PRIMITIVE_THUD puede crear un efecto fuerte y con resonancia, que se puede combinar con la visualización de un impacto, por ejemplo, en un video o una animación, para aumentar la experiencia general.

A continuación, se muestra un ejemplo de una simple animación de caída de la bola mejorada con un efecto de golpe que se reproduce cada vez que la bola rebota en la parte inferior de la pantalla:

Animación de una bola que rebota desde la parte inferior de la pantalla
Gráfico de la forma de onda de la vibración 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;
          }
        }
      });
  }
}