Créer des effets haptiques personnalisés

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'ondes de vibration standards dans une application Android.

Cette page comprend 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 un effet personnalisé, tenez compte des points suivants:

  • Fonctionnalités de l'appareil requises pour l'effet
  • Que faire lorsque l'appareil ne peut pas lire l'effet ?

La documentation de référence de l'API haptique Android explique comment vérifier la prise en charge des composants impliqués dans vos haptiques afin que votre application puisse fournir une expérience globale cohérente.

Selon votre cas d'utilisation, vous pouvez désactiver les effets personnalisés ou en fournir d'autres en fonction des différentes fonctionnalités potentielles.

Planifiez les classes de fonctionnalités d'appareils générales suivantes:

  • Si vous utilisez des primitives haptiques: appareils compatibles avec ces primitives requises par les effets personnalisés. (Pour en savoir plus sur les primitives, consultez la section suivante.)

  • Appareils avec contrôle de l'amplitude

  • Appareils compatibles avec les vibrations de base (marche/arrêt), c'est-à-dire ceux qui ne disposent pas de contrôle de l'amplitude.

Si le choix de l'effet haptique de votre application tient compte de ces catégories, son expérience utilisateur haptique devrait rester prévisible pour chaque appareil.

Utilisation de primitives haptiques

Android inclut plusieurs primitives haptiques qui varient à la fois en amplitude et en fréquence. Vous pouvez utiliser une primitive seule ou plusieurs primitives combinées pour obtenir des effets haptiques riches.

  • Utilisez des délais de 50 ms ou plus pour les é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 d'au moins 1,4 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 à intensité faible, moyenne et élevée d'une primitive.

Créer des modèles de vibration personnalisés

Les modèles de vibration sont souvent utilisés dans les haptiques d'attention, comme les notifications et les sonneries. Le service Vibrator peut diffuser de longs motifs de vibration qui modifient l'amplitude des vibrations au fil du temps. Ces effets sont appelés "formes d'onde".

Les effets de forme d'onde sont généralement perceptibles, mais des vibrations soudaines et prolongées peuvent effrayer l'utilisateur s'ils sont diffusés dans un environnement calme. Une montée trop rapide vers une amplitude cible peut également produire des bourdonnements audibles. Concevez des modèles de forme d'onde pour lisser les transitions d'amplitude afin de créer des effets de montée et de descente.

Exemples de modèles de vibration

Les sections suivantes fournissent plusieurs exemples de modèles 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 de durées, en millisecondes, pour 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 "arrêt" du vibreur et 255 l'amplitude maximale de l'appareil.
  3. Indice de répétition:indice 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. Le premier impulsion est une montée progressive jusqu'à l'amplitude maximale, et le second est une montée rapide pour maintenir l'amplitude maximale. L'arrêt à la fin est défini par la valeur négative de l'indice 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));

Schéma répétitif

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

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

Modèle avec remplacement

Le contrôle de l'amplitude d'une vibration est une capacité dépendant du matériel. Si vous lisez une forme d'onde sur un appareil d'entrée de gamme sans cette fonctionnalité, l'appareil vibre à l'amplitude maximale pour chaque entrée positive dans le tableau d'amplitude. Si votre application doit prendre en charge de tels appareils, utilisez un modèle qui ne génère pas d'effet de bourdonnement lorsqu'il est lu dans cette condition, ou concevez un modèle ON/OFF plus simple qui peut être lu à 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));
}

Créer des compositions de vibration

Cette section présente des façons de composer des vibrations en effets personnalisés plus longs et plus complexes, et va plus loin pour explorer les retours haptiques riches à l'aide de fonctionnalités matérielles plus avancées. Vous pouvez utiliser des combinaisons d'effets qui varient l'amplitude et la fréquence pour créer des effets haptiques plus complexes sur les appareils dotés d'actionneurs haptiques dont la bande passante de fréquence est plus large.

Le processus de création de modèles de vibration personnalisés, décrit précédemment sur cette page, explique comment contrôler l'amplitude des vibrations pour créer des effets de montée et de descente fluides. La haptique riche améliore ce concept en explorant la plage de fréquences plus large du vibreur de l'appareil pour rendre l'effet encore plus fluide. Ces formes d'onde sont particulièrement efficaces pour créer un effet crescendo ou diminuendo.

Les primitives de composition, décrites plus haut sur cette page, sont implémentées par le fabricant de l'appareil. Ils offrent une vibration nette, courte et agréable qui s'aligne sur les principes haptiques pour une haptique claire. Pour en savoir plus sur ces fonctionnalités et leur fonctionnement, consultez Présentation des actionneurs à vibration.

Android ne fournit pas de solutions de remplacement pour les compositions avec des primitives non compatibles. Par conséquent, procédez comme suit:

  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 non seulement les effets pour lesquels une primitive est manquante.

Pour savoir comment vérifier la compatibilité de l'appareil, consultez les sections suivantes.

Créer des 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:

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 dans l'ordre. Chaque primitive est également évolutive, ce qui vous permet de contrôler l'amplitude des vibrations générées 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 de la même primitive, créez des rapports de force d'au moins 1, 4 afin que la différence d'intensité puisse être facilement perçue. N'essayez pas de créer plus de trois niveaux d'intensité de la même primitive, car ils ne sont pas distincts visuellement. 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 les retards à 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 l'ordre de 50 ms ou plus si vous souhaitez créer un écart perceptible entre deux primitives. Voici un exemple de composition avec des retards:

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

Vous pouvez utiliser les API suivantes pour vérifier la prise en charge de primitives spécifiques par l'appareil:

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 des "tic" faibles)

Vous pouvez contrôler l'amplitude de la vibration primitive pour transmettre des commentaires utiles à une action en cours. Vous pouvez utiliser des valeurs d'échelle espacées de manière rapprochée 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. Vous trouverez un exemple dans l'animation de vue suivante, contrôlée par un geste de glissement et enrichie par la technologie haptique.

Animation d'un cercle qui est glissé 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 sortie 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 montée et descente)

Il existe deux primitives pour augmenter l'intensité des vibrations perçues : 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, 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 permet également de prolonger la durée globale de l'effet. Perceptuellement, les utilisateurs remarquent toujours plus la partie ascendante que la partie descendante. Vous pouvez donc réduire la partie ascendante 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 de montée peut renforcer la sensation d'expansion pendant l'animation. La combinaison des effets de montée et de descente permet de mettre en avant le repliement à la fin de l'animation.

Animation d'un cercle qui se développe.
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;
    }
}

Ondulation (avec des rotations)

L'un des principes haptiques clés est de ravir les utilisateurs. Pour ajouter un effet de vibration inattendu et agréable, vous pouvez utiliser PRIMITIVE_SPIN. Cette primitive est la plus efficace lorsqu'elle est appelée plusieurs fois. La concatenaison de plusieurs rotations peut créer un effet de vacillement et d'instabilité, qui peut être encore amélioré en appliquant une mise à l'échelle quelque peu aléatoire à chaque primitive. Vous pouvez également tester l'espace entre les primitives de rotation successives. Deux rotations sans aucun espace (0 ms entre les deux) créent une sensation de rotation serrée. Augmenter l'intervalle entre les rotations de 10 à 50 ms entraîne une sensation de rotation plus lâche et peut être utilisé pour adapter la durée d'une vidéo ou d'une animation.

N'utilisez pas d'espace supérieur à 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é tirée vers le bas, puis relâchée. L'animation est améliorée par une paire d'effets de rotation, lus avec des intensités variables proportionnelles au déplacement de 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 sortie 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 est la simulation d'interactions physiques. PRIMITIVE_THUD peut créer un effet fort 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 choc joué chaque fois que la balle rebondit en bas de l'écran:

Animation d&#39;une balle qui tombe et rebondit sur le bas de l&#39;écran.
Graphique de la forme d&#39;onde de vibration d&#39;entrée.

Figure 4. Cette forme d'onde représente l'accélération de sortie 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;
            }
            }
        });
    }
}