En esta página, se incluyen ejemplos de cómo usar diferentes APIs de tecnología táctil para crear efectos personalizados más allá de las formas de onda de vibración estándar en una app para Android.
En esta página, se incluyen los siguientes ejemplos:
- Composiciones fijadas en el cronograma con
VibrationEffect.Builder- Redacta con ajustes predeterminados: Secuencia sensaciones táctiles predefinidas.
- Compón con envolventes y ajustes predeterminados: Combina envolventes y ajustes predeterminados en una línea de tiempo.
- Reutiliza y desplaza eventos: Desplaza y reutiliza eventos de composición existentes.
- Composiciones repetitivas: Crea efectos de línea de tiempo repetitivos.
- Patrones de vibración personalizados
- Patrón de aumento gradual: Es un patrón que comienza de forma gradual.
- Patrón repetitivo: Es un patrón sin fin.
- Patrón con resguardo: Es una demostración de resguardo.
- Composiciones de primitivas de vibración
- Resistencia: Es un efecto de arrastre con intensidad dinámica.
- Expandir: Un efecto de aumento y, luego, de disminución.
- Wobble: Es un efecto de tambaleo que usa la primitiva
SPIN. - Rebote: Es un efecto de rebote que usa la primitiva
THUD.
- Forma de onda de vibración con envolventes
- Resorte que rebota: Es un efecto de rebote elástico que usa efectos de envolvente básicos.
- Lanzamiento de cohete: Es un efecto de lanzamiento de cohete que usa efectos de envolvente de forma de onda.
Para ver más ejemplos, consulta Cómo agregar respuestas táctiles a eventos y sigue siempre los principios de diseño de las respuestas táctiles.
Usa resguardos para controlar la compatibilidad de los dispositivos
Cuando implementas efectos de respuesta táctil personalizados, la compatibilidad con dispositivos y el comportamiento de resguardo dependen de la plataforma de la API que elijas:
VibrationEffect.Builder(recomendado): A partir de Android 16 (26Q4), los efectos creados conVibrationEffect.Builderincluyen un resguardo automático a nivel del framework. Si un dispositivo no admite de forma nativa unPresetsolicitado o unEnvelopebásico, el framework lo traduce automáticamente a una alternativa adecuada en el momento de la reproducción con el mejor esfuerzo. No es necesario que verifiques de forma manual las capacidades del dispositivo por primitiva antes de reproducir efectos compuestos conVibrationEffect.Builder.- Excepción: Las envolventes de formas de onda avanzadas creadas con
WaveformEnvelopeBuilderno admiten el resguardo automático porque dependen de asignaciones de frecuencia de hardware específicas (FOAM). Si no es compatible, no se reproducirá.
- Excepción: Las envolventes de formas de onda avanzadas creadas con
VibrationEffect.Composition: Las composiciones creadas con la API destartComposition()no tienen resguardos automáticos. Si una composición contiene incluso una primitiva no compatible, no se reproducirá toda la vibración. Debes verificar las capacidades de forma manual convibrator.arePrimitivesSupported().- Formas de onda con control de amplitud: Las amplitudes que no son cero se redondean al 100% en los dispositivos que no tienen control de amplitud. Verifica
vibrator.hasAmplitudeControl()y recurre a un patrón de ACTIVADO/DESACTIVADO diseñado de forma explícita si es necesario.
Uso de primitivas de respuesta táctil
Android incluye varias primitivas de respuesta táctil que varían tanto en amplitud como en frecuencia. Puedes usar una sola primitiva o varias combinadas para lograr efectos táctiles enriquecidos.
- Usa retrasos de 50 ms o más para que haya intervalos perceptibles entre dos primitivas, y ten 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 se perciba mejor la diferencia de intensidad.
Usa escalas de 0.5, 0.7 y 1.0 para crear una versión de intensidad baja, media y alta de una primitiva.
Cómo crear patrones de vibración personalizados
Los patrones de vibración se suelen usar en la tecnología táctil de atención, como las notificaciones y los tonos de llamada. El servicio Vibrator puede reproducir patrones de vibración largos que cambian la amplitud de la vibración con el tiempo. Estos efectos se denominan
formas de onda.
Por lo general, los efectos de forma de onda son perceptibles, pero las vibraciones largas y repentinas pueden asustar al usuario si se reproducen en un entorno silencioso. Aumentar la amplitud objetivo con demasiada rapidez también puede producir ruidos de zumbido audibles. Diseña patrones de formas de onda para suavizar las transiciones de amplitud y crear efectos de aumento y disminución.
Ejemplos de patrones de vibración
En las siguientes secciones, se proporcionan varios ejemplos de patrones de vibración:
Patrón de aumento gradual
Las formas de onda se representan como VibrationEffect con tres parámetros:
- Tiempos: Es un array de duraciones, en milisegundos, para cada segmento de forma de onda.
- 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 el "estado de apagado" del vibrador y 255 es la amplitud máxima del dispositivo.
- Índice de repetición: 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 forma de onda que emite dos pulsos 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. La detención al final se define por el valor negativo del índice de repetición.
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 ) )
Patrón repetitivo
Las formas de onda también se pueden reproducir de forma repetida hasta que se cancelen. La forma de crear una forma de onda repetitiva es establecer un parámetro repeat no negativo. Cuando reproduces una forma de onda repetitiva, la vibración continúa hasta que se cancela de forma explícita en el servicio:
fun 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. val repeatingEffect: VibrationEffect = VibrationEffect.createWaveform( timings, amplitudes, repeat ) // repeatingEffect can be used in multiple places. vibrator.vibrate(repeatingEffect) } fun stopVibrating() { vibrator.cancel() }
Esto es muy útil para eventos intermitentes que requieren la acción del usuario para confirmarlos. Algunos ejemplos de estos eventos son las llamadas telefónicas entrantes y las alarmas activadas.
Patrón con resguardo
Controlar la amplitud de una vibración es una función que depende del hardware. Si se reproduce una forma de onda en un dispositivo de gama baja sin esta capacidad, el dispositivo vibrará con la amplitud máxima para cada entrada positiva en el array de amplitud. Si tu app necesita adaptarse a esos dispositivos, usa un patrón que no genere un efecto de zumbido cuando se reproduzca en esa condición o, en su lugar, diseña un patrón de encendido y apagado más simple que se pueda reproducir como resguardo.
if (vibrator.hasAmplitudeControl()) { vibrator.vibrate( VibrationEffect.createWaveform( smoothTimings, amplitudes, smoothRepeatIdx ) ) } else { vibrator.vibrate( VibrationEffect.createWaveform( onOffTimings, onOffRepeatIdx ) ) }
Composiciones ancladas a la línea de tiempo con VibrationEffect.Builder
A partir de Android 16 (26Q4), VibrationEffect.Builder es la API preferida para crear efectos y composiciones de vibración complejos. Te permite crear sensaciones táctiles expresivas secuenciando elementos táctiles discretos en un cronograma absoluto con startTimeMillis.
VibrationEffect.Builder admite la combinación de varios tipos de vibración:
- Ajustes predeterminados: Son pulsos de respuesta táctil predefinidos (
VibrationEffect.Preset), como clics y tics. - Envolventes: Formas de onda continuas dinámicas, incluidas las envolventes básicas independientes del hardware (
BasicEnvelopeBuilder) y las envolventes avanzadas moduladas por frecuencia (WaveformEnvelopeBuilder). - VibrationEffects existentes: Composiciones primitivas (
VibrationEffect.Composition), formas de onda de pasos (VibrationEffect.createWaveform), efectos predefinidos (VibrationEffect.createPredefined) y de un solo uso (VibrationEffect.createOneShot) importados conaddEvents()o copia de constructor. - Secuencias repetitivas: Son patrones continuos configurados con
setRepeatingEffect().
Las vibraciones creadas con VibrationEffect.Builder tienen un resguardo automático integrado a nivel del framework en todos sus elementos (incluidos los ajustes predeterminados, las envolventes básicas y los efectos de vibración combinados), lo que garantiza una experiencia del usuario coherente en diferentes dispositivos sin necesidad de realizar verificaciones de capacidad manuales. (Las envolventes de forma de onda avanzadas creadas con WaveformEnvelopeBuilder requieren compatibilidad con hardware y no tienen compatibilidad con resguardo automático).
Redacta con ajustes predeterminados
Usa VibrationEffect.Preset para agregar pulsos táctiles cortos comunes y predefinidos (como PRESET_CLICK, PRESET_TICK o PRESET_LOW_TICK) a una composición.
El fabricante del dispositivo implementa los ajustes predeterminados para proporcionar una vibración nítida, breve y agradable que se alinee con los principios de la tecnología táctil para obtener una tecnología táctil clara. Para obtener más detalles sobre estas funciones y cómo funcionan, consulta la guía básica de actuadores de vibración.
Los ajustes predeterminados reemplazan las primitivas cortas de la API de VibrationEffect.Composition.
Para sensaciones táctiles más largas o continuas, como subir y bajar, usa formas de onda de envolvente (PWLE).
A cada ajuste predeterminado, se le puede asignar una escala opcional entre 0.0f y 1.0f, y se coloca en una hora de inicio explícita (en milisegundos) desde el comienzo de la composición.
Kotlin
val clickPreset = VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_CLICK, /* scale= */ 0.8f
)
val tickPreset = VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_TICK, /* scale= */ 0.5f
)
val effect = VibrationEffect.Builder()
.addPreset(/* startTimeMillis= */ 0L, clickPreset)
.addPreset(/* startTimeMillis= */ 100L, tickPreset)
.build()
vibrator.vibrate(effect)
Java
VibrationEffect.Preset clickPreset = VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_CLICK, /* scale= */ 0.8f
);
VibrationEffect.Preset tickPreset = VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_TICK, /* scale= */ 0.5f
);
VibrationEffect effect = new VibrationEffect.Builder()
.addPreset(/* startTimeMillis= */ 0L, clickPreset)
.addPreset(/* startTimeMillis= */ 100L, tickPreset)
.build();
vibrator.vibrate(effect);
Compón con envolventes y ajustes predeterminados
Puedes combinar sin problemas instancias de VibrationEffect.Envelope (creadas con BasicEnvelopeBuilder o WaveformEnvelopeBuilder) con ajustes predeterminados para crear patrones táctiles enriquecidos de varios segmentos.
Este es un ejemplo que reproduce una envolvente de aumento y disminución graduales seguida de un ajuste predeterminado de clic agudo:
Kotlin
val basicEnvelope = VibrationEffect.Envelope.create(
VibrationEffect.BasicEnvelopeBuilder()
.setInitialSharpness(0.0f)
.addControlPoint(1.0f, 1.0f, 300L)
.addControlPoint(0.0f, 0.5f, 100L)
)
val clickPreset = VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_CLICK, 0.9f
)
val mixedEffect = VibrationEffect.Builder()
.addEnvelope(/* startTimeMillis= */ 0L, basicEnvelope)
.addPreset(/* startTimeMillis= */ 450L, clickPreset)
.build()
vibrator.vibrate(mixedEffect)
Java
VibrationEffect.Envelope basicEnvelope = VibrationEffect.Envelope.create(
new VibrationEffect.BasicEnvelopeBuilder()
.setInitialSharpness(0.0f)
.addControlPoint(1.0f, 1.0f, 300L)
.addControlPoint(0.0f, 0.5f, 100L)
);
VibrationEffect.Preset clickPreset = VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_CLICK, 0.9f
);
VibrationEffect mixedEffect = new VibrationEffect.Builder()
.addEnvelope(/* startTimeMillis= */ 0L, basicEnvelope)
.addPreset(/* startTimeMillis= */ 450L, clickPreset)
.build();
vibrator.vibrate(mixedEffect);
Reutiliza y cambia los eventos existentes
Para reutilizar o concatenar un VibrationEffect existente (incluido VibrationEffect.Composition), recupera su lista de objetos VibrationEffect.Event con getEvents() y agrégalos con un desplazamiento usando addEvents(startTimeShiftMillis, events) (o pasa el efecto directamente al constructor VibrationEffect.Builder(effect)). Cuando importas instancias de VibrationEffect.Composition de esta manera, el framework convierte automáticamente sus primitivas en ajustes predeterminados, lo que habilita la compatibilidad con el resguardo del entorno de ejecución.
Kotlin
val existingEffect = VibrationEffect.Builder()
.addPreset(
0L,
VibrationEffect.Preset.create(VibrationEffect.Preset.PRESET_CLICK)
)
.addPreset(
80L,
VibrationEffect.Preset.create(VibrationEffect.Preset.PRESET_TICK)
)
.build()
// Shift and append the existing events 200ms into the new composition.
val combinedEffect = VibrationEffect.Builder()
.addEvents(/* startTimeShiftMillis= */ 200L, existingEffect.events)
.build()
vibrator.vibrate(combinedEffect)
Java
VibrationEffect existingEffect = new VibrationEffect.Builder()
.addPreset(
0L,
VibrationEffect.Preset.create(VibrationEffect.Preset.PRESET_CLICK)
)
.addPreset(
80L,
VibrationEffect.Preset.create(VibrationEffect.Preset.PRESET_TICK)
)
.build();
// Shift and append the existing events 200ms into the new composition.
VibrationEffect combinedEffect = new VibrationEffect.Builder()
.addEvents(/* startTimeShiftMillis= */ 200L, existingEffect.getEvents())
.build();
vibrator.vibrate(combinedEffect);
Crear composiciones repetitivas
Usa setRepeatingEffect(startTimeMillis, repeatingEffect, durationMillis) para agregar un patrón repetitivo a una composición:
Kotlin
val repeatingPattern = VibrationEffect.Builder()
.addPreset(
0L,
VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_CLICK, 1.0f
)
)
.addPreset(
150L,
VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_LOW_TICK, 0.6f
)
)
.build()
val repeatingEffect = VibrationEffect.Builder()
.setRepeatingEffect(
/* startTimeMillis= */ 0L,
/* effect= */ repeatingPattern,
/* durationMillis= */ 300L
)
.build()
vibrator.vibrate(repeatingEffect)
Java
VibrationEffect repeatingPattern = new VibrationEffect.Builder()
.addPreset(
0L,
VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_CLICK, 1.0f
)
)
.addPreset(
150L,
VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_LOW_TICK, 0.6f
)
)
.build();
VibrationEffect repeatingEffect = new VibrationEffect.Builder()
.setRepeatingEffect(
/* startTimeMillis= */ 0L,
/* effect= */ repeatingPattern,
/* durationMillis= */ 300L
)
.build();
vibrator.vibrate(repeatingEffect);
Administración de tiempos, validación y desvío
Cuando diseñes composiciones con VibrationEffect.Builder, ten en cuenta las siguientes reglas de sincronización y validación:
- Horas de inicio que aumentan de forma estricta: Cada elemento que se agrega al compilador debe tener un
startTimeMillisque sea estrictamente mayor o igual que la hora de inicio del elemento anterior. - Validación en el momento de la compilación: El compilador realiza la validación de mejor esfuerzo en el momento de la
build()con duraciones de elementos conocidas (o un mínimo de 1 ms para los ajustes predeterminados). Si se detecta una superposición imposible, se arroja unaIllegalArgumentException. - Cambio secuencial de la reproducción: Si un elemento de vibración anterior aún se está reproduciendo físicamente cuando llega la hora de inicio del siguiente elemento, el framework cambia automáticamente el siguiente elemento al horario disponible más temprano. Esto garantiza que los eventos no se superpongan y que no se pierdan vibraciones, aunque puede generar una leve desviación de tiempo si los eventos se programan con muy poca diferencia de tiempo. Para minimizar la desviación, deja suficiente tiempo (p.ej., 50 ms o más) entre eventos táctiles consecutivos.
Crea composiciones de primitivas de vibración
En esta sección, se explica cómo componer vibraciones con VibrationEffect.Composition. El fabricante del dispositivo implementa las primitivas de composición, que se describieron anteriormente en esta página. Proporcionan una vibración nítida, breve 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 la introducción a los actuadores de vibración.
A diferencia de VibrationEffect.Builder, la API de VibrationEffect.Composition
no tiene resguardos automáticos para las primitivas no compatibles. Por lo tanto, ten en cuenta lo siguiente:
Antes de activar la tecnología táctil avanzada, verifica que un dispositivo determinado admita todas las primitivas que usas.
Inhabilita el conjunto coherente de experiencias que no son compatibles, no solo los efectos a los que les falta una primitiva.
- Kotlin:
val fallbackEffect = VibrationEffect.Builder(compositionEffect).build()(o.addEvents(0L, compositionEffect.events)) - Java:
VibrationEffect fallbackEffect =new VibrationEffect.Builder(compositionEffect).build();(o.addEvents(0L, compositionEffect.getEvents()))
Cuando se construye con VibrationEffect.Builder, el framework convierte las primitivas de composición en ajustes predeterminados y proporciona automáticamente un resguardo de tiempo de ejecución si alguna primitiva no es compatible con el dispositivo del usuario.
Efectos de vibración compuestos
Puedes crear efectos de vibración compuestos con VibrationEffect.Composition. Este es un ejemplo de un efecto de aumento lento seguido de un efecto de clic agudo:
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, por lo que puedes controlar la amplitud de la vibración que genera 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 el usuario puede sentir (apenas) esta primitiva.
Cómo crear variantes en las primitivas de vibración
Si quieres crear una versión débil y una fuerte de la misma primitiva, crea proporciones de intensidad de 1.4 o más, de modo que la diferencia de intensidad se pueda percibir fácilmente. No intentes crear más de tres niveles de intensidad de la misma primitiva, ya que no se distinguen de forma perceptible. Por ejemplo, usa escalas de 0.5, 0.7 y 1.0 para crear versiones de intensidad baja, media y alta de una primitiva.
Agrega intervalos entre las primitivas de vibración
La composición también puede especificar retrasos que se agregarán entre primitivas consecutivas. 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 detectarse. Usa un intervalo de 50 ms o más si quieres crear un intervalo perceptible entre dos primitivas. Este es un ejemplo de una composición con retrasos:
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() )
Verifica qué primitivas son compatibles
Las siguientes APIs se pueden usar para verificar la compatibilidad del dispositivo con primitivas específicas:
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. }
También puedes verificar varias primitivas y, luego, decidir cuáles componer según el nivel de compatibilidad del dispositivo:
val primitives: IntArray = intArrayOf( VibrationEffect.Composition.PRIMITIVE_LOW_TICK, VibrationEffect.Composition.PRIMITIVE_TICK, VibrationEffect.Composition.PRIMITIVE_CLICK ) val supported: BooleanArray = vibrator.arePrimitivesSupported(*primitives)
Ejemplos de composiciones de vibración
En las siguientes secciones, se proporcionan varios ejemplos de composiciones de vibración, tomados de la app de ejemplo de respuesta táctil en GitHub.
Resistir (con marcas bajas)
Puedes controlar la amplitud de la vibración primitiva para transmitir comentarios útiles sobre una acción en curso. Los valores de escala con poco espacio entre sí se pueden usar para crear un efecto de crescendo suave de una primitiva. La demora entre las 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.
Figura 1: Esta forma de onda representa la aceleración de salida de la vibración en un dispositivo.
@Composable fun ResistScreen() { // Control variables for the dragging of the indicator. var isDragging by remember { mutableStateOf(false) } var dragOffset by remember { mutableFloatStateOf(0f) } // Only vibrates while the user is dragging LaunchedEffect(isDragging) { // Continuously run the effect for vibration to occur even when the view // is not being drawn, when user stops dragging midway through gesture. while (isDragging) { // 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() ) } } 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) } }
Expandir (con aumento y disminución)
Existen dos primitivas para aumentar la intensidad de vibración percibida: PRIMITIVE_QUICK_RISE y PRIMITIVE_SLOW_RISE. Ambas alcanzan
el mismo objetivo, pero con duraciones diferentes. Solo hay una primitiva para
reducir verticalmente, PRIMITIVE_QUICK_FALL. Estas primitivas funcionan mejor juntas para crear un segmento de forma de onda que aumenta de intensidad y, luego, se apaga. Puedes alinear las primitivas escaladas para evitar saltos repentinos de amplitud entre ellas, lo que también funciona bien para extender la duración general del efecto.
En términos de percepción, las personas siempre notan más la parte ascendente que la descendente, por lo que hacer que la parte ascendente sea más corta que la descendente puede usarse para cambiar el énfasis hacia la parte descendente.
Este es un ejemplo de una aplicación de esta composición para expandir y contraer un círculo. El efecto de elevación puede mejorar la sensación de expansión durante la animación. La combinación de los efectos de elevación y descenso ayuda a enfatizar el colapso al final de la animación.
Figura 2. Esta forma de onda representa la aceleración de salida de la vibración en un dispositivo.
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) Column( Modifier .clickable( onClick = { 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) } }
Tambaleo (con giros)
Uno de los principios de la tecnología táctil clave es deleitar a los usuarios. Una forma divertida de introducir un efecto de vibración inesperado y agradable es usar PRIMITIVE_SPIN. Esta primitiva es más eficaz cuando se llama más
de una vez. La concatenación de varios giros puede crear un efecto de balanceo e inestabilidad, que se puede mejorar aún más aplicando un escalamiento un tanto aleatorio en cada primitiva. También puedes experimentar con la brecha entre las primitivas de giro sucesivas. Dos giros sin ningún intervalo (0 ms entre ellos) crean una sensación de giro rápido. Aumentar el intervalo entre giros de 10 a 50 ms genera una sensación de giro más suelta y se puede usar para que coincida con la duración de un video o una animación.
No uses un intervalo de más de 100 ms, ya que los giros sucesivos ya no se integran bien y comienzan a sentirse como efectos individuales.
Este es un ejemplo de una forma elástica que rebota después de arrastrarla hacia abajo y, luego, soltarla. La animación se mejora con un par de efectos de giro, que se reproducen con intensidades variables que son proporcionales al desplazamiento del rebote.
Figura 3: Esta forma de onda representa la aceleración de salida de la vibración en un dispositivo.
@Composable fun WobbleScreen() { // Control variables for the dragging and animating state of the elastic. var dragDistance by remember { mutableFloatStateOf(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 ), // The spring ends on its target. An end value of 0f means that the // shape is back at rest, so the wobble is over. finishedListener = { endValue -> if (endValue == 0f) isWobbling = false }, ) LaunchedEffect(isWobbling) { while (isWobbling) { 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 = { // Wobble only if the shape is far enough from rest to vibrate. // It then springs back to 0f, and the finishedListener ends // the wobble. val displacement = dragDistanceAnimated / MAX_DRAG_DISTANCE isWobbling = displacement > SPIN_MIN_DISPLACEMENT dragDistance = 0f }, orientation = Orientation.Vertical, state = rememberDraggableState { delta -> isWobbling = false dragDistance += delta } ) ) { // Draw the wobbling shape using the animated spring-like value. The // lambda defers the read to the draw phase, so the animation doesn't // recompose this screen on every frame. 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) }
Rebote (con golpes sordos)
Otra aplicación avanzada de los efectos de vibración es simular interacciones físicas. PRIMITIVE_THUD puede crear un efecto fuerte y reverberante, que se puede vincular con la visualización de un impacto, por ejemplo, en un video o una animación, para mejorar la experiencia general.
A continuación, se muestra un ejemplo de una animación de caída de pelota mejorada con un efecto de sonido de golpe que se reproduce cada vez que la pelota rebota en la parte inferior de la pantalla:
Figura 4: Esta forma de onda representa la aceleración de salida de la vibración en un dispositivo.
enum class BallPosition { Start, End } @Composable fun BounceScreen() { // Control variable for the state of the ball. var ballPosition by remember { mutableStateOf(BallPosition.Start) } // Plain holder for the bounce count. It's read only in the effect and in // the click handler, never in composition, so it needs no snapshot state. val bounceCount = remember { intArrayOf(0) } // Animation for the bouncing ball. val transitionData = updateTransitionData(ballPosition) val collisionData = updateCollisionData(transitionData) // Ball is about to contact floor. The effect runs only when the collision // state changes, so it vibrates once per collision. Nothing here suspends, // so a keyed SideEffect does the job without launching a coroutine. SideEffect(collisionData.collisionWithFloor) { if (collisionData.collisionWithFloor) { val vibrationScale = 0.7.pow(bounceCount[0]++).toFloat() vibrator.vibrate( VibrationEffect.startComposition().addPrimitive( VibrationEffect.Composition.PRIMITIVE_THUD, vibrationScale ).compose() ) } } Box( Modifier .fillMaxSize() .clickable { if (transitionData.isAtStart) { ballPosition = BallPosition.End } else { ballPosition = BallPosition.Start bounceCount[0] = 0 } }, ) { // Build the ball UI based on the current state. BouncingBall(transitionData) } }
Forma de onda de vibración con envolventes
El proceso para crear patrones de vibración personalizados te permite controlar la amplitud de la vibración para crear efectos suaves de aumento y disminución. En esta sección, se explica cómo crear efectos táctiles dinámicos con envolventes de forma de onda que permiten un control preciso de la amplitud y la frecuencia de la vibración a lo largo del tiempo. Esto te permite crear experiencias de respuesta táctil más ricas y con más matices.
A partir de Android 16 (nivel de API 36), el sistema proporciona las siguientes APIs para crear una envolvente de forma de onda de vibración definiendo una secuencia de puntos de control:
BasicEnvelopeBuilder: Es un enfoque accesible para crear efectos táctiles independientes del hardware.WaveformEnvelopeBuilder: Es un enfoque más avanzado para crear efectos táctiles y requiere familiaridad con el hardware táctil.
Android no proporciona resguardos para los efectos de envolvente. Si necesitas esta asistencia, completa los siguientes pasos:
- Verifica que un dispositivo determinado admita efectos de envolvente con
Vibrator.areEnvelopeEffectsSupported(). - Inhabilita el conjunto coherente de experiencias que no se admiten o usa patrones de vibración personalizados o composiciones como alternativas de resguardo.
Para crear efectos de envolvente más básicos, usa BasicEnvelopeBuilder con estos parámetros:
- Un valor de intensidad en el rango \( [0, 1] \), que representa la fuerza percibida de la vibración Por ejemplo, un valor de \( 0.5 \) se percibe como la mitad de la intensidad máxima global que puede alcanzar el dispositivo.
Un valor de nitidez en el rango \( [0, 1] \), que representa la claridad de la vibración Los valores más bajos se traducen en vibraciones más suaves, mientras que los valores más altos crean una sensación más nítida.
Un valor de duración, que representa el tiempo, en milisegundos, que se tarda en pasar del último punto de control (es decir, un par de intensidad y nitidez) al nuevo.
Este es un ejemplo de forma de onda que aumenta la intensidad de una vibración de tono bajo a una de tono alto y fuerza máxima durante 500 ms y, luego, vuelve a bajar a\( 0 \) (desactivado) durante 100 ms.
vibrator.vibrate( VibrationEffect.BasicEnvelopeBuilder() .setInitialSharpness(0.0f) .addControlPoint(1.0f, 1.0f, 500) .addControlPoint(0.0f, 1.0f, 100) .build() )
Si tienes conocimientos más avanzados sobre la tecnología táctil, puedes definir efectos de envolvente con WaveformEnvelopeBuilder. Cuando usas este objeto, puedes acceder a la asignación de frecuencia a aceleración de salida (FOAM) a través de VibratorFrequencyProfile.
- Es un valor de amplitud en el rango \( [0, 1] \), que representa la intensidad de vibración que se puede alcanzar en una frecuencia determinada, según lo establecido por el FOAM del dispositivo. Por ejemplo, un valor de \( 0.5 \) genera la mitad de la aceleración de salida máxima que se puede alcanzar en la frecuencia determinada.
Un valor de frecuencia, especificado en hercios.
Un valor de duración, que representa el tiempo, en milisegundos, que se tarda en pasar del último punto de control al nuevo.
En el siguiente código, se muestra un ejemplo de forma de onda que define un efecto de vibración de 400 ms. Comienza con una rampa de amplitud de 50 ms, de apagado a completo, a una frecuencia constante de 60 Hz. Luego, la frecuencia aumenta a 120 Hz durante los siguientes 100 ms y permanece en ese nivel durante 200 ms. Por último, la amplitud disminuye a \( 0 \)y la frecuencia vuelve a 60 Hz durante los últimos 50 ms:
vibrator.vibrate( VibrationEffect.WaveformEnvelopeBuilder() .addControlPoint(1.0f, 60f, 50) .addControlPoint(1.0f, 120f, 100) .addControlPoint(1.0f, 120f, 200) .addControlPoint(0.0f, 60f, 50) .build() )
En las siguientes secciones, se proporcionan varios ejemplos de formas de onda de vibración con envolventes.
Resorte que rebota
En un ejemplo anterior, se usa PRIMITIVE_THUD para simular interacciones de rebote físico. La API de envolvente básica ofrece un control mucho más preciso, lo que te permite adaptar la intensidad y la nitidez de la vibración.
Esto genera una respuesta táctil que sigue con mayor precisión los eventos animados.
A continuación, se muestra un ejemplo de un resorte en caída libre con la animación mejorada con un efecto de envolvente básico que se reproduce cada vez que el resorte rebota en la parte inferior de la pantalla:
Figura 5: Gráfico de forma de onda de aceleración de salida para una vibración que simula un resorte que rebota.
@Composable fun BouncingSpringAnimation() { // The spring's height above the floor: 1f is the drop height, 0f is the floor. val springHeight = remember { Animatable(1f) } var isAnimating by remember { mutableStateOf(true) } LaunchedEffect(isAnimating) { if (!isAnimating) return@LaunchedEffect springHeight.snapTo(1f) // Each bounce reaches a fraction of the previous height. var bounceHeight = 1f while (bounceHeight > MIN_BOUNCE_HEIGHT) { // Fall to the floor, speeding up like a falling object. A fall from // a greater height takes longer. val fallDurationMs = (DROP_DURATION_MS * sqrt(bounceHeight)).toInt() springHeight.animateTo(0f, tween(fallDurationMs, easing = EaseInQuad)) // The spring hits the floor: play a "boing" envelope effect that fades // out. A higher bounce gives a stronger and sharper vibration. vibrator.vibrate( VibrationEffect.BasicEnvelopeBuilder() // Starting from zero sharpness here, will simulate a smoother // "boing" effect. .setInitialSharpness(0f) // Add a control point to reach the target intensity and // sharpness very quickly. .addControlPoint(bounceHeight, bounceHeight * MAX_SHARPNESS, 20L) // Add a control point to fade out the vibration intensity while // maintaining sharpness. .addControlPoint(0f, bounceHeight * MAX_SHARPNESS, fallDurationMs.toLong()) .build() ) // Bounce back up to a lower height, slowing down like a thrown object. // When the next bounce is too small, the spring stays on the floor. bounceHeight *= BOUNCE_DAMPING if (bounceHeight > MIN_BOUNCE_HEIGHT) { val riseDurationMs = (DROP_DURATION_MS * sqrt(bounceHeight)).toInt() springHeight.animateTo(bounceHeight, tween(riseDurationMs, easing = EaseOutQuad)) } } isAnimating = false } Box( modifier = Modifier .fillMaxSize() .clickable(enabled = !isAnimating) { isAnimating = true } .drawBehind { // Read the height in the draw phase, and scale it to the distance // between the top of the box and the floor. val floorTop = size.height - FLOOR_HEIGHT.toPx() val dropDistance = floorTop - SPRING_HEIGHT.toPx() drawSpring(topY = dropDistance * (1f - springHeight.value)) drawFloor(top = floorTop) } ) { if (!isAnimating) { Text("Tap to restart", Modifier.align(Alignment.Center)) } } }
Lanzamiento de cohete
En un ejemplo anterior, se muestra cómo usar la API de envolvente básica para simular una reacción de resorte con rebote. La WaveformEnvelopeBuilder permite un control preciso de todo el rango de frecuencia del dispositivo, lo que habilita efectos de respuesta táctil altamente personalizados. Si combinas esto con los datos de FOAM, puedes adaptar
las vibraciones a capacidades de frecuencia específicas.
Este es un ejemplo que muestra una simulación del lanzamiento de un cohete con un patrón de vibración dinámico. El efecto va desde la salida de aceleración de frecuencia mínima admitida, 0.1 G, hasta la frecuencia de resonancia, siempre manteniendo una entrada de amplitud del 10%. Esto permite que el efecto comience con una salida razonablemente fuerte y que aumente la intensidad y la nitidez percibidas, incluso aunque la amplitud de la unidad sea la misma. Cuando se alcanza la resonancia, la frecuencia del efecto vuelve a descender al mínimo, lo que se percibe como una disminución de la intensidad y la nitidez. Esto crea una sensación de resistencia inicial seguida de una liberación, lo que imita un lanzamiento al espacio.
Este efecto no es posible con la API de envolvente básica, ya que abstrae la información específica del dispositivo sobre su frecuencia de resonancia y su curva de aceleración de salida. Aumentar la nitidez puede llevar la frecuencia equivalente más allá de la resonancia, lo que podría causar una caída de aceleración no deseada.
Figura 6: Un gráfico de forma de onda de aceleración de salida para una vibración que simula el lanzamiento de un cohete.
@Composable fun RocketLaunchAnimation() { var isLaunched by remember { mutableStateOf(false) } val animation = remember { Animatable(0f) } val animationDuration = 3000 LaunchedEffect(isLaunched) { if (isLaunched) { animation.animateTo( 1.2f, // Overshoot so that the rocket goes off the screen. animationSpec = tween( durationMillis = animationDuration, // Applies an easing curve with a slow start and rapid acceleration // towards the end. easing = CubicBezierEasing(1f, 0f, 0.75f, 1f) ) ) animation.snapTo(0f) isLaunched = false } } Canvas( modifier = Modifier .fillMaxSize() .clickable { if (!isLaunched) { // Play vibration with same duration as the animation, using 70% of // the time for the rise of the vibration, to match the easing curve // defined previously. playVibration(vibrator, animationDuration.toLong(), 0.7f) isLaunched = true } } .background(colorResource(R.color.background)) ) { // Read the animation in the draw phase, and scale it to the canvas height. drawRocket(size.height * animation.value) } } private fun playVibration( vibrator: Vibrator, totalDurationMs: Long, riseBias: Float, minOutputAccelerationGs: Float = 0.1f, ) { require(riseBias in 0f..1f) { "Rise bias must be between 0 and 1." } if (!vibrator.areEnvelopeEffectsSupported()) { return } val resonantFrequency = vibrator.resonantFrequency if (resonantFrequency.isNaN()) { // Device doesn't have or expose a resonant frequency. return } val startFrequency = vibrator.frequencyProfile?.getFrequencyRange(minOutputAccelerationGs)?.lower ?: return if (startFrequency >= resonantFrequency) { // Vibrator can't generate the minimum required output at lower frequencies. return } val minDurationMs = vibrator.envelopeEffectInfo.minControlPointDurationMillis val rampUpDurationMs = (riseBias * totalDurationMs).toLong() - minDurationMs val rampDownDurationMs = totalDurationMs - rampUpDurationMs - minDurationMs vibrator.vibrate( VibrationEffect.WaveformEnvelopeBuilder() // Quickly reach the target output at the start frequency .addControlPoint(0.1f, startFrequency, minDurationMs) .addControlPoint(0.1f, resonantFrequency, rampUpDurationMs) .addControlPoint(0.1f, startFrequency, rampDownDurationMs) // Controlled ramp down to zero to avoid ringing after the vibration. .addControlPoint(0.0f, startFrequency, minDurationMs) .build() ) }
LavaBeats
Al igual que en el ejemplo de Rocket Launch, la API de WaveformEnvelopeBuilder permite diseñar muchos efectos táctiles complejos, ya que permite controlar los segmentos de amplitud y frecuencia en una vibración. Otro ejemplo de este tipo de diseño es la emulación de sensaciones físicas más abstractas, como la "vivacidad".
Esto se puede lograr representando los biomarcadores de una señal de electrocardiograma (ECG) típica con segmentos de vibración de ciertas amplitudes y frecuencias. LavaBeats es un ejemplo en el que dos segmentos característicos de un registro de ECG se representan como dos pulsos separados por un retraso de tiempo. El
primer pulso característico es el complejo QRS, que se muestra como un pico agudo de
amplitud alta y duración corta. El segundo pulso es la onda T, que tiene una amplitud más baja, una duración más larga y una forma más suave (consulta la Figura 7).
Usa el WaveformEnvelopeBuilder para construir varias repeticiones de estos dos pulsos separados por un retraso fijo entre el primer y el segundo pulso. El primer pulso puede ser una señal de chirp que comienza en una frecuencia baja y termina en una frecuencia más alta durante un período breve. El segundo pulso se puede representar como un solo
período de una sinusoide de baja frecuencia. Podemos componer los dos pulsos en un latido
y repetir la composición varias veces con un retraso intermedio, siguiendo una
frecuencia típica de latidos por minuto (ppm). El resultado es un efecto de respuesta táctil que se asemeja a un corazón que late.
Puedes probar LavaBeats en nuestra app de ejemplo de respuesta táctil en GitHub y sentir el efecto acompañado de una visualización de lámpara de lava que late al mismo ritmo que el efecto táctil. También puedes cambiar la configuración del efecto para crear diferentes sensaciones de latido modificando las amplitudes, las frecuencias, las duraciones y los retrasos de los dos pulsos.
Figura 7: Un segmento de un registro de ECG con el complejo QRS y la onda T
private fun createEnvelopeEffect( beatParameters: List<BeatParameter> ): VibrationEffect = VibrationEffect.WaveformEnvelopeBuilder() .apply { repeat(beatParameters.getNumBeats()) { // First pulse chirp addControlPoint( beatParameters.getFirstPulseAmplitude(), beatParameters.getFirstPulseStartFreq(), ENVELOPE_RAMP_DURATION_MILLIS, ) addControlPoint( beatParameters.getFirstPulseAmplitude(), beatParameters.getFirstPulseEndFreq(), beatParameters.getFirstPulseDurationMillis().toLong(), ) addControlPoint( 0f, beatParameters.getFirstPulseEndFreq(), ENVELOPE_RAMP_DURATION_MILLIS, ) // Delay between first and second pulse addControlPoint( 0f, beatParameters.getFirstPulseEndFreq(), beatParameters.getFirstToSecondPulseDelayMillis().toLong(), ) // Second pulse addControlPoint( beatParameters.getSecondPulseAmplitude(), beatParameters.getSecondPulseFreq(), ENVELOPE_RAMP_DURATION_MILLIS, ) addControlPoint( beatParameters.getSecondPulseAmplitude(), beatParameters.getSecondPulseFreq(), (1_000 / (2f * beatParameters.getSecondPulseFreq())).toLong(), ) addControlPoint( 0f, beatParameters.getSecondPulseFreq(), ENVELOPE_RAMP_DURATION_MILLIS, ) addControlPoint( 0f, beatParameters.getSecondPulseFreq(), beatParameters.getBeatDelayMillis().toLong(), ) } } .build() /** A parameter of a haptic beat effect that represents an ECG signal parameter */ data class BeatParameter( val description: String = "", val value: Float = 0f, val range: ClosedFloatingPointRange<Float> = 0f..1f, val steps: Int = 0, val isFrequencyType: Boolean = false, )