Cette page présente des exemples d'utilisation de différentes API de retour haptique pour créer des effets personnalisés au-delà des formes d'onde de vibration standards dans une application Android.
Cette page inclut les exemples suivants :
- Compositions ancrées à la chronologie avec
VibrationEffect.Builder- Composer avec des préréglages : séquencer des sensations haptiques prédéfinies.
- Composer avec des enveloppes et des préréglages : combinez des enveloppes et des préréglages sur une chronologie.
- Réutiliser et décaler des événements : décalez et réutilisez des événements de composition existants.
- Compositions répétées : créez des effets de chronologie répétitifs.
- Types de vibration personnalisés
- Modèle de montée en puissance : modèle qui commence en douceur.
- Motif répétitif : motif sans fin.
- Modèle avec remplacement : démonstration d'un remplacement.
- Compositions de primitives de vibration
- Résistance : effet de traînée avec une intensité dynamique.
- Expansion : effet de montée puis de descente.
- Wobble : effet d'oscillation à l'aide de la primitive
SPIN. - Bounce : effet de rebond à l'aide de la primitive
THUD.
- Forme d'onde de vibration avec enveloppes
- Ressort rebondissant : effet de rebond élastique utilisant des effets d'enveloppe de base.
- Lancement de fusée : effet de lancement de fusée utilisant des effets d'enveloppe de forme d'onde.
Pour d'autres exemples, consultez Ajouter un retour haptique aux événements et suivez toujours les principes de conception haptique.
Utiliser des solutions de remplacement pour gérer la compatibilité des appareils
Lorsque vous implémentez des effets haptiques personnalisés, la compatibilité de l'appareil et le comportement de remplacement dépendent de la surface d'API que vous choisissez :
VibrationEffect.Builder(recommandé) : à partir d'Android 16 (4e trimestre 2026), les effets créés à l'aide deVibrationEffect.Builderincluent un comportement de remplacement automatique au niveau du framework. Si un appareil n'est pas compatible de manière native avec unPresetdemandé ou unEnvelopede base, le framework le traduit automatiquement en une alternative appropriée au moment de la lecture, dans la mesure du possible. Vous n'avez pas besoin de vérifier manuellement les fonctionnalités de l'appareil par primitive avant de lire des effets composés avecVibrationEffect.Builder.- Exception : Les enveloppes de forme d'onde avancées créées avec
WaveformEnvelopeBuilderne sont pas compatibles avec le remplacement automatique, car elles dépendent de mappages de fréquence matériels spécifiques (FOAM). Si ce n'est pas le cas, elles ne seront pas lues.
- Exception : Les enveloppes de forme d'onde avancées créées avec
VibrationEffect.Composition: Les compositions créées avec l'APIstartComposition()ne disposent pas de solutions de remplacement automatiques. Si une composition contient ne serait-ce qu'une primitive non compatible, la vibration ne se déclenche pas. Vous devez vérifier manuellement les fonctionnalités à l'aide devibrator.arePrimitivesSupported().- Formes d'onde avec contrôle de l'amplitude : les amplitudes non nulles sont arrondies à 100% sur les appareils ne disposant pas de contrôle de l'amplitude. Vérifiez
vibrator.hasAmplitudeControl()et revenez à un modèle ON/OFF explicitement conçu si nécessaire.
Utilisation de primitives haptiques
Android inclut plusieurs primitives haptiques qui varient en amplitude et en fréquence. Vous pouvez utiliser une seule primitive ou plusieurs primitives en combinaison pour obtenir des effets haptiques riches.
- Utilisez des délais de 50 ms ou plus pour créer des écarts perceptibles entre deux primitives, en tenant également compte de la durée de la primitive si possible.
- Utilisez des échelles qui diffèrent d'un ratio de 1,4 ou plus afin que la différence d'intensité soit mieux perçue.
Utilisez des échelles de 0,5, 0,7 et 1,0 pour créer une version de faible, moyenne et forte intensité d'une primitive.
Créer des séquences de vibrations personnalisées
Les modèles de vibration sont souvent utilisés dans les haptiques attentionnelles, comme les notifications et les sonneries. Le service Vibrator peut lire des modèles de vibration longs qui modifient l'amplitude de vibration au fil du temps. Ces effets sont appelés "formes d'onde".
Les effets de forme d'onde sont généralement perceptibles, mais de longues vibrations soudaines peuvent surprendre l'utilisateur si elles sont lues dans un environnement calme. Une augmentation trop rapide de l'amplitude cible peut également produire des bourdonnements audibles. Concevez des formes d'onde pour lisser les transitions d'amplitude afin de créer des effets de montée et de descente.
Exemples de types de vibration
Les sections suivantes fournissent plusieurs exemples de séquences de vibrations :
Modèle d'activation progressive
Les formes d'onde sont représentées par VibrationEffect avec trois paramètres :
- Timings : tableau de durées, en millisecondes, pour chaque segment de forme d'onde.
- Amplitudes : amplitude de vibration souhaitée pour chaque durée spécifiée dans le premier argument, représentée par une valeur entière comprise entre 0 et 255, 0 représentant l'état "désactivé" du vibreur et 255 l'amplitude maximale de l'appareil.
- Index de répétition : index du tableau spécifié dans le premier argument pour commencer à répéter la forme d'onde, ou -1 si le motif ne doit être lu qu'une seule fois.
Voici un exemple de forme d'onde qui émet deux impulsions avec une pause de 350 ms entre elles. La première impulsion est une augmentation progressive jusqu'à l'amplitude maximale, et la seconde est une augmentation rapide pour maintenir l'amplitude maximale. L'arrêt à la fin est défini par la valeur d'index de répétition négative.
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 ) )
Motif récurrent
Les formes d'onde peuvent également être lues de manière répétée jusqu'à ce qu'elles soient annulées. Pour créer une forme d'onde répétée, définissez un paramètre repeat non négatif. Lorsque vous lisez une forme d'onde répétée, la vibration se poursuit jusqu'à ce qu'elle soit explicitement annulée dans le service :
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() }
Cette fonctionnalité est très utile pour les événements intermittents qui nécessitent une action de l'utilisateur pour les confirmer. Ces événements incluent les appels téléphoniques entrants et les alarmes déclenchées.
Modèle avec remplacement
Le contrôle de l'amplitude d'une vibration est une fonctionnalité dépendante du matériel. La lecture d'une forme d'onde sur un appareil d'entrée de gamme ne disposant pas de cette fonctionnalité entraîne la vibration de l'appareil à l'amplitude maximale pour chaque entrée positive du tableau d'amplitude. Si votre application doit s'adapter à de tels appareils, utilisez un motif qui ne génère pas d'effet de bourdonnement lorsqu'il est lu dans ces conditions, ou concevez un motif ON/OFF plus simple qui peut être lu en remplacement.
if (vibrator.hasAmplitudeControl()) { vibrator.vibrate( VibrationEffect.createWaveform( smoothTimings, amplitudes, smoothRepeatIdx ) ) } else { vibrator.vibrate( VibrationEffect.createWaveform( onOffTimings, onOffRepeatIdx ) ) }
Compositions ancrées dans la chronologie avec VibrationEffect.Builder
À partir d'Android 16 (26Q4), VibrationEffect.Builder est l'API recommandée pour créer des effets et des compositions de vibration complexes. Elle vous permet de créer des sensations haptiques expressives en séquençant des éléments haptiques discrets le long d'une chronologie absolue avec startTimeMillis.
VibrationEffect.Builder permet de combiner plusieurs types de vibrations :
- Préréglages : impulsions haptiques prédéfinies (
VibrationEffect.Preset) telles que des clics et des tics. - Enveloppes : formes d'onde continues dynamiques, y compris les enveloppes de base indépendantes du matériel (
BasicEnvelopeBuilder) et les enveloppes avancées à modulation de fréquence (WaveformEnvelopeBuilder). - VibrationEffects existants : compositions primitives (
VibrationEffect.Composition), formes d'onde par étapes (VibrationEffect.createWaveform), effets prédéfinis (VibrationEffect.createPredefined) et one-shots (VibrationEffect.createOneShot) importés à l'aide deaddEvents()ou de la copie du constructeur. - Séquences répétées : modèles continus configurés avec
setRepeatingEffect().
Les vibrations créées avec VibrationEffect.Builder disposent d'un système de remplacement automatique intégré au niveau du framework pour leurs éléments (y compris les préréglages, les enveloppes de base et les effets de vibration combinés). Cela garantit une expérience utilisateur cohérente sur différents appareils sans nécessiter de vérifications manuelles des capacités. (Les enveloppes de forme d'onde avancées créées avec WaveformEnvelopeBuilder nécessitent une compatibilité matérielle et ne disposent pas d'une compatibilité de remplacement automatique.)
Composer avec des préréglages
Utilisez VibrationEffect.Preset pour ajouter des impulsions haptiques courtes, prédéfinies et courantes (telles que PRESET_CLICK, PRESET_TICK ou PRESET_LOW_TICK) à une composition.
Les préréglages sont implémentés par le fabricant de l'appareil pour fournir une vibration nette, courte et agréable qui s'aligne sur les principes haptiques pour des effets haptiques clairs. Pour en savoir plus sur ces fonctionnalités et leur fonctionnement, consultez Présentation des actionneurs de vibration.
Les préréglages remplacent les primitives courtes de l'API VibrationEffect.Composition.
Pour des sensations haptiques plus longues ou continues, comme une augmentation et une diminution progressives, utilisez plutôt des formes d'onde d'enveloppe (PWLE).
Chaque préréglage peut se voir attribuer une échelle facultative comprise entre 0.0f et 1.0f, et est placé à une heure de début explicite (en millisecondes) à partir du début de la composition.
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);
Composer avec des enveloppes et des préréglages
Vous pouvez combiner facilement des instances VibrationEffect.Envelope (créées à l'aide de BasicEnvelopeBuilder ou WaveformEnvelopeBuilder) avec des préréglages pour créer des modèles haptiques riches et multisegments.
Voici un exemple qui lit une enveloppe de montée et de descente progressive, suivie d'un préréglage de clic net :
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);
Réutiliser et décaler des événements existants
Pour réutiliser ou concaténer un VibrationEffect existant (y compris VibrationEffect.Composition), récupérez sa liste d'objets VibrationEffect.Event à l'aide de getEvents() et ajoutez-les avec un décalage à l'aide de addEvents(startTimeShiftMillis, events) (ou transmettez l'effet directement au constructeur VibrationEffect.Builder(effect)). Lorsque vous importez des instances VibrationEffect.Composition de cette manière, le framework convertit automatiquement leurs primitives en préréglages, ce qui permet d'activer la compatibilité avec les solutions de remplacement au moment de l'exécution.
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);
Créer des compositions répétées
Utilisez setRepeatingEffect(startTimeMillis, repeatingEffect, durationMillis) pour ajouter un motif répétitif à une composition :
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);
Calendrier, validation et gestion de la dérive
Lorsque vous concevez des compositions avec VibrationEffect.Builder, tenez compte des règles de timing et de validation suivantes :
- Heures de début strictement croissantes : chaque élément ajouté au compilateur doit avoir une valeur
startTimeMillisstrictement supérieure ou égale à l'heure de début de l'élément précédent. - Validation au moment de la compilation : le compilateur effectue une validation au mieux au moment de l'exécution de
build()en utilisant des durées d'éléments connues (ou un minimum de 1 ms pour les préréglages). Si un chevauchement impossible est détecté, une exceptionIllegalArgumentExceptionest générée. - Décalage séquentiel de la lecture : si un élément de vibration précédent est toujours en cours de lecture physique lorsque l'heure de début de l'élément suivant arrive, le framework décale automatiquement l'élément suivant vers le premier créneau disponible. Cela permet d'éviter que les événements ne se chevauchent et qu'aucune vibration ne soit ignorée, même si cela peut entraîner un léger décalage temporel si les événements sont programmés trop près les uns des autres. Pour minimiser la dérive, prévoyez un délai suffisant (par exemple, 50 ms ou plus) entre deux événements haptiques consécutifs.
Créer des compositions de primitives de vibration
Cette section explique comment composer des vibrations à l'aide de VibrationEffect.Composition. Les primitives de composition, décrites précédemment sur cette page, sont implémentées par le fabricant de l'appareil. Ils fournissent une vibration nette, courte et agréable qui s'aligne sur les principes haptiques pour des retours haptiques clairs. Pour en savoir plus sur ces fonctionnalités et leur fonctionnement, consultez Présentation des actionneurs de vibration.
Contrairement à VibrationEffect.Builder, l'API VibrationEffect.Composition ne dispose pas de solutions de remplacement automatiques pour les primitives non compatibles. Par conséquent :
Avant d'activer les fonctionnalités haptiques avancées, vérifiez qu'un appareil donné est compatible avec toutes les primitives que vous utilisez.
Désactivez l'ensemble cohérent d'expériences non compatibles, et pas seulement les effets auxquels il manque une primitive.
- Kotlin :
val fallbackEffect = VibrationEffect.Builder(compositionEffect).build()(ou.addEvents(0L, compositionEffect.events)) - Java :
VibrationEffect fallbackEffect =new VibrationEffect.Builder(compositionEffect).build();(ou.addEvents(0L, compositionEffect.getEvents()))
Lorsqu'il est construit avec VibrationEffect.Builder, le framework convertit les primitives de composition en préréglages et fournit automatiquement un remplacement d'exécution si une primitive n'est pas compatible avec l'appareil de l'utilisateur.
Effets de vibration composés
Vous pouvez créer des effets de vibration composés avec VibrationEffect.Composition. Voici un exemple d'effet de montée lente suivi d'un effet de clic net :
vibrator.vibrate( VibrationEffect.startComposition().addPrimitive( VibrationEffect.Composition.PRIMITIVE_SLOW_RISE ).addPrimitive( VibrationEffect.Composition.PRIMITIVE_CLICK ).compose() )
Une composition est créée en ajoutant des primitives à lire de manière séquentielle. Chaque primitive est également évolutive, ce qui vous permet de contrôler l'amplitude de la vibration générée par chacune d'elles. L'échelle est définie comme une valeur comprise entre 0 et 1, où 0 correspond à une amplitude minimale à laquelle cette primitive peut être (à peine) ressentie par l'utilisateur.
Créer des variantes dans les primitives de vibration
Si vous souhaitez créer une version faible et une version forte de la même primitive, créez des ratios d'intensité de 1,4 ou plus, afin que la différence d'intensité soit facilement perceptible. N'essayez pas de créer plus de trois niveaux d'intensité pour une même primitive, car ils ne sont pas distincts d'un point de vue perceptif. Par exemple, utilisez des échelles de 0,5, 0,7 et 1,0 pour créer des versions de faible, moyenne et forte intensité d'une primitive.
Ajouter des intervalles entre les primitives de vibration
La composition peut également spécifier des délais à ajouter entre des primitives consécutives. Ce délai est exprimé en millisecondes depuis la fin de la primitive précédente. En général, un intervalle de 5 à 10 ms entre deux primitives est trop court pour être détectable. Utilisez un intervalle de l'ordre de 50 ms ou plus si vous souhaitez créer un intervalle perceptible entre deux primitives. Voici un exemple de composition avec des délais :
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() )
Vérifier les primitives compatibles
Les API suivantes peuvent être utilisées pour vérifier la compatibilité de l'appareil avec des primitives spécifiques :
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. }
Il est également possible de vérifier plusieurs primitives, puis de décider lesquelles composer en fonction du niveau de compatibilité de l'appareil :
val primitives: IntArray = intArrayOf( VibrationEffect.Composition.PRIMITIVE_LOW_TICK, VibrationEffect.Composition.PRIMITIVE_TICK, VibrationEffect.Composition.PRIMITIVE_CLICK ) val supported: BooleanArray = vibrator.arePrimitivesSupported(*primitives)
Exemples de compositions de vibrations
Les sections suivantes fournissent plusieurs exemples de compositions de vibrations, tirés de l'application exemple d'effets haptiques sur GitHub.
Résister (avec des tiques faibles)
Vous pouvez contrôler l'amplitude de la vibration primitive pour transmettre des informations utiles sur une action en cours. Des valeurs d'échelle rapprochées peuvent être utilisées pour créer un effet de crescendo fluide d'une primitive. Le délai entre les primitives consécutives peut également être défini de manière dynamique en fonction de l'interaction de l'utilisateur. Ceci est illustré dans l'exemple suivant d'une animation de vue contrôlée par un geste de déplacement et augmentée par des effets haptiques.
Figure 1. Cette forme d'onde représente l'accélération de sortie de la vibration sur un appareil.
@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) } }
Développer (avec montée et descente)
Il existe deux primitives pour augmenter l'intensité de vibration perçue : PRIMITIVE_QUICK_RISE et PRIMITIVE_SLOW_RISE. Elles atteignent toutes les deux la même cible, mais avec des durées différentes. Il n'existe qu'une seule primitive pour la réduction progressive : PRIMITIVE_QUICK_FALL. Ces primitives fonctionnent mieux ensemble pour créer un segment de forme d'onde qui gagne en intensité, puis s'estompe. Vous pouvez aligner les primitives mises à l'échelle pour éviter les sauts d'amplitude soudains entre elles, ce qui permet également d'étendre la durée globale de l'effet.
En termes de perception, les utilisateurs remarquent toujours davantage la partie ascendante que la partie descendante. Par conséquent, raccourcir la partie ascendante par rapport à la partie descendante peut être utilisé pour mettre l'accent sur la partie descendante.
Voici un exemple d'application de cette composition pour agrandir et réduire un cercle. L'effet d'élévation peut renforcer l'impression d'expansion pendant l'animation. La combinaison des effets de montée et de descente permet de mettre en valeur l'effondrement à la fin de l'animation.
Figure 2 : Cette forme d'onde représente l'accélération de sortie de la vibration sur un appareil.
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) } }
Wobble (avec rotations)
L'un des principaux principes de l'haptique est de ravir les utilisateurs. Pour introduire un effet de vibration agréable et inattendu, vous pouvez utiliser PRIMITIVE_SPIN. Cette primitive est plus efficace lorsqu'elle est appelée plusieurs fois. La concaténation de plusieurs rotations peut créer un effet d'oscillation et d'instabilité, qui peut être encore renforcé en appliquant une mise à l'échelle quelque peu aléatoire sur chaque primitive. Vous pouvez également tester l'écart entre les primitives de rotation successives. Deux rotations sans intervalle (0 ms entre les deux) créent une sensation de rotation rapide. Augmenter l'intervalle entre les rotations de 10 à 50 ms donne une sensation de rotation plus lâche et peut être utilisé pour correspondre à la durée d'une vidéo ou d'une animation.
N'utilisez pas d'intervalle supérieur à 100 ms, car les rotations successives ne s'intègrent plus correctement et commencent à ressembler à des effets individuels.
Voici un exemple de forme élastique qui rebondit après avoir été étirée vers le bas, puis relâchée. L'animation est améliorée avec deux effets de rotation, lus avec des intensités variables proportionnelles au déplacement du rebond.
Figure 3. Cette forme d'onde représente l'accélération de sortie de la vibration sur un appareil.
@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) }
Rebond (avec bruits sourds)
Une autre application avancée des effets de vibration consiste à simuler des interactions physiques. PRIMITIVE_THUD peut créer un effet puissant et réverbérant, qui peut être associé à la visualisation d'un impact, dans une vidéo ou une animation par exemple, pour améliorer l'expérience globale.
Voici un exemple d'animation de chute de balle améliorée avec un effet de bruit sourd joué chaque fois que la balle rebondit en bas de l'écran :
Figure 4. Cette forme d'onde représente l'accélération de sortie de la vibration sur un appareil.
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) } }
Forme d'onde de vibration avec enveloppes
Le processus de création de modèles de vibration personnalisés vous permet de contrôler l'amplitude des vibrations pour créer des effets fluides d'augmentation et de diminution. Cette section explique comment créer des effets haptiques dynamiques à l'aide d'enveloppes de forme d'onde qui permettent de contrôler précisément l'amplitude et la fréquence des vibrations au fil du temps. Cela vous permet de créer des expériences haptiques plus riches et plus nuancées.
À partir d'Android 16 (niveau d'API 36), le système fournit les API suivantes pour créer une enveloppe de forme d'onde de vibration en définissant une séquence de points de contrôle :
BasicEnvelopeBuilder: une approche accessible pour créer des effets haptiques indépendants du matériel.WaveformEnvelopeBuilder: approche plus avancée de la création d'effets haptiques, qui nécessite de connaître le matériel haptique.
Android ne fournit pas de solutions de remplacement pour les effets d'enveloppe. Si vous avez besoin de cette assistance, procédez comme suit :
- Vérifiez qu'un appareil donné est compatible avec les effets d'enveloppe à l'aide de
Vibrator.areEnvelopeEffectsSupported(). - Désactivez l'ensemble d'expériences cohérentes qui ne sont pas compatibles, ou utilisez des modèles de vibration personnalisés ou des compositions comme alternatives de remplacement.
Pour créer des effets d'enveloppe plus basiques, utilisez BasicEnvelopeBuilder avec les paramètres suivants :
- Une valeur d'intensité comprise entre \( [0, 1] \), qui représente la force perçue de la vibration. Par exemple, une valeur de \( 0.5 \)est perçue comme la moitié de l'intensité maximale globale pouvant être atteinte par l'appareil.
Une valeur de netteté comprise entre \( [0, 1] \), qui représente la netteté de la vibration. Des valeurs plus faibles se traduisent par des vibrations plus douces, tandis que des valeurs plus élevées créent une sensation plus nette.
Une valeur de durée, qui représente le temps, en millisecondes, nécessaire pour passer du dernier point de contrôle (c'est-à-dire une paire d'intensité et de netteté) au nouveau.
Voici un exemple de forme d'onde qui augmente l'intensité d'une vibration de faible intensité à une vibration de forte intensité et de force maximale sur 500 ms, puis la réduit à\( 0 \) (désactivé) sur 100 ms.
vibrator.vibrate( VibrationEffect.BasicEnvelopeBuilder() .setInitialSharpness(0.0f) .addControlPoint(1.0f, 1.0f, 500) .addControlPoint(0.0f, 1.0f, 100) .build() )
Si vous avez des connaissances plus avancées en haptique, vous pouvez définir des effets d'enveloppe à l'aide de WaveformEnvelopeBuilder. Lorsque vous utilisez cet objet, vous pouvez accéder au mappage de la fréquence à l'accélération de sortie (FOAM) via VibratorFrequencyProfile.
- Une valeur d'amplitude comprise entre \( [0, 1] \), qui représente l'intensité de vibration pouvant être atteinte à une fréquence donnée, telle que déterminée par le FOAM de l'appareil. Par exemple, la valeur \( 0.5 \) génère la moitié de l'accélération de sortie maximale pouvant être atteinte à la fréquence donnée.
Une valeur de fréquence, spécifiée en hertz.
Une valeur de durée, qui représente le temps, en millisecondes, nécessaire pour passer du dernier point de contrôle au nouveau.
Le code suivant montre un exemple de forme d'onde qui définit un effet de vibration de 400 ms. Il commence par une rampe d'amplitude de 50 ms, de l'état désactivé à l'état complet, à une fréquence constante de 60 Hz. Ensuite, la fréquence augmente jusqu'à 120 Hz au cours des 100 ms suivantes et reste à ce niveau pendant 200 ms. Enfin, l'amplitude diminue jusqu'à \( 0 \), et la fréquence revient à 60 Hz au cours des 50 dernières millisecondes :
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() )
Les sections suivantes fournissent plusieurs exemples de formes d'onde de vibration avec des enveloppes.
Ressort rebondissant
Un exemple précédent utilise PRIMITIVE_THUD pour simuler des interactions de rebond physique. L'API d'enveloppe de base offre un contrôle nettement plus précis, ce qui vous permet d'ajuster précisément l'intensité et la netteté des vibrations.
Le retour haptique suit ainsi plus précisément les événements animés.
Voici un exemple de ressort en chute libre, avec une animation améliorée par un effet d'enveloppe de base joué chaque fois que le ressort rebondit en bas de l'écran :
Figure 5. Graphique de forme d'onde d'accélération de sortie pour une vibration qui simule un ressort qui rebondit.
@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)) } } }
Lancement de fusée
Un exemple précédent montre comment utiliser l'API d'enveloppe de base pour simuler une réaction de ressort rebondissant. Le WaveformEnvelopeBuilder permet de contrôler précisément l'ensemble de la plage de fréquences de l'appareil, ce qui permet d'obtenir des effets haptiques hautement personnalisés. En combinant cela avec les données FOAM, vous pouvez adapter les vibrations à des capacités de fréquence spécifiques.
Voici un exemple de simulation de lancement de fusée à l'aide d'un modèle de vibration dynamique. L'effet va de la sortie d'accélération de fréquence minimale compatible (0,1 G) à la fréquence de résonance, en maintenant toujours une entrée d'amplitude de 10 %. Cela permet à l'effet de démarrer avec une sortie raisonnablement forte et d'augmenter l'intensité et la netteté perçues, même si l'amplitude de pilotage est la même. Une fois la résonance atteinte, la fréquence de l'effet redescend au minimum, ce qui est perçu comme une diminution de l'intensité et de la netteté. Cela crée une sensation de résistance initiale suivie d'un relâchement, imitant un lancement dans l'espace.
Cet effet n'est pas possible avec l'API d'enveloppe de base, car elle fait abstraction des informations spécifiques à l'appareil concernant sa fréquence de résonance et sa courbe d'accélération de sortie. L'augmentation de la netteté peut pousser la fréquence équivalente au-delà de la résonance, ce qui peut entraîner une baisse involontaire de l'accélération.
Figure 6. Graphique de forme d'onde d'accélération de sortie pour une vibration qui simule un lancement de fusée.
@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
Comme dans l'exemple Lancement de fusée, l'API WaveformEnvelopeBuilder permet de concevoir de nombreux effets haptiques complexes en contrôlant les segments d'amplitude et de fréquence d'une vibration. L'émulation de sensations physiques plus abstraites, telles que la "vivacité", est un autre exemple de ce type de conception.
Pour ce faire, vous pouvez représenter les biomarqueurs d'un signal d'électrocardiogramme (ECG) standard avec des segments de vibration de certaines amplitudes et fréquences. LavaBeats est un exemple où deux segments caractéristiques d'un enregistrement ECG sont représentés par deux impulsions séparées par un délai. La première impulsion caractéristique est le complexe QRS, qui se présente sous la forme d'un pic net de forte amplitude et de courte durée. La deuxième impulsion est l'onde T, qui présente une amplitude plus faible, une durée plus longue et une forme plus lisse (voir la figure 7).
Utilisez WaveformEnvelopeBuilder pour construire différentes répétitions de ces deux impulsions séparées par un délai fixe entre la première et la deuxième impulsion. La première impulsion peut être un signal de chirp qui commence à une fréquence basse et se termine à une fréquence plus élevée sur une courte durée. La deuxième impulsion peut être représentée comme une seule période d'une sinusoïde basse fréquence. Nous pouvons composer les deux impulsions en un battement et répéter la composition plusieurs fois avec un délai entre les deux, en suivant un rythme typique de battements par minute (bpm). Le résultat est un effet haptique qui ressemble à un battement de cœur.
Vous pouvez essayer LavaBeats dans notre application exemple d'effets haptiques sur GitHub et ressentir l'effet accompagné d'une visualisation de lampe à lave qui bat au même rythme que l'effet haptique. Vous pouvez également modifier les paramètres de l'effet pour créer différentes sensations de battement en modifiant les amplitudes, les fréquences, les durées et les délais des deux impulsions.
Figure 7. Segment d'un enregistrement d'ECG avec le complexe QRS et l'onde 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, )