Crea effetti aptici personalizzati

Questa pagina contiene esempi di come utilizzare diverse API aptica per creare effetti personalizzati in un'app per Android. Poiché molte delle informazioni in questa pagina si basano su una buona conoscenza del funzionamento di un attuatore con vibrazione, ti consigliamo di leggere l'articolo Introduzione per attuatori con vibrazione.

Questa pagina include i seguenti esempi.

Per ulteriori esempi, consulta l'articolo Aggiungere feedback aptico agli eventi e segui sempre i principi di progettazione della tecnologia aptica.

Usa i fallback per gestire la compatibilità dei dispositivi

Quando implementi un effetto personalizzato, tieni presente quanto segue:

  • Quali funzionalità del dispositivo sono necessarie per l'effetto
  • Cosa fare quando il dispositivo non è in grado di riprodurre l'effetto

Il riferimento API Android aptics fornisce dettagli su come verificare la presenza di supporto per i componenti coinvolti nella tecnologia aptica, in modo che la tua app possa fornire un'esperienza complessiva coerente.

A seconda del tuo caso d'uso, potresti voler disabilitare gli effetti personalizzati o fornire effetti personalizzati alternativi in base a diverse potenziali funzionalità.

Pianifica le seguenti classi di alto livello di funzionalità del dispositivo:

  • Se usi le primitive aptiche: dispositivi che supportano le primitive necessarie per gli effetti personalizzati. (Vedi la prossima sezione per dettagli sui primitivi).

  • Dispositivi con controllo dell'ampiezza.

  • I dispositivi con supporto della vibrazione di base (on/off), in altre parole, quelli privi del controllo dell'ampiezza.

Se la scelta dell'effetto aptico dell'app tiene conto di queste categorie, l'esperienza utente aptica dovrebbe rimanere prevedibile per ogni singolo dispositivo.

Utilizzo di primitive aptiche

Android include diverse primitive aptiche che variano sia per ampiezza che frequenza. Puoi usare una primitiva da sola o più primitive in combinazione per ottenere effetti aptici avanzati.

  • Utilizza ritardi di almeno 50 ms per intervalli distinguibili tra due primitive, tenendo anche conto della durata primitiva, se possibile.
  • Utilizza scale che differiscono di un rapporto di 1,4 o più in modo che la differenza di intensità sia percepita meglio.
  • Utilizza scale da 0,5, 0,7 e 1,0 per creare una versione a bassa, media e alta intensità di una primitiva.

Crea modelli di vibrazione personalizzati

Le vibrazioni vengono spesso utilizzate nella tecnologia aptica dell'attenzione, come notifiche e suonerie. Il servizio Vibrator può riprodurre lunghi schemi di vibrazione che cambiano l'ampiezza della vibrazione nel tempo. Questi effetti sono chiamati forme d'onda.

Gli effetti della forma d'onda possono essere facilmente percepiti, ma lunghe vibrazioni improvvise possono far spaventare l'utente se vengono riprodotte in un ambiente silenzioso. Anche un aumento troppo rapido dell'ampiezza target potrebbe produrre ronziosi udibili. Il consiglio per progettare modelli di forme d'onda è di attenuare le transizioni di ampiezza per creare effetti con rampa su e giù.

Esempio: modello di applicazione graduale

Le forme d'onda sono rappresentate come VibrationEffect con tre parametri:

  1. Tempi: un array di durate, in millisecondi, per ogni segmento di forma d'onda.
  2. Ampiezzazioni: l'ampiezza della vibrazione desiderata per ogni durata specificata nel primo argomento, rappresentata da un valore intero compreso tra 0 e 255, dove 0 rappresenta la vibrazione "disattivata" e 255 è l'ampiezza massima del dispositivo.
  3. Ripeti l'indice: l'indice nell'array specificato nel primo argomento per iniziare a ripetere la forma d'onda oppure -1 se deve riprodurre il pattern solo una volta.

Ecco un esempio di forma d'onda che pulsa due volte con una pausa di 350 ms tra un impulso e l'altro. Il primo impulso è un'accelerazione graduale fino all'ampiezza massima, mentre il secondo è un'accelerazione rapida per mantenere l'ampiezza massima. L'arresto alla fine è definito dal valore negativo dell'indice di ripetizione.

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

Esempio: sequenza ricorrente

Le forme d'onda possono anche essere riprodotte più volte fino all'annullamento. Il modo per creare una forma d'onda ripetuta è impostare un parametro di ripetizione non negativo. Quando riproduci una forma d'onda ripetuta, la vibrazione continua fino a quando non viene annullata esplicitamente nel servizio:

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

Questo è molto utile per eventi intermittenti che richiedono un'azione dell'utente per confermarli. Esempi di tali eventi sono chiamate in arrivo e allarmi attivati.

Esempio: pattern con fallback

Il controllo dell'ampiezza di una vibrazione è una funzionalità dipendente dall'hardware. La riproduzione di una forma d'onda su un dispositivo di fascia bassa senza questa funzionalità fa vibrare il dispositivo all'ampiezza massima per ogni voce positiva nell'array di ampiezza. Se la tua app deve supportare questi dispositivi, si consiglia di assicurarti che il tuo pattern non generi un effetto di ronzio quando viene riprodotto in quella condizione oppure di progettare un pattern ON/OFF più semplice che possa essere riprodotto come riserva.

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

Crea composizioni con vibrazione

Questa sezione illustra come comporre questi effetti per creare effetti personalizzati più lunghi e complessi e va oltre per esplorare la tecnologia aptica avanzata utilizzando funzionalità hardware più avanzate. Puoi utilizzare combinazioni di effetti che variano in ampiezza e frequenza per creare effetti aptici più complessi sui dispositivi con attuatori aptici che hanno una larghezza di banda di frequenza più ampia.

Il processo per creare modelli di vibrazione personalizzati, descritto in precedenza in questa pagina, spiega come controllare l'ampiezza della vibrazione per creare effetti fluidi di incremento e riduzione. La tecnologia aptica avanzata migliora questo concetto esplorando l'intervallo di frequenza più ampio della vibrazione del dispositivo per rendere l'effetto ancora più fluido. Queste forme d'onda sono particolarmente efficaci nella creazione di un effetto crescendo o diminuendo.

Le primitive di composizione, descritte in precedenza in questa pagina, sono implementate dal produttore del dispositivo. Forniscono una vibrazione nitida, corta e piacevole in linea con i principi della tecnologia aptica per una tecnologia aptica chiara. Per ulteriori dettagli su queste funzionalità e sul loro funzionamento, consulta la pagina Primitore per attuatori con vibrazione.

Android non fornisce elementi di riserva per le composizioni con primitivi non supportati. Ti consigliamo di procedere nel seguente modo:

  1. Prima di attivare la tecnologia aptica avanzata, verifica che un determinato dispositivo supporti tutte le primitive che stai utilizzando.

  2. Disattiva l'insieme coerente di esperienze non supportate, non solo gli effetti per cui manca una primitiva. Di seguito sono riportate ulteriori informazioni su come verificare il supporto del dispositivo.

Puoi creare effetti di vibrazione composti con VibrationEffect.Composition. Ecco un esempio di effetto in lenta crescita seguito da un forte effetto clic:

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

Una composizione viene creata aggiungendo primitive da riprodurre in sequenza. Anche ogni elemento primitivo è scalabile, quindi puoi controllare l'ampiezza della vibrazione generata da ciascuna di esse. La scala è definita come un valore compreso tra 0 e 1, dove 0 corrisponde in realtà a un'ampiezza minima a cui questa primitiva può essere (a malapena) percepita dall'utente.

Se vuoi creare una versione debole ed efficace della stessa primitiva, è consigliabile che le scale differiscano con un rapporto di 1,4 o più, in modo che la differenza di intensità possa essere facilmente percepita. Non cercare di creare più di tre livelli di intensità della stessa primitiva, perché non sono percettivamente distinti. Ad esempio, utilizza scale da 0,5, 0,7 e 1,0 per creare una versione a bassa, media e alta intensità di una primitiva.

La composizione può anche specificare ritardi da aggiungere tra le primitive consecutive. Questo ritardo è espresso in millisecondi dalla fine della primitiva precedente. In generale, un intervallo di 5-10 ms tra due primitive è troppo breve per essere rilevabile. Potresti usare un intervallo di almeno 50 ms se vuoi creare un divario distinguibile tra due primitive. Ecco un esempio di composizione con ritardi:

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

Le API seguenti possono essere utilizzate per verificare il supporto dei dispositivi per primitive specifiche:

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

È anche possibile controllare più primitive e decidere quali comporre in base al livello di supporto del dispositivo:

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

Esempio: Resisti (con graduazioni basse)

Puoi controllare l'ampiezza della vibrazione primitiva per trasmettere un feedback utile a un'azione in corso. Puoi usare valori di scala ravvicinati per creare un effetto crescendo uniforme di una primitiva. Il ritardo tra le primitive consecutive può anche essere impostato dinamicamente in base all'interazione dell'utente. Nell'esempio seguente, questo è illustrato di un'animazione di visualizzazione controllata da un gesto di trascinamento e aumentata con la tecnologia aptica.

Animazione di un cerchio trascinato verso il basso
Grafico della forma d'onda della vibrazione di input

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

Esempio: espansione (con aumento e in diminuzione)

Esistono due primitivi per aumentare l'intensità della vibrazione percepita: PRIMITIVE_QUICK_RISE e PRIMITIVE_SLOW_RISE. Entrambe raggiungono lo stesso target, ma con durate diverse. C'è solo una primitiva per la riduzione, PRIMITIVE_QUICK_FALL. Queste primitive funzionano meglio insieme per creare un segmento di forma d'onda che aumenta di intensità e poi si spegne. Puoi allineare le primitive scalate per evitare salti improvvisi di ampiezza tra di loro, il che funziona anche per estendere la durata complessiva dell'effetto. Percettualmente, le persone notano sempre la parte crescente più che la parte in calo, quindi rendendo la parte crescente più breve di quella in calo può essere utilizzato per spostare l'enfasi verso la parte in calo.

Ecco un esempio di applicazione di questa composizione per espandere e comprimere un cerchio. L'effetto aumento può migliorare la sensazione di espansione durante l'animazione. La combinazione di effetti relativi a rialzo e calo aiuta a enfatizzare il collasso alla fine dell'animazione.

Animazione di un cerchio che si espande
Grafico della forma d'onda della vibrazione di input

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

Esempio: oscilla (con rotazioni)

Uno dei principali principi della tecnologia aptica è la soddisfazione degli utenti. Un modo divertente per introdurre un piacevole effetto di vibrazione imprevisto è utilizzare PRIMITIVE_SPIN. Questa primitiva è più efficace quando viene chiamata più volte. Più rotazione concatenate possono creare un effetto traballante e instabile, che può essere ulteriormente migliorato applicando una scala un po' casuale a ogni primitiva. Puoi anche sperimentare il divario tra le primitive con rotazione successiva. Due giri senza intervalli di 0 ms creano una forte sensazione di rotazione. L'aumento dell'intervallo di rotazione da 10 a 50 ms genera una sensazione di rotazione più allentata e può essere utilizzato per far corrispondere la durata di un video o di un'animazione.

Sconsigliamo di utilizzare un intervallo di tempo superiore a 100 ms, poiché le rotazione successive non si integrano più correttamente e iniziano a sembrare singoli effetti.

Ecco un esempio di forma elastica che si riprende dopo essere stata trascinata verso il basso e poi rilasciata. L'animazione è migliorata con una coppia di effetti di rotazione riprodotti con intensità variabili, proporzionali allo spostamento del rimbalzo.

Animazione di una forma elastica con rimbalzo
Grafico della forma d'onda della vibrazione di input

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

Esempio: Rimbalzo (con pulsazioni)

Un'altra applicazione avanzata degli effetti di vibrazione è simulare le interazioni fisiche. Il PRIMITIVE_THUD può creare un forte effetto di riverbero, che può essere abbinato alla visualizzazione di un impatto, ad esempio in un video o in un'animazione, per migliorare l'esperienza complessiva.

Ecco un esempio di una semplice animazione di lancio della palla migliorata con un effetto tonfo che viene riprodotto ogni volta che la palla rimbalza dalla parte inferiore dello schermo:

Animazione di una palla caduta che rimbalza dalla parte inferiore dello schermo
Grafico della forma d&#39;onda della vibrazione di input

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