Créer des effets haptiques personnalisés

Fournir des exemples et des conseils sur la création d'effets haptiques personnalisés dans Android, y compris des compositions ancrées dans la chronologie avec VibrationEffect.Builder, des schémas de vibration personnalisés et des enveloppes de forme d'onde avancées. keywords_public: > Android, haptics, custom effects, vibration, haptic APIs, vibration patterns, VibrationEffect.Builder, compositions, haptic primitives, waveform envelopes, UI

Cette page présente des exemples d'utilisation de différentes API haptiques 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 :

Pour obtenir 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é des appareils et le comportement de secours 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 de VibrationEffect.Builder incluent une solution de secours automatique au niveau du framework. Si un appareil n'est pas compatible en mode natif avec un Preset ou un Envelope de base demandé, le framework le traduit automatiquement en une alternative appropriée au moment de la lecture, en faisant de son mieux. Vous n'avez pas besoin de vérifier manuellement les capacités de chaque appareil primitif avant de lire les effets composés avec VibrationEffect.Builder.
    • Exception : Les enveloppes de forme d'onde avancées créées avec WaveformEnvelopeBuilder ne sont pas compatibles avec le remplacement automatique, car elles dépendent de mappages de fréquence matériels spécifiques (FOAM). Si le format n'est pas compatible, la vidéo ne sera pas lue.
  • VibrationEffect.Composition : les compositions créées avec l'API startComposition() ne disposent pas de solutions de remplacement automatiques. Si une composition contient au moins une primitive non prise en charge, l'ensemble de la vibration ne sera pas lu. Vous devez vérifier manuellement les capacités à l'aide de vibrator.arePrimitivesSupported().
  • Formes d'onde avec contrôle de l'amplitude : les amplitudes non nulles sont arrondies à 100% sur les appareils sans contrôle de l'amplitude. Vérifiez vibrator.hasAmplitudeControl() et revenez à un schéma ON/OFF explicitement conçu si nécessaire.

Utilisation des primitives haptiques

Android inclut plusieurs primitives haptiques dont l'amplitude et la fréquence varient. Vous pouvez utiliser une seule primitive ou plusieurs primitives combinées pour obtenir des effets haptiques riches.

  • Utilisez des délais de 50 ms ou plus pour créer des espaces visibles entre deux primitives, en tenant également compte de la durée de la primitive si possible.
  • Utilisez des échelles dont le rapport est d'au moins 1,4 afin que la différence d'intensité soit mieux perçue.
  • Utilisez des facteurs de mise à l'échelle de 0,5, 0,7 et 1,0 pour créer une version à faible, moyenne et haute intensité d'une primitive.

Créer des schémas de vibration personnalisés

Les schémas de vibration sont souvent utilisés dans les technologies haptiques attentionnelles, comme les notifications et les sonneries. Le service Vibrator peut lire de longs schémas de vibration qui modifient l'amplitude de la 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 jouées dans un environnement calme. Si vous augmentez trop rapidement l'amplitude cible, vous risquez également de produire des bourdonnements audibles. Concevez des formes d'onde pour lisser les transitions d'amplitude et créer des effets de montée et de descente.

Exemples de schémas de vibration

Les sections suivantes fournissent plusieurs exemples de schémas de vibration :

Modèle d'activation progressive

Les formes d'onde sont représentées sous la forme VibrationEffect avec trois paramètres :

  1. Timings : tableau des durées, en millisecondes, de chaque segment de forme d'onde.
  2. 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, où 0 représente l'état "vibreur désactivé" et 255 l'amplitude maximale de l'appareil.
  3. 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 modèle 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 les impulsions. 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 négative de l'index de répétition.

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

Motif récurrent

Les formes d'onde peuvent également être lues en boucle 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 :

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

Cela est très utile pour les événements intermittents qui nécessitent une action de l'utilisateur pour être reconnus. Il peut s'agir, par exemple, d'appels téléphoniques entrants ou d'alarmes déclenchées.

Modèle avec remplacement

Le contrôle de l'amplitude d'une vibration est une fonctionnalité qui dépend du matériel. La lecture d'une forme d'onde sur un appareil d'entrée de gamme sans 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 schéma qui ne génère pas d'effet de bourdonnement lorsqu'il est lu dans ces conditions, ou concevez un schéma ON/OFF plus simple qui peut être utilisé à la place.

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

Compositions ancrées dans la timeline avec VibrationEffect.Builder

À partir d'Android 16 (4e trimestre 2023), VibrationEffect.Builder est l'API recommandée pour créer des effets et des compositions de vibration complexes. Il 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 :

Les vibrations créées avec VibrationEffect.Builder disposent d'une solution de secours automatique 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érification manuelle des fonctionnalités. (Les enveloppes de forme d'onde avancées créées avec WaveformEnvelopeBuilder nécessitent une prise en charge matérielle et ne disposent pas d'une prise en charge de secours automatique.)

Composer avec des préréglages

Utilisez VibrationEffect.Preset pour ajouter des impulsions haptiques courtes courantes et prédéfinies (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 retours haptiques clairs. Pour en savoir plus sur ces fonctionnalités et leur fonctionnement, consultez Principes de base 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 ou une diminution progressive, 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 de manière fluide des instances VibrationEffect.Envelope (créées à l'aide de BasicEnvelopeBuilder ou WaveformEnvelopeBuilder) avec des préréglages pour créer des schémas haptiques multisegments riches.

Voici un exemple qui lit une enveloppe de montée en puissance et de diminution 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éplacer 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 la prise en charge du 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);

Timing, 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 générateur doit avoir un startTimeMillis strictement supérieur ou égal à 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 build() à l'aide des durées d'éléments connues (ou d'un minimum de 1 ms pour les préréglages). Si un chevauchement impossible est détecté, une IllegalArgumentException est 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 horaire disponible. Cela permet de s'assurer que les événements ne se chevauchent pas et qu'aucune vibration n'est perdue, mais cela peut entraîner une légère dérive temporelle si les événements sont programmés trop près les uns des autres. Pour minimiser la dérive, laissez suffisamment de temps (par exemple, 50 ms ou plus) entre les é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 plus haut sur cette page, sont implémentées par le fabricant de l'appareil. Elles 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 Principes de base des actionneurs de vibration.

Contrairement à VibrationEffect.Builder, l'API VibrationEffect.Composition ne dispose pas de solutions de secours automatiques pour les primitives non compatibles. Par conséquent :

  1. Avant d'activer les retours haptiques avancés, vérifiez qu'un appareil donné est compatible avec toutes les primitives que vous utilisez.

  2. Désactivez l'ensemble cohérent d'expériences non compatibles, et pas seulement les effets pour lesquels 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 fallback d'exécution si une primitive n'est pas prise en charge sur 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 volume qui augmente lentement, suivi d'un effet de clic net :

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

Une composition est créée en ajoutant des primitives à lire en séquence. 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 en fait à 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 du même primitif, 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 la même primitive, car ils ne sont pas perceptuellement distincts. Par exemple, utilisez des échelles de 0,5, 0,7 et 1,0 pour créer des versions à faible, moyenne et haute intensité d'une primitive.

Ajouter des espaces entre les primitives de vibration

La composition peut également spécifier des délais à ajouter entre les primitives consécutives. Ce délai est exprimé en millisecondes depuis la fin de la primitive précédente. En général, un écart de 5 à 10 ms entre deux primitives est trop court pour être détectable. Utilisez un écart de 50 ms ou plus si vous souhaitez créer un écart perceptible entre deux primitives. Voici un exemple de composition avec des délais :

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

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 :

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.
}

Il est également possible de vérifier plusieurs primitives, puis de décider lesquelles composer en fonction du niveau de compatibilité de l'appareil :

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

Exemples de compositions de vibrations

Les sections suivantes fournissent plusieurs exemples de compositions de vibrations, tirés de l'application exemple haptique sur GitHub.

Résistance (avec peu de coches)

Vous pouvez contrôler l'amplitude de la vibration primitive pour transmettre des informations utiles sur une action en cours. Des valeurs d'échelle très proches peuvent être utilisées pour créer un effet 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 avec des retours haptiques.

Animation d'un cercle qui est déplacé vers le bas.
Graphique de la forme d'onde de vibration d'entrée.

Figure 1. Cette forme d'onde représente l'accélération de la vibration sur un appareil.

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

Développer (avec augmentation et diminution)

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 diminution progressive, PRIMITIVE_QUICK_FALL. Ces primitives fonctionnent mieux ensemble pour créer un segment de forme d'onde dont l'intensité augmente, puis diminue. Vous pouvez aligner les primitives mises à l'échelle pour éviter les sauts d'amplitude soudains entre elles, ce qui fonctionne également bien pour prolonger la durée globale de l'effet. Sur le plan perceptuel, les gens remarquent toujours davantage la partie montante que la partie descendante. Vous pouvez donc raccourcir la partie montante par rapport à la partie descendante pour mettre l'accent sur la partie descendante.

Voici un exemple d'application de cette composition pour développer 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 évidence l'effondrement à la fin de l'animation.

Animation d'un cercle qui s'agrandit.
Graphique de la forme d'onde de vibration d'entrée.

Figure 2 : cette forme d'onde représente l'accélération de sortie de la vibration sur un appareil.

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

Oscillation (avec rotations)

L'un des principes clés 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 de vacillement et d'instabilité, qui peut être renforcé en appliquant une mise à l'échelle quelque peu aléatoire à 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 intense. Si vous augmentez l'intervalle entre les rotations de 10 à 50 ms, la sensation de rotation sera plus lente. Vous pouvez utiliser cette option pour faire correspondre la durée d'une vidéo ou d'une animation.

N'utilisez pas d'intervalle de plus de 100 ms, car les rotations successives ne s'intègrent plus bien et commencent à ressembler à des effets individuels.

Voici un exemple de forme élastique qui rebondit après avoir été déplacée vers le bas, puis relâchée. L'animation est améliorée par une paire d'effets de rotation, joués avec des intensités variables proportionnelles au déplacement du rebond.

Animation d'une forme élastique qui rebondit
Graphique de la forme d'onde de vibration d'entrée

Figure 3. Cette forme d'onde représente l'accélération de la vibration sur un appareil.

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

Rebond (avec des bruits sourds)

Une autre application avancée des effets de vibration consiste à simuler des interactions physiques. PRIMITIVE_THUD peut créer un effet fort et retentissant, 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 sur le bas de l'écran :

Animation d'une balle qui tombe et rebondit en bas de l'écran.
Graphique de la forme d'onde de vibration d'entrée.

Figure 4. Cette forme d'onde représente l'accélération de la vibration sur un appareil.

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

Forme d'onde de vibration avec enveloppes

La procédure de création de types de vibration personnalisés vous permet de contrôler l'amplitude des vibrations pour créer des effets de montée et de descente en douceur. 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 : approche accessible pour créer des effets haptiques indépendants du matériel.
  • WaveformEnvelopeBuilder : approche plus avancée pour créer des effets haptiques. 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 :

  1. Vérifiez qu'un appareil donné est compatible avec les effets d'enveloppe à l'aide de Vibrator.areEnvelopeEffectsSupported().
  2. Désactivez l'ensemble cohérent d'expériences non compatibles ou utilisez des schémas de vibration personnalisés ou des compositions comme alternatives de secours.

Pour créer des effets d'enveloppe plus basiques, utilisez BasicEnvelopeBuilder avec les paramètres suivants :

  • Une valeur intensity comprise dans la plage \( [0, 1] \), qui représente l'intensité perçue de la vibration. Par exemple, une valeur de \( 0.5 \)est perçue comme la moitié de l'intensité maximale globale que l'appareil peut atteindre.
  • Valeur de netteté dans la plage \( [0, 1] \), qui représente la netteté de la vibration. Les valeurs faibles correspondent à des vibrations plus douces, tandis que les valeurs élevées créent une sensation plus vive.

  • Valeur duration, qui représente le temps, en millisecondes, nécessaire pour passer du dernier point de contrôle (c'est-à-dire une paire intensité/netteté) au nouveau.

Voici un exemple de forme d'onde qui augmente l'intensité d'une vibration de faible intensité à une vibration de haute intensité et de puissance maximale en 500 ms, puis la réduit à\( 0 \) (désactivée) en 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 sur les technologies haptiques, 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.

  • Valeur d'amplitude comprise dans la plage \( [0, 1] \), qui représente l'intensité de vibration réalisable à une fréquence donnée, telle que déterminée par le FOAM de l'appareil. Par exemple, une valeur de \( 0.5 \) génère la moitié de l'accélération de sortie maximale pouvant être atteinte à la fréquence donnée.
  • Valeur de fréquence, spécifiée en Hertz.

  • Valeur duration, 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. Elle commence par une rampe d'amplitude de 50 ms, de l'arrêt à la pleine puissance, à 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 ms suivantes :

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 beaucoup plus précis, ce qui vous permet d'adapter précisément l'intensité et la netteté des vibrations. Cela permet d'obtenir un retour haptique qui suit 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 sur le bas de l'écran :

Animation d'un ressort qui rebondit sur le bas de l'écran.
Graphique de la forme d'onde de vibration d'entrée.

Figure 5. Graphique de forme d'onde d'accélération de sortie pour une vibration simulant un ressort rebondissant.

@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")
    }
  }
}

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 toute la gamme de fréquences de l'appareil, ce qui permet de créer des effets haptiques très personnalisés. En combinant ces données 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 utilisant un schéma de vibration dynamique. L'effet passe de la sortie d'accélération de fréquence minimale prise en charge, 0,1 G, à la fréquence de résonance, en conservant toujours une entrée d'amplitude de 10 %. L'effet peut ainsi commencer avec une sortie raisonnablement forte et 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 abstrait les informations spécifiques à l'appareil concernant sa fréquence de résonance et sa courbe d'accélération de sortie. Si vous augmentez la netteté, la fréquence équivalente peut dépasser la résonance, ce qui peut entraîner une baisse d'accélération involontaire.

Animation d'une fusée qui décolle depuis le bas de l'écran.
Graphique de la forme d'onde de vibration d'entrée.

Figure 6. Graphique de forme d'onde d'accélération de sortie pour une vibration simulant le lancement d'une fusée.

@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

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. Un autre exemple de conception de ce type est l'émulation de sensations physiques plus abstraites, telles que la "vivacité".

Pour ce faire, vous pouvez représenter les biomarqueurs d'un signal d'électrocardiogramme (ECG) typique 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 aigu de forte amplitude et de courte durée. La deuxième impulsion est l'onde T, qui a 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 chirp qui commence à une basse fréquence 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 pulsations en un rythme et répéter la composition plusieurs fois avec un délai entre chaque répétition, 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 haptique 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.

Animation d'une lampe à lave qui bat au rythme d'un cœur haptique.
Graphique d'un segment ECG et de ses formes d'onde caractéristiques

Figure 7.  Segment d'un enregistrement ECG avec le complexe QRS et l'onde 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,
)