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. Autant d'informations sur cette page repose sur une bonne connaissance du fonctionnement d'un actionneur de vibration, nous vous recommandons de lire le Introduction aux actionneurs des vibrations.

Cette page inclut les exemples suivants.

Pour obtenir d'autres exemples, consultez la section Ajouter un retour haptique aux événements. 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:

  • les fonctionnalités d'appareil requises pour l'effet ;
  • Que faire lorsque l'appareil ne peut pas diffuser l'effet ?

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

Selon votre cas d'utilisation, vous souhaiterez peut-être désactiver les effets personnalisés ou proposer d'autres effets personnalisés en fonction de différentes capacités potentielles.

Prévoyez les classes de haut niveau suivantes pour les capacités de l'appareil:

  • Si vous utilisez des primitives haptiques: appareils compatibles avec ces primitives nécessaires pour les effets personnalisés. (Consultez la section suivante pour en savoir plus sur primitives.)

  • Appareils avec contrôle de l'amplitude.

  • Appareils dotés du vibreur de base (activé/désactivé), en d'autres termes qui manquent de contrôle de l'amplitude.

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

Utilisation de primitives haptiques

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

  • Utilisez des retards de 50 ms ou plus pour les écarts visibles entre deux primitives, en tenant compte également des primitives durée si possible.
  • Utilisez des échelles avec un ratio de 1,4 ou plus pour que la différence de l'intensité est mieux perçue.
  • Utilisez des échelles de 0,5, 0,7 et 1,0 pour créer une échelle faible, moyenne et élevée d'une primitive.

Créez des vibrations personnalisées

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

Les effets de la forme d'onde sont faciles à percevoir, mais de longues vibrations soudaines peuvent surprendre l'utilisateur si vous jouez dans un environnement calme. Accélération jusqu'à une amplitude cible trop rapide peut également émettre des bruits de bourdonnement audibles. La recommandation de concevoir des motifs de forme d'onde consiste à lisser les transitions d'amplitude pour créer des effets d'augmentation et de diminution.

Exemple: modèle d'augmentation

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

  1. Durées:tableau des durées, en millisecondes, pour chaque forme d'onde segment.
  2. Amplitudes:l'amplitude de vibration souhaitée pour chaque durée spécifiée dans le premier argument, représenté par une valeur entière comprise entre 0 et 255, avec 0 représentant l'arrêt du vibreur 255 étant la valeur maximale de l'appareil, de l'amplitude.
  3. Répéter l'index:index du tableau spécifié dans le premier argument pour commencer à répéter la forme d'onde, ou sur -1 si le schéma ne doit être lu qu'une seule fois.

Voici un exemple de forme d'onde qui clignote deux fois avec une pause de 350 ms entre les deux clignotements. La première impulsion est une montée en douceur jusqu'à l'amplitude maximale, et la seconde est une rampe 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));

Échantillon: modèle 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. La façon de créer un "répéter" permet de définir un paramètre "répété" non négatif. Lorsque vous jouez 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();
}

Ceci est très utile pour les événements intermittents qui nécessitent une action de l'utilisateur pour le confirmer. Il peut s'agir, par exemple, d'appels téléphoniques entrants et déclenché des alarmes.

Exemple: Modèle avec création de remplacement

Le contrôle de l'amplitude d'une vibration fonctionnalité dépendante du matériel. Lecture d'une forme d'onde sur un mais sans cette fonctionnalité, il vibre au maximum amplitude pour chaque entrée positive du tableau d'amplitude. Si votre application doit s'adapter à ces appareils, nous vous recommandons de vous assurer ne génère pas d'effet de bourdonnement lorsqu'il est joué dans ces conditions, ou pour concevoir un schéma d'activation/de désactivation plus simple qui peut être utilisé comme solution de 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 vous explique comment les regrouper des effets personnalisés plus longs et plus complexes, et vous permet d'explorer le retour haptique à l'aide de capacités matérielles plus avancées. Vous pouvez utiliser des combinaisons effets qui varient en amplitude et en fréquence pour créer des effets haptiques plus complexes sur les appareils avec des actionneurs haptiques qui ont une bande passante de fréquences plus large.

La procédure de création du vibreur personnalisé des modèles, décrits précédemment sur cette page, explique comment contrôler l'amplitude des vibrations pour créer des effets fluides à la hausse ou à la baisse. Le retour haptique enrichi améliore ce concept en explorant pour une plus large gamme de fréquences du vibreur de l'appareil, afin de rendre l'effet encore plus fluide. Ces formes d'onde sont particulièrement efficaces pour créer un crescendo ou un diminuendo l'effet.

Les primitives de composition, décrites plus tôt sur cette page, sont implémentées par le fabricant de l'appareil. Ils offrent une vibration courte, nette et agréable conforme aux principes haptiques pour un retour haptique clair. Pour plus pour en savoir plus sur ces fonctionnalités et leur fonctionnement, reportez-vous à la section Actionneurs de vibrations présentation.

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

  1. Avant d'activer le retour haptique avancé, vérifiez qu'un appareil donné est compatible toutes les primitives que vous utilisez.

  2. Désactivez l'ensemble cohérent d'expériences non compatibles, et pas seulement les pour lesquels il manque une primitive. Pour savoir comment consulter les la compatibilité de chaque appareil est indiquée ci-dessous.

Vous pouvez créer des effets de vibration composés d'éléments avec VibrationEffect.Composition. Voici un exemple d'effet de montée lente suivie d'un effet de clic fort:

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 La primitive est également évolutive, ce qui vous permet de contrôler l'amplitude de la vibration. générées par chacun d'eux. L'échelle est définie comme une valeur comprise entre 0 et 1, où 0 correspond à une amplitude minimale à laquelle cette primitive peut être (à peine) ressenties par l'utilisateur.

Si vous souhaitez créer une version faible et forte de la même primitive, il est recommandé que les échelles diffèrent d'un ratio de 1, 4 ou plus afin que la différence en intensité peut être facilement perçue. N'essayez pas d'en créer plus de trois niveaux d'intensité de la même primitive, car ils ne sont pas perceptuels distincts. Par exemple, utilisez des échelles de 0,5, 0,7 et 1,0 pour créer une classification faible, moyenne et haute intensité d'une primitive.

La composition peut également spécifier des retards à ajouter entre des primitives. Ce délai est exprimé en millisecondes écoulées depuis la fin de la primitive. En général, un écart de 5 à 10 ms entre deux primitives est trop important. court pour être détectable. Envisagez d'utiliser un intervalle de 50 ms ou plus si vous voulez créer un écart visible entre deux primitives. Voici une exemple de composition avec 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 compatibilité des appareils avec des primitives:

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 à utiliser Compose 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 feedback utile à une action en cours. Des valeurs d'échelle très espacées peuvent être utilisé pour créer un crescendo lisse d'une primitive. Le délai entre Des primitives consécutives peuvent également être définies de manière dynamique en fonction de l'utilisateur d'interaction. Ce processus est illustré dans l'exemple suivant, qui illustre une animation de type "vue" contrôlé par un geste de glissement et renforcé par des retours haptiques.

Animation d'un cercle faisant glisser vers le bas
Tracé 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)

Deux primitives permettent d'augmenter l'intensité des vibrations perçues : PRIMITIVE_QUICK_RISE et PRIMITIVE_SLOW_RISE. Elles atteignent le même objectif, mais avec des durées différentes. Il n'y en a qu'une primitive pour la rétrogradation, PRIMITIVE_QUICK_FALL Ces primitives fonctionnent mieux ensemble pour créer un segment de forme d'onde qui s'étend puis s'éteint. Vous pouvez aligner les primitives mises à l'échelle pour éviter les sauts d'amplitude entre eux, ce qui est idéal pour étendre la portée globale la durée de l'effet. Perceptuellement, les gens remarquent toujours cette partie qui monte plus que la partie descendante, donc en rendant la partie montante plus courte que la partie pour accentuer la partie descendante.

Voici un exemple d'application de cette composition pour l'expansion et réduire un cercle. L'effet de rehaussement peut accroître la sensation d'expansion pendant l'animation. La combinaison des effets de hausse et de chute aide à mettre en évidence qui se réduit à la fin de l'animation.

Animation d'un cercle qui s'agrandit
Tracé 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)

L'un des principes clés du retour haptique consiste à satisfaire les utilisateurs. Une façon amusante pour introduire un effet de vibration agréable et inattendu, utilisez la PRIMITIVE_SPIN Cette primitive est plus efficace lorsqu'elle est appelée plusieurs fois. Multiples les rotations concaténées peuvent créer un effet oscillant et instable, qui peut être encore amélioré en appliquant une mise à l'échelle assez aléatoire à chaque primitive. Toi peut également tester l'écart entre les primitives de rotation successives. Deux tours sans espace (0 ms entre les deux) crée une sensation de virage serré. En hausse l'intervalle entre les rotations (de 10 à 50 ms) entraîne une sensation de rotation plus lâche. peut être utilisée pour 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 requêtes successives les rotations ne s'intègrent plus correctement et commencent à ressembler à des effets individuels.

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

Animation d'un rebond de forme élastique
Tracé 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 des cristaux)

Une autre application avancée des effets de vibration est la simulation des interactions. La PRIMITIVE_THUD vous pouvez créer un effet puissant et résonnant, qui peuvent être associés la visualisation d'un impact, dans une vidéo ou une animation, par exemple, pour enrichir une expérience globale.

Voici un exemple d'animation simple "goutte de balle" améliorée avec un effet stupéfiant joué à chaque fois que la balle rebondit depuis le bas de l'écran:

Animation d&#39;une balle abandonnée qui rebondit par le bas de l&#39;écran
Tracé 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;
          }
        }
      });
  }
}