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 ancladas en la línea de tiempo con
VibrationEffect.Builder- Compose with presets: Secuencia sensaciones hápticas predefinidas.
- Composición con envolventes y ajustes predeterminados: Combina envolventes y ajustes predeterminados a lo largo de una línea de tiempo.
- Reutilizar y desplazar 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 elementos primitivos de vibración
- Resistencia: Un efecto de arrastre con intensidad dinámica.
- Expandir: Es un efecto de aumento y luego caída.
- Wobble: Un efecto de bamboleo que usa la primitiva
SPIN. - Rebote: Un efecto de rebote que usa la primitiva
THUD.
- Forma de onda de vibración con envolventes
- Resorte que rebota: Un efecto de resorte que rebota con efectos de envolvente básicos.
- Lanzamiento de cohete: Un efecto de lanzamiento de cohete que usa efectos de envolvente de forma de onda.
Para obtener 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 alternativas para controlar la compatibilidad de los dispositivos
Cuando implementes efectos hápticos personalizados, la compatibilidad del dispositivo y el comportamiento de resguardo dependerán de la plataforma de API que elijas:
VibrationEffect.Builder(recomendado): A partir de Android 16 (4º trimestre de 2026), los efectos creados conVibrationEffect.Builderincluyen resguardo automático a nivel del framework. Si un dispositivo no admite de forma nativa unPreseto unEnvelopebásico solicitados, el framework los traduce automáticamente a una alternativa adecuada en el momento de la reproducción con el mejor esfuerzo. No es necesario que verifiques manualmente las capacidades de cada dispositivo antes de reproducir los efectos compuestos conVibrationEffect.Builder.- Excepción: Los sobres de forma de onda avanzados creados con
WaveformEnvelopeBuilderno admiten la reversión automática porque dependen de asignaciones de frecuencia de hardware específicas (FOAM). Si no son compatibles, no se reproducirán.
- Excepción: Los sobres de forma de onda avanzados creados con
VibrationEffect.Composition: Las composiciones creadas con la API destartComposition()no tienen alternativas automáticas. Si una composición contiene al menos un elemento primitivo no admitido, 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 distintas de cero se redondean al 100% en los dispositivos que no tienen control de amplitud. Verifica
vibrator.hasAmplitudeControl()y recurre a un patrón de ENCENDIDO/APAGADO diseñado de forma explícita si es necesario.
Uso de primitivas hápticas
Android incluye varias primitivas de tecnología táctil que varían en amplitud y frecuencia. Puedes usar una sola primitiva o varias en combinación para lograr efectos hápticos enriquecidos.
- Usa retrasos de 50 ms o más para que haya brechas perceptibles entre dos primitivas, y también ten en cuenta 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 en la intensidad.
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.
Cómo crear patrones de vibración personalizados
Los patrones de vibración se suelen usar en la háptica atencional, 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 llaman 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 demasiado rápido también puede producir zumbidos audibles. Diseña patrones de forma de onda para suavizar las transiciones de amplitud y crear efectos de aumento y disminución gradual.
Ejemplos de patrones de vibración
En las siguientes secciones, se proporcionan varios ejemplos de patrones de vibración:
Patrón de aumento
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: La amplitud de vibración deseada para cada duración especificada en el primer argumento, representada por un valor entero de 0 a 255, en el que 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 se 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 entre los parpadeos. El primer pulso es un aumento gradual y uniforme hasta la amplitud máxima, y el segundo es un aumento rápido para mantener la amplitud máxima. El í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 // Don't repeat.
vibrator.vibrate(VibrationEffect.createWaveform(
timings, amplitudes, repeatIndex))
Java
long[] timings = new long[] {
50, 50, 50, 50, 50, 100, 350, 25, 25, 25, 25, 200 };
int[] amplitudes = new int[] {
33, 51, 75, 113, 170, 255, 0, 38, 62, 100, 160, 255 };
int repeatIndex = -1; // Don't repeat.
vibrator.vibrate(VibrationEffect.createWaveform(
timings, amplitudes, repeatIndex));
Patrón repetitivo
Las formas de onda también se pueden reproducir de forma repetida hasta que se cancelen. Para crear una forma de onda repetitiva, debes establecer un parámetro repeat no negativo. Cuando reproduces una forma de onda repetitiva, 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 son las llamadas telefónicas entrantes y las alarmas activadas.
Patrón con resguardo
Controlar la amplitud de una vibración es una capacidad dependiente del hardware. Reproducir una forma de onda en un dispositivo de gama baja sin esta capacidad hace que el dispositivo vibre con la amplitud máxima para cada entrada positiva en el array de amplitud. Si tu app necesita adaptarse a estos dispositivos, usa un patrón que no genere un efecto de zumbido cuando se reproduzca en esa condición o diseña un patrón de ENCENDIDO/APAGADO más simple que se pueda reproducir como alternativa.
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));
}
Composiciones ancladas en 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 hápticas expresivas secuenciando elementos hápticos discretos a lo largo de una línea de tiempo absoluta con startTimeMillis.
VibrationEffect.Builder admite la combinación de varios tipos de vibración:
- Ajustes predeterminados: Son pulsos hápticos predefinidos (
VibrationEffect.Preset), como clics y marcas. - 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 efectos únicos (VibrationEffect.createOneShot) importados conaddEvents()o con la copia del constructor. - Secuencias repetitivas: Patrones continuos configurados con
setRepeatingEffect().
Las vibraciones creadas con VibrationEffect.Builder tienen una reserva automática integrada a nivel del framework en todos sus elementos (incluidos los ajustes predeterminados, los sobres básicos y los efectos de vibración combinados), lo que garantiza una experiencia del usuario coherente en diferentes dispositivos sin necesidad de realizar verificaciones manuales de capacidades. (Los envolventes de forma de onda avanzados creados con WaveformEnvelopeBuilder requieren compatibilidad con hardware y no tienen compatibilidad automática con resguardo).
Cómo componer con ajustes predeterminados
Usa VibrationEffect.Preset para agregar pulsos hápticos 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, corta y agradable que se alinea con los principios de háptica para una háptica clara. Para obtener más detalles sobre estas capacidades y cómo funcionan, consulta Introducción a los actuadores de vibración.
Los ajustes predeterminados reemplazan los elementos primitivos cortos de la API de VibrationEffect.Composition.
Para sensaciones hápticas más largas o continuas, como las que aumentan y disminuyen gradualmente, usa formas de onda de envolvente (PWLE) en su lugar.
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 inicio 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);
Composició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 hápticos enriquecidos de varios segmentos.
Este es un ejemplo que reproduce una envolvente de aumento y disminución graduales suaves, seguida de un ajuste predeterminado de clic nítido:
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 desplaza 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 se importan instancias de VibrationEffect.Composition de esta manera, el framework convierte automáticamente sus elementos primitivos en ajustes predeterminados, lo que habilita la compatibilidad con la reversión en el tiempo 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);
Crea 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 del tiempo, la validación y la desviación
Cuando diseñes composiciones con VibrationEffect.Builder, ten en cuenta las siguientes reglas de validación y sincronización:
- Horas de inicio estrictamente crecientes: 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 tiempo de compilación: El compilador realiza la mejor validación posible en el tiempo de
build()con duraciones de elementos conocidos (o un mínimo de 1 ms para los ajustes predeterminados). Si se detecta una superposición imposible, se arroja unIllegalArgumentException. - Desplazamiento secuencial de la reproducción: Si un elemento de vibración anterior sigue reproduciéndose físicamente cuando llega la hora de inicio del siguiente elemento, el framework desplaza automáticamente el siguiente elemento al intervalo de tiempo disponible más temprano. Esto garantiza que los eventos no se superpongan y que no se pierdan vibraciones, aunque puede introducir una ligera desviación del tiempo si los eventos se programan demasiado cerca unos de otros. Para minimizar la desviación, deja suficiente tiempo (p.ej., 50 ms o más) entre eventos hápticos consecutivos.
Crea composiciones de primitivas de vibración
En esta sección, se muestra 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, corta y agradable que se alinea con los principios de la tecnología háptica para obtener una respuesta háptica clara. Para obtener más detalles sobre estas capacidades y cómo funcionan, consulta Introducción a los actuadores de vibración.
A diferencia de VibrationEffect.Builder, la API de VibrationEffect.Composition no tiene alternativas automáticas para los tipos primitivos no admitidos. Por lo tanto, ten en cuenta lo siguiente:
Antes de activar la respuesta háptica avanzada, verifica que un dispositivo determinado admita todos los elementos primitivos 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 los elementos básicos de composición en ajustes predeterminados y proporciona automáticamente una alternativa de tiempo de ejecución si no se admite ningún elemento básico en el dispositivo del usuario.
Efectos de vibración compuestos
Puedes crear efectos de vibración compuestos con VibrationEffect.Composition. A continuación, se muestra un ejemplo de un efecto de aumento lento seguido de un efecto de clic brusco:
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());
Una composición se crea agregando primitivas para que se reproduzcan 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, en el que 0 se asigna a una amplitud mínima en la que el usuario puede sentir (apenas) esta primitiva.
Crea variantes en elementos primitivos de vibración
Si deseas crear una versión débil y una fuerte del mismo elemento primitivo, crea relaciones de intensidad de 1.4 o más, de modo que la diferencia en la 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 perceptualmente. 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 espacios entre los elementos básicos de vibración
La composición también puede especificar retrasos que se agregarán entre las primitivas consecutivas. Este retraso se expresa en milisegundos desde el final de la primitiva anterior. En general, una brecha de 5 a 10 ms entre dos primitivas es demasiado corta para ser detectable. Usa una brecha del orden de 50 ms o más si deseas crear una brecha discernible 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());
Verifica qué elementos primitivos son compatibles
Se pueden usar las siguientes APIs para verificar la compatibilidad del dispositivo con elementos básicos específicos:
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 varios elementos básicos 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);
Ejemplos de composiciones de vibración
En las siguientes secciones, se proporcionan varios ejemplos de composiciones de vibración, extraídos de la app de ejemplo de tecnología háptica en GitHub.
Resistencia (con pocas marcas)
Puedes controlar la amplitud de la vibración primitiva para transmitir comentarios útiles sobre una acción en curso. Los valores de escala muy cercanos se pueden usar para crear un efecto de crescendo suave de una primitiva. La demora 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.
Figura 1: Esta forma de onda representa la aceleración de salida de la vibración en un dispositivo.
Kotlin
@Composable
fun ResistScreen() {
// Control variables for the dragging of the indicator.
var isDragging by remember { mutableStateOf(false) }
var dragOffset by remember { mutableStateOf(0f) }
// Only vibrates while the user is dragging
if (isDragging) {
LaunchedEffect(Unit) {
// Continuously run the effect for vibration to occur even when the view
// is not being drawn, when user stops dragging midway through gesture.
while (true) {
// Calculate the interval inversely proportional to the drag offset.
val vibrationInterval = calculateVibrationInterval(dragOffset)
// Calculate the scale directly proportional to the drag offset.
val vibrationScale = calculateVibrationScale(dragOffset)
delay(vibrationInterval)
vibrator.vibrate(
VibrationEffect.startComposition().addPrimitive(
VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
vibrationScale
).compose()
)
}
}
}
Screen() {
Column(
Modifier
.draggable(
orientation = Orientation.Vertical,
onDragStarted = {
isDragging = true
},
onDragStopped = {
isDragging = false
},
state = rememberDraggableState { delta ->
dragOffset += delta
}
)
) {
// Build the indicator UI based on how much the user has dragged it.
ResistIndicator(dragOffset)
}
}
}
Java
class DragListener implements View.OnTouchListener {
// Control variables for the dragging of the indicator.
private int startY;
private int vibrationInterval;
private float vibrationScale;
@Override
public boolean onTouch(View view, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startY = event.getRawY();
vibrationInterval = calculateVibrationInterval(0);
vibrationScale = calculateVibrationScale(0);
startVibration();
break;
case MotionEvent.ACTION_MOVE:
float dragOffset = event.getRawY() - startY;
// Calculate the interval inversely proportional to the drag offset.
vibrationInterval = calculateVibrationInterval(dragOffset);
// Calculate the scale directly proportional to the drag offset.
vibrationScale = calculateVibrationScale(dragOffset);
// Build the indicator UI based on how much the user has dragged it.
updateIndicator(dragOffset);
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
// Only vibrates while the user is dragging
cancelVibration();
break;
}
return true;
}
private void startVibration() {
vibrator.vibrate(
VibrationEffect.startComposition()
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
vibrationScale)
.compose());
// Continuously run the effect for vibration to occur even when the view
// is not being drawn, when user stops dragging midway through gesture.
handler.postDelayed(this::startVibration, vibrationInterval);
}
private void cancelVibration() {
handler.removeCallbacksAndMessages(null);
}
}
Expandir (con aumento y disminución)
Hay dos primitivas para aumentar la intensidad de vibración percibida: PRIMITIVE_QUICK_RISE y PRIMITIVE_SLOW_RISE. Ambos alcanzan el mismo objetivo, pero con duraciones diferentes. Solo hay un elemento primitivo para la reducción gradual, PRIMITIVE_QUICK_FALL. Estas primitivas funcionan mejor juntas para crear un segmento de forma de onda que aumenta en intensidad y luego se desvanece. Puedes alinear las primitivas escaladas para evitar saltos repentinos en la amplitud entre ellas, lo que también funciona bien para extender la duración del efecto general.
Desde el punto de vista de la 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 desplazar el énfasis hacia la parte descendente.
Este es un ejemplo de la 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 efectos de aumento y disminución 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.
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;
}
}
Oscilación (con giros)
Uno de los principales principios de la tecnología háptica es deleitar a los usuarios. Una forma divertida de introducir un efecto de vibración agradable e inesperado es usar PRIMITIVE_SPIN. Este elemento primitivo es más eficaz cuando se llama más de una vez. La concatenación de varios giros puede crear un efecto de bamboleo e inestabilidad, que se puede mejorar aún más aplicando un ajuste de escala algo aleatorio en cada primitiva. También puedes experimentar con el intervalo entre las primitivas de giro sucesivas. Dos giros sin ninguna brecha (0 ms entre ellos) crean una sensación de giro ajustada. 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 espacio mayor a 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 arrastrarse hacia abajo y, luego, soltarse. La animación se mejora con un par de efectos de rotación 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.
Kotlin
@Composable
fun WobbleScreen() {
// Control variables for the dragging and animating state of the elastic.
var dragDistance by remember { mutableStateOf(0f) }
var isWobbling by remember { mutableStateOf(false) }
// Use drag distance to create an animated float value behaving like a spring.
val dragDistanceAnimated by animateFloatAsState(
targetValue = if (dragDistance > 0f) dragDistance else 0f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioHighBouncy,
stiffness = Spring.StiffnessMedium
),
)
if (isWobbling) {
LaunchedEffect(Unit) {
while (true) {
val displacement = dragDistanceAnimated / MAX_DRAG_DISTANCE
// Use some sort of minimum displacement so the final few frames
// of animation don't generate a vibration.
if (displacement > SPIN_MIN_DISPLACEMENT) {
vibrator.vibrate(
VibrationEffect.startComposition().addPrimitive(
VibrationEffect.Composition.PRIMITIVE_SPIN,
nextSpinScale(displacement)
).addPrimitive(
VibrationEffect.Composition.PRIMITIVE_SPIN,
nextSpinScale(displacement)
).compose()
)
}
// Delay the next check for a sufficient duration until the
// current composition finishes. Note that you can use
// Vibrator.getPrimitiveDurations API to calculcate the delay.
delay(VIBRATION_DURATION)
}
}
}
Box(
Modifier
.fillMaxSize()
.draggable(
onDragStopped = {
isWobbling = true
dragDistance = 0f
},
orientation = Orientation.Vertical,
state = rememberDraggableState { delta ->
isWobbling = false
dragDistance += delta
}
)
) {
// Draw the wobbling shape using the animated spring-like value.
WobbleShape(dragDistanceAnimated)
}
}
// Calculate a random scale for each spin to vary the full effect.
fun nextSpinScale(displacement: Float): Float {
// Generate a random offset in the range [-0.1, +0.1] to be added to the
// vibration scale so the spin effects have slightly different values.
val randomOffset: Float = Random.Default.nextFloat() * 0.2f - 0.1f
return (displacement + randomOffset).absoluteValue.coerceIn(0f, 1f)
}
Java
class AnimationListener implements DynamicAnimation.OnAnimationUpdateListener {
private final Random vibrationRandom = new Random(seed);
private final long lastVibrationUptime;
@Override
public void onAnimationUpdate(
DynamicAnimation animation, float value, float velocity) {
// Delay the next check for a sufficient duration until the current
// composition finishes. Note that you can use
// Vibrator.getPrimitiveDurations API to calculcate the delay.
if (SystemClock.uptimeMillis() - lastVibrationUptime < VIBRATION_DURATION) {
return;
}
float displacement = calculateRelativeDisplacement(value);
// Use some sort of minimum displacement so the final few frames
// of animation don't generate a vibration.
if (displacement < SPIN_MIN_DISPLACEMENT) {
return;
}
lastVibrationUptime = SystemClock.uptimeMillis();
vibrator.vibrate(
VibrationEffect.startComposition()
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_SPIN,
nextSpinScale(displacement))
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_SPIN,
nextSpinScale(displacement))
.compose());
}
// Calculate a random scale for each spin to vary the full effect.
float nextSpinScale(float displacement) {
// Generate a random offset in the range [-0.1,+0.1] to be added to
// the vibration scale so the spin effects have slightly different
// values.
float randomOffset = vibrationRandom.nextFloat() * 0.2f - 0.1f
return MathUtils.clamp(displacement + randomOffset, 0f, 1f)
}
}
Rebote (con golpes)
Otra aplicación avanzada de los efectos de vibración es la simulación de interacciones físicas. PRIMITIVE_THUD puede crear un efecto fuerte y reverberante, que se puede combinar 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 una pelota que cae, mejorada con un efecto 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.
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;
}
}
});
}
}
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 hápticos dinámicos con envolventes de forma de onda que permiten controlar con precisión la amplitud y la frecuencia de la vibración a lo largo del tiempo. Esto te permite crear experiencias hápticas más ricas y matizadas.
A partir de Android 16 (nivel de API 36), el sistema proporciona las siguientes APIs para crear un envolvente de forma de onda de vibración definiendo una secuencia de puntos de control:
BasicEnvelopeBuilder: Un enfoque accesible para crear efectos hápticos independientes del hardware.WaveformEnvelopeBuilder: Es un enfoque más avanzado para crear efectos táctiles que requiere familiaridad con el hardware de tecnología háptica.
Android no proporciona alternativas para los efectos de envolvente. Si necesitas esta asistencia, completa los siguientes pasos:
- Verifica si un dispositivo determinado admite efectos de envolvente con
Vibrator.areEnvelopeEffectsSupported(). - Inhabilita el conjunto coherente de experiencias que no son compatibles 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:
- Es un valor de intensidad en el rango \( [0, 1] \), que representa la intensidad 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 nitidez 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.
Es un valor de duración que representa el tiempo, en milisegundos, que se tarda en realizar la transición desde el último punto de control (es decir, un par de intensidad y nitidez) hasta el 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 máxima intensidad en 500 ms, y luego vuelve a disminuir a\( 0 \) (apagado) en 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 háptica, puedes definir efectos de envolvente con WaveformEnvelopeBuilder. Cuando usas este objeto, puedes acceder a la asignación de aceleración de frecuencia a salida (FOAM) a través de VibratorFrequencyProfile.
- Es un valor de amplitud en el rango \( [0, 1] \), que representa la intensidad de vibración alcanzable en una frecuencia determinada, según lo determina 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 lograr con la frecuencia determinada.
Es un valor de frecuencia, especificado en hercios.
Un valor de duración, que representa el tiempo, en milisegundos, que se tarda en realizar la transición desde el último punto de control hasta el 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, desde apagado hasta 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 una muestra anterior, se usó PRIMITIVE_THUD para simular interacciones de rebote físico. La API de sobre básico ofrece un control mucho más preciso, lo que te permite personalizar con exactitud la intensidad y la nitidez de la vibración.
Esto genera una respuesta háptica 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() {
var springX by remember { mutableStateOf(SPRING_WIDTH) }
var springY by remember { mutableStateOf(SPRING_HEIGHT) }
var velocityX by remember { mutableFloatStateOf(INITIAL_VELOCITY) }
var velocityY by remember { mutableFloatStateOf(INITIAL_VELOCITY) }
var sharpness by remember { mutableFloatStateOf(INITIAL_SHARPNESS) }
var intensity by remember { mutableFloatStateOf(INITIAL_INTENSITY) }
var multiplier by remember { mutableFloatStateOf(INITIAL_MULTIPLIER) }
var bottomBounceCount by remember { mutableIntStateOf(0) }
var animationStartTime by remember { mutableLongStateOf(0L) }
var isAnimating by remember { mutableStateOf(false) }
val (screenHeight, screenWidth) = getScreenDimensions(context)
LaunchedEffect(isAnimating) {
animationStartTime = System.currentTimeMillis()
isAnimating = true
while (isAnimating) {
velocityY += GRAVITY
springX += velocityX.dp
springY += velocityY.dp
// Handle bottom collision
if (springY > screenHeight - FLOOR_HEIGHT - SPRING_HEIGHT / 2) {
// Set the spring's y-position to the bottom bounce point, to keep it
// above the floor.
springY = screenHeight - FLOOR_HEIGHT - SPRING_HEIGHT / 2
// Reverse the vertical velocity and apply damping to simulate a bounce.
velocityY *= -BOUNCE_DAMPING
bottomBounceCount++
// Calculate the fade-out duration of the vibration based on the
// vertical velocity.
val fadeOutDuration =
((abs(velocityY) / GRAVITY) * FRAME_DELAY_MS).toLong()
// Create a "boing" envelope vibration effect that fades out.
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(intensity, sharpness, 20L)
// Add a control point to fade out the vibration intensity while
// maintaining sharpness.
.addControlPoint(0f, sharpness, fadeOutDuration)
.build()
)
// Decrease the intensity and sharpness of the vibration for subsequent
// bounces, and reduce the multiplier to create a fading effect.
intensity *= multiplier
sharpness *= multiplier
multiplier -= 0.1f
}
if (springX > screenWidth - SPRING_WIDTH / 2) {
// Prevent the spring from moving beyond the right edge of the screen.
springX = screenWidth - SPRING_WIDTH / 2
}
// Check for 3 bottom bounces and then slow down.
if (bottomBounceCount >= MAX_BOTTOM_BOUNCE &&
System.currentTimeMillis() - animationStartTime > 1000) {
velocityX *= 0.9f
velocityY *= 0.9f
}
delay(FRAME_DELAY_MS) // Control animation speed.
// Determine if the animation should continue based on the spring's
// position and velocity.
isAnimating = (springY < screenHeight + SPRING_HEIGHT ||
springX < screenWidth + SPRING_WIDTH)
&& (velocityX >= 0.1f || velocityY >= 0.1f)
}
}
Box(
modifier = Modifier
.fillMaxSize()
.noRippleClickable {
if (!isAnimating) {
resetAnimation()
}
}
.width(screenWidth)
.height(screenHeight)
) {
DrawSpring(mutableStateOf(springX), mutableStateOf(springY))
DrawFloor()
if (!isAnimating) {
DrawText("Tap to restart")
}
}
}
Lanzamiento de cohete
En un ejemplo anterior, se muestra cómo usar la API de sobre básico para simular una reacción de resorte elástica. El WaveformEnvelopeBuilder desbloquea el control preciso sobre el rango de frecuencia completo del dispositivo, lo que permite efectos hápticos 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 de lanzamiento de 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, y siempre mantiene una entrada de amplitud del 10%. Esto permite que el efecto comience con una salida razonablemente fuerte y aumente la intensidad y la nitidez percibidas, aunque la amplitud de conducción sea la misma. Cuando se alcanza la resonancia, la frecuencia del efecto desciende hasta el 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, 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 la 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 no deseada en la aceleración.
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() {
val context = LocalContext.current
val screenHeight = remember { mutableFloatStateOf(0f) }
var rocketPositionY by remember { mutableFloatStateOf(0f) }
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)
)
) {
rocketPositionY = screenHeight.floatValue * value
}
animation.snapTo(0f)
rocketPositionY = 0f;
isLaunched = false;
}
}
Box(
modifier = Modifier
.fillMaxSize()
.noRippleClickable {
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, 0.7f)
isLaunched = true
}
}
.background(Color(context.getColor(R.color.background)))
.onSizeChanged { screenHeight.floatValue = it.height.toFloat() }
) {
drawRocket(rocketPositionY)
}
}
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 - rampUpDuration - 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 lanzamiento de cohete, la API de WaveformEnvelopeBuilder permite diseñar muchos efectos hápticos complejos, ya que habilita el control de 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 un electrocardiograma (ECG) típico 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 retardo. El primer pulso característico es el complejo QRS, que se muestra como un pico pronunciado de gran amplitud y corta duración. 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 WaveformEnvelopeBuilder para construir varias repeticiones de estos dos pulsos separados por un retardo fijo del primer al segundo pulso. El primer pulso puede ser un tono que comienza en una frecuencia baja y termina en una frecuencia más alta en un período corto. El segundo pulso se puede representar como un solo período de una sinusoide de baja frecuencia. Podemos componer los dos pulsos en un ritmo y repetir la composición varias veces con un retraso entre cada repetición, siguiendo un ritmo típico de pulsaciones por minuto (PPM). El resultado es un efecto háptico que se asemeja a un corazón que late.
Puedes probar LavaBeats en nuestra app de ejemplo de tecnología 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 háptico. 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
@RequiresApi(Build.VERSION_CODES.BAKLAVA)
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 */
@Stable
data class BeatParameter(
val description: String = "",
val value: Float = 0f,
val range: ClosedFloatingPointRange<Float> = 0f..1f,
val steps: Int = 0,
val isFrequencyType: Boolean = false,
)