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. Étant donné que la plupart des informations de cette page reposent sur une bonne connaissance du fonctionnement d'un actionneur de vibration, nous vous recommandons de lire l'article Introduction à l'actionneur de vibration.

Cette page comprend les exemples suivants.

Pour obtenir des exemples supplémentaires, consultez la section Ajouter un retour haptique aux événements et suivez toujours les principes de conception haptique.

Utiliser des créations 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 n'est pas en mesure de diffuser l'effet ?

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

En fonction de votre cas d'utilisation, vous pouvez désactiver les effets personnalisés ou fournir d'autres effets personnalisés basés sur différentes capacités potentielles.

Prévoyez les classes générales de capacité des appareils suivantes:

  • Si vous utilisez des primitives haptiques: 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 (activés/désactivés), autrement dit ceux qui ne peuvent pas contrôler l'amplitude.

Si le choix d'effets haptiques de votre application tient compte de ces catégories, son expérience utilisateur haptique doit rester prévisible pour chaque appareil.

Utilisation des primitives haptiques

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

  • Utilisez des retards de 50 ms ou plus pour les écarts visibles entre deux primitives, en tenant également compte si possible de la durée de la primitive.
  • Utilisez des échelles qui diffèrent par un ratio de 1,4 ou plus afin que la différence d'intensité soit mieux perçue.
  • Utilisez des échelles de 0,5, 0,7 et 1,0 pour créer une version basse, moyenne et haute intensité d'une primitive.

Créez des vibrations personnalisées.

Les motifs de vibration sont souvent utilisés dans le retour haptique attentionnel, 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 la forme d'onde sont facilement perceptibles, mais les longues vibrations soudaines peuvent surprendre l'utilisateur s'il joue dans un environnement calme. Une accélération trop rapide vers une amplitude cible peut également produire des bourdonnements audibles. Il est recommandé de lisser les transitions d'amplitude pour créer des modèles de forme d'onde afin de créer des effets de montée et de descente.

Exemple: Modèle d'expansion

Les formes d'ondes sont représentées par VibrationEffect avec trois paramètres:

  1. Timings (Durées) : tableau des 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, 0 représentant l'arrêt du vibreur et 255 l'amplitude maximale de l'appareil.
  3. Repeat index:index du tableau spécifié dans le premier argument pour commencer à répéter la forme d'onde, ou -1 s'il ne doit lire le format qu'une seule fois.

Voici un exemple de forme d'onde qui pulse deux fois avec une pause de 350 ms entre les impulsions. La première impulsion est une montée en douceur jusqu'à l'amplitude maximale, et la seconde une montée rapide pour maintenir l'amplitude maximale. L'arrêt à la fin est défini par la valeur d'indice de répétition négative.

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: Récurrence

Les formes d'ondes peuvent également être lues à plusieurs reprises jusqu'à ce qu'elles soient annulées. Pour créer une forme d'onde qui se répète, vous devez définir un paramètre "repeat" non négatif. Lorsque vous jouez une forme d'onde répétée, la vibration continue 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 opération est très utile pour les événements intermittents nécessitant une action de l'utilisateur pour en confirmer l'existence. tels que les appels téléphoniques entrants et les alarmes déclenchées.

Exemple: Schéma avec création de remplacement

Le contrôle de l'amplitude d'une vibration est une fonctionnalité qui dépend du matériel. Si une forme d'onde est lue sur un appareil bas de gamme sans cette fonctionnalité, celui-ci vibre à l'amplitude maximale pour chaque entrée positive dans le tableau d'amplitudes. Si votre application doit s'adapter à ces appareils, nous vous recommandons de vous assurer que votre schéma ne génère pas d'effet de bourdonnement lorsqu'il est joué dans cette condition, ou de concevoir un schéma ON/OFF plus simple qui peut être lu en remplacement.

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 vibrantes

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

Le processus de création de motifs 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 fluides d'augmentation et de descente. Le retour haptique enrichi 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 de crescendo ou de diminuendo.

Les primitives de composition, décrites précédemment sur cette page, sont implémentées par le fabricant de l'appareil. Ils offrent une vibration nette, courte et agréable qui respecte les principes haptiques pour un retour haptique clair. Pour en savoir plus sur ces fonctionnalités et leur fonctionnement, consultez la section Amorce des actionneurs de vibration.

Android ne fournit pas de solutions de remplacement pour les compositions avec des primitives non compatibles. Nous vous recommandons de procéder comme suit:

  1. Avant d'activer le retour haptique avancé, 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. Pour savoir comment vérifier la compatibilité de l'appareil, procédez comme suit.

Vous pouvez créer des effets de vibration composés avec VibrationEffect.Composition. Voici un exemple d'effet d'augmentation lente suivi d'un effet de clic accentué:

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 qui seront lues en séquence. 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 à une amplitude minimale à laquelle l'utilisateur peut ressentir (à peine) cette primitive.

Si vous souhaitez créer une version faible et forte de la même primitive, il est recommandé que les échelles diffèrent par un ratio de 1,4 ou plus, afin que la différence d'intensité soit 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 perçus comme étant différenciés. Par exemple, utilisez des échelles de 0,5, 0,7 et 1,0 pour créer une version basse, moyenne et haute intensité d'une primitive.

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

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

Les API suivantes permettent de 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 choisir celles à 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ésister (avec des graduations faibles)

Vous pouvez contrôler l'amplitude de la vibration primitive pour transmettre un retour utile à une action en cours. Des valeurs d'échelle très espacées peuvent être utilisées pour créer un effet de crescendo lisse d'une primitive. Le délai entre des primitives consécutives peut également être défini de manière dynamique en fonction de l'interaction utilisateur. Ceci est illustré dans l'exemple suivant, qui illustre une animation de vue contrôlée par un geste de déplacement et complétée par un retour haptique.

Animation d'un cercle en train d'être glissé vers le bas
Trace de la forme d'onde de vibration en entrée

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: Expansion (avec hausse et baisse)

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 de la capacité : PRIMITIVE_QUICK_FALL. Ces primitives fonctionnent mieux ensemble pour créer un segment de forme d'onde qui augmente en intensité, puis s'éteint. Vous pouvez aligner les primitives mises à l'échelle pour éviter les sauts d'amplitude brusques entre elles, ce qui est également efficace pour prolonger la durée globale de l'effet. Perceptuellement, les gens remarquent toujours la partie croissante plus que la partie décroissante. Par conséquent, vous pouvez raccourcir la partie montante par rapport à la partie décroissante afin de déplacer l'accentuation vers 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 améliorer la sensation d'expansion pendant l'animation. La combinaison d'effets de montée et de chute permet de mettre en évidence le repli à la fin de l'animation.

Animation d'un cercle développé
Trace de la forme d'onde de vibration en entrée

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: Ondulation (avec rotations)

Le plaisir des utilisateurs est l'un des principaux principes haptiques. Un moyen ludique d'introduire un effet de vibration agréable et inattendu consiste à utiliser PRIMITIVE_SPIN. Cette primitive est plus efficace lorsqu'elle est appelée plusieurs fois. Plusieurs microservices concaténés peuvent créer un effet de horodaté et d'instabilité, qui peut être encore amélioré en appliquant une mise à l'échelle relativement aléatoire à chaque primitive. Vous pouvez également tester l'écart entre les primitives de rotation successive. Deux rotations sans espace (0 ms entre les deux) créent une sensation de rotation serrée. Augmenter l'écart entre les rotations de 10 à 50 ms entraîne une sensation de rotation plus lâche. Cette valeur peut être utilisée pour faire correspondre la durée d'une vidéo ou d'une animation.

Nous vous déconseillons d'utiliser un 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é glissée vers le bas, puis libérée. L'animation est améliorée par une paire d'effets de rotation, lus avec des intensités variables proportionnelles au déplacement du rebond.

Animation d'un rebond d'une forme élastique
Trace de la forme d'onde de vibration en entrée

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. L'PRIMITIVE_THUD peut créer un effet fort et rythmé, qui peut être associé à la visualisation d'un impact, dans une vidéo ou une animation, par exemple, afin d'améliorer l'expérience globale.

Voici un exemple d'animation simple consistant à lancer une balle, avec un effet de coup de balle déclenché chaque fois que la balle rebondit au bas de l'écran:

Animation d&#39;une balle qui rebondit en bas de l&#39;écran
Trace de la forme d&#39;onde de vibration en entrée

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