Cómo crear efectos táctiles personalizados

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

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

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

Cómo usar resguardos para controlar la compatibilidad de dispositivos

Cuando implementes cualquier efecto personalizado, ten en cuenta lo siguiente:

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

La referencia de la API de tecnología táctil de Android proporciona detalles para comprobar compatibilidad con componentes involucrados en la tecnología táctil, de modo que tu app pueda proporcionar experiencia general coherente.

Según tu caso de uso, es posible que desees inhabilitar los efectos personalizados o proporcionar efectos personalizados 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 esas primitivas necesarios por los efectos personalizados. (Consulta la siguiente sección para obtener más detalles las primitivas).

  • Dispositivos con control de amplitud.

  • Los dispositivos que admiten vibración básica (activada/desactivada), es decir, los o que carece de control de amplitud.

Si la elección del efecto táctil de tu app se tiene en cuenta para estas categorías, entonces, su la experiencia táctil del usuario debe seguir siendo predecible para cualquier dispositivo individual.

Uso de primitivas táctiles

Android incluye varias primitivas táctiles que varían en amplitud y frecuencia. Puedes usar una sola primitiva o varias primitivas en combinación. para lograr efectos táctiles enriquecidos.

  • Usa retrasos de 50 ms o más para espacios perceptibles entre dos primitivas, también teniendo en cuenta la primitiva Duración si es posible.
  • Usa escalas que difieren en una proporción de 1,4 o más para que la diferencia en la intensidad de la luz se percibe mejor.
  • Usa escalas de 0.5, 0.7 y 1.0 para crear una puntuación baja, media y alta. de intensidad de un objeto primitivo.

Crea patrones de vibración personalizados

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

Los efectos de la forma de onda pueden percibirse fácilmente, pero las vibraciones largas y repentinas asustar al usuario si se juega en un entorno silencioso. Escalamiento hasta una amplitud objetivo demasiado rápido también puede producir ruidos de zumbidos audibles. La recomendación para diseñar patrones de forma de onda es suavizar las transiciones de amplitud para crear efectos de aumento y disminución.

Muestra: Patrón de aumento

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

  1. Tiempos: Es un array de las duraciones, en milisegundos, de cada forma de onda. segmento.
  2. Amplitudes: La amplitud de vibración deseada para cada duración especificada. en el primer argumento, representado por un valor entero de 0 a 255, con 0 que representa al vibrador “apagado” y 255 es la cantidad máxima en la amplitud.
  3. Repetir índice: el índice del array especificado en el primer argumento en empezar 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 parpadea dos veces con una pausa de 350 ms los pulsos. El primer pulso es un aumento suave hasta la amplitud máxima, y el y la segunda es una rampa rápida para conservar la amplitud máxima. La detención al final está definida por el valor del índice de repetición negativo.

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 repetidamente hasta que se cancele. La forma de crear un de la forma de onda repetida consiste en configurar un parámetro "repetir" no negativo. Cuando reproduces un repetir la forma de onda, 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 una acción del usuario para y reconocerlo. Algunos ejemplos de tales eventos incluyen las llamadas telefónicas entrantes y alarmas activadas.

Muestra: Patrón con resguardo

Controlar la amplitud de una vibración es una capacidad dependiente del hardware. Reproducir una forma de onda en un un dispositivo de gama baja sin esta capacidad hace que vibre al máximo para cada entrada positiva en la matriz de amplitud. Si tu app necesita para estos dispositivos, la recomendación es asegurarse de que su patrón no genera un efecto de zumbido cuando se reproduce en esa condición, o para diseñar un patrón de encendido/apagado más simple que se pueda jugar 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 muestran formas de integrarlos efectos personalizados más largos y complejos, y va más allá para explorar la tecnología táctil 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 mayor.

El proceso para crear vibración personalizada de imágenes, como se describió anteriormente en esta página, explica cómo controlar la amplitud de la vibración para crear efectos suaves de aumentan y disminuyen. La tecnología táctil enriquecida mejora este concepto explorando la más amplio rango de frecuencia del vibrador del dispositivo para que el efecto sea aún más fluido. Estas formas de onda son especialmente eficaces para crear un crescendo o diminuendo. efecto.

Las primitivas de composición, descritas anteriormente en esta página, son implementadas por el fabricante del dispositivo. Proporcionan una vibración nítida, breve y agradable. que se alinea con los principios de tecnología táctil para ofrecer una tecnología táctil clara. Para ver más detalles sobre estas capacidades y cómo funcionan, consulte Accionadores de vibración manual.

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

  1. Antes de activar la tecnología táctil avanzada, verifica que un dispositivo determinado sea compatible todas las primitivas que uses.

  2. Inhabilita el conjunto coherente de experiencias que no son compatibles, no solo las efectos a los que les falta un primitivo. Más información sobre cómo verificar el la compatibilidad del dispositivo se muestra de la siguiente manera.

Puedes crear efectos de vibración compuestos con VibrationEffect.Composition. Este es un ejemplo de un efecto que aumenta lentamente seguido de un efecto de clic 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());

Para crear una composición, se agregan primitivas que se reproducirán en secuencia. Cada también es escalable, por lo que puedes controlar la amplitud de la vibración generadas por cada una de ellas. La escala se define como un valor entre 0 y 1, donde 0 se asigna a una amplitud mínima en la que esta primitiva (casi) sintió el usuario.

Si quieres crear una versión débil y sólida del mismo primitivo, es recomendó que las escalas difieran en una proporción de 1.4 o más, de modo que la diferencia de intensidad pueden percibirse fácilmente. No intentes crear más de tres de intensidad del mismo primitivo, porque no están distinto. Por ejemplo, usa escalas de 0.5, 0.7 y 1.0 para crear un rango y de alta intensidad de un primitivo.

La composición también puede especificar retrasos que se deben agregar entre primitivas. Esta demora 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. Te recomendamos usar un espacio de 50 ms o más. si quieres crear un espacio perceptible entre dos primitivas. Este es 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());

Se pueden usar las siguientes APIs para verificar la compatibilidad del dispositivo con determinados primitivas:

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 Compose 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: Resiste (con marcas bajas)

Puedes controlar la amplitud de la vibración primitiva para transmitir feedback útil a una acción en curso. Los valores de escala muy espaciados pueden para crear un efecto crescendo suave de un elemento primitivo. La demora entre primitivas consecutivas también se pueden configurar dinámicamente en función del interacción. Esto se ilustra en el siguiente ejemplo de una animación de vista controlado con un gesto de arrastre y aumentado con tecnología táctil.

Animación de un círculo que se arrastra hacia abajo
Trama 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: Expansión (con aumento y descenso)

Existen 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 distintas duraciones. Solo hay una básica para la reducción, el PRIMITIVE_QUICK_FALL Estas primitivas funcionan mejor juntas para crear un segmento en forma de onda que crezca en intensa y, luego, se apaga. Puedes alinear primitivas escaladas para evitar ataques en la amplitud entre ellos, lo que también funciona bien para extender la duración del efecto. En la percepción, las personas siempre observan que la porción en aumento más que a la parte que cae, de modo que hacer que la parte ascendente sea más corta se puede usar para cambiar el énfasis hacia la parte que cae.

Aquí hay un ejemplo de una aplicación de esta composición para expandir y y contrae un círculo. El efecto aumento puede mejorar la sensación de expansión durante la animación. La combinación de los efectos de salida y caída ayuda a enfatizar la y se contrae al final de la animación.

Animación de un círculo que se expande
Trama 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: Balancín (con giros)

Uno de los principios de la tecnología táctil clave es deleitar a los usuarios. Una forma divertida para introducir un efecto de vibración agradable inesperado es utilizar el PRIMITIVE_SPIN Esta primitiva es más eficaz cuando se la llama más de una vez. Múltiples o giros concatenados pueden crear un efecto tambaleante e inestable, que puede y se mejoró aún más mediante la aplicación de un escalamiento aleatorio en cada primitiva. Tú también puedes experimentar con la brecha entre las primitivas de giro sucesivas. Dos giros sin espacios (0 ms en el medio) crea una sensación que gira drásticamente. En aumento la brecha entre giros de 10 a 50 ms genera una sensación de giro más suelta, y se puede usar para hacer coincidir la duración de un video o una animación.

No recomendamos usar un intervalo superior a 100 ms, ya que el tiempo ya no se integran bien y empiezan a parecer efectos individuales.

Aquí hay un ejemplo de una forma elástica que rebota después de ser arrastrada hacia abajo y, luego, se lanzan. Se mejora la animación con un par de efectos de giro que se reproducen con intensidades variables proporcionales al desplazamiento por rebote.

Animación de una forma elástica que rebota
Trama 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)
  }
}

Muestra: Rebotes (con golpes)

Otra aplicación avanzada de los efectos de vibración es la simulación física interacciones. El PRIMITIVE_THUD pueden crear un efecto potente y reverberante, que puede combinarse con visualización de un impacto, en un video o animación, por ejemplo, para aumentar la experiencia general.

Este es un ejemplo de una animación simple de caída de la bola mejorada con un efecto de golpe corto se juega cada vez que la pelota rebota en la parte inferior de la pantalla:

Animación de una pelota soltada que rebota en la parte inferior de la pantalla
Trama 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;
          }
        }
      });
  }
}