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 dans une application Android. Comme la plupart des informations de cette page reposent sur une bonne connaissance du fonctionnement d'un actionneur de vibration, nous vous recommandons de lire le composant d'activation des actionneurs de vibrations.

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: les appareils compatibles avec ces primitives nécessaires aux 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 dont l'amplitude et la fréquence varient. 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 lire de longs modèles de vibration qui modifient l'amplitude de vibration au fil du temps. Ces effets sont appelés "formes d'onde".

Les effets de forme d'onde peuvent être facilement perceptibles, mais les vibrations soudaines et longues peuvent effrayer l'utilisateur s'ils sont diffusés dans un environnement calme. L'augmentation trop rapide de l'amplitude cible peut également produire des bourdonnements audibles. Pour concevoir des formes d'onde, nous vous recommandons d'adoucir les transitions d'amplitude afin de créer des effets de montée et de descente.

Exemple: 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 le vibreur "désactivé" 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 // Do not 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; // Do not repeat.

vibrator.vibrate(VibrationEffect.createWaveform(timings, amplitudes, repeatIndex));

Exemple: Motif 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.

Exemple: Format avec création de remplacement

Le contrôle de l'amplitude d'une vibration est une fonctionnalité dépendant du matériel. Si vous lisez une forme d'onde sur un appareil bas de gamme sans cette fonctionnalité, il vibre à l'amplitude maximale pour chaque entrée positive dans le tableau d'amplitude. Si votre application doit prendre en charge de tels appareils, nous vous recommandons de vous assurer que votre modèle ne génère pas d'effet de bourdonnement lorsqu'il est lu dans cette condition, ou de concevoir 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 les composer 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.

La procédure de création de motifs de vibration personnalisés, décrite précédemment sur cette page, explique comment contrôler l'amplitude des vibrations pour créer des effets fluides de montée et de descente. La haptique enrichie 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 est conforme aux principes haptiques pour une haptique claire. Pour en savoir plus sur ces fonctionnalités et leur fonctionnement, consultez la section Présentation des actionneurs à vibration.

Android ne fournit pas de solutions de remplacement pour les compositions avec des primitives non compatibles. Nous vous recommandons de suivre les étapes ci-dessous:

  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 informations ci-dessous.

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.

Si vous souhaitez créer une version faible et une version forte de la même primitive, il est recommandé que les échelles diffèrent d'un ratio d'au moins 1,4, afin que la différence d'intensité soit facilement perceptible. N'essayez pas de créer plus de trois niveaux d'intensité de la même primitive, car ils ne sont pas distincts de manière perceptive. Par exemple, utilisez des échelles de 0,5, 0,7 et 1,0 pour créer une version à faible, moyenne et haute intensité d'une primitive.

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. Envisagez d'utiliser 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());

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

Exemple: Résistance (avec des "tic" faibles)

Vous pouvez contrôler l'amplitude de la vibration primitive pour transmettre des commentaires utiles sur 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 d'animation de vue contrôlée par un geste de glissement et enrichie de retour haptique dans l'exemple suivant.

Animation d'un cercle qui est tiré 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);
  }
}

Exemple: 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 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 permet également de prolonger la durée globale de l'effet. Perceptuellement, les utilisateurs remarquent toujours davantage 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;
  }
}

Exemple: Wobble (avec des rotations)

L'un des principes haptiques clés est de ravir les utilisateurs. Une façon amusante d'introduire un effet de vibration agréable et inattendu consiste à 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 balancement 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.

Nous vous déconseillons d'utiliser un écart 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 [-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 [-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)
  }
}

Exemple: 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 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 simple améliorée par un effet de choc 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;
          }
        }
      });
  }
}