Crea effetti aptici personalizzati

In questa pagina vengono riportati esempi di utilizzo di diverse API aptica per creare effetti personalizzati in un'applicazione Android. Quante informazioni questa pagina si basa su una buona conoscenza del funzionamento di un attuatore a vibrazione, ti consigliamo di leggere la Guida introduttiva all'attuatore con vibrazione.

Questa pagina include i seguenti esempi.

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

Usare i fallback per gestire la compatibilità dei dispositivi

Quando implementi un effetto personalizzato, considera 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 dell'API Android haptics fornisce dettagli su come verificare la presenza di per i componenti coinvolti nella tecnologia aptica, in modo che l'app possa fornire un'esperienza complessiva coerente.

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

Pianifica le seguenti classi di alto livello di funzionalità dei dispositivi:

  • Se utilizzi i primitivi aptici: i dispositivi che supportano queste primitive necessari per gli effetti personalizzati. (Consulta la prossima sezione per maggiori dettagli primitive).

  • Dispositivi con controllo dell'ampiezza.

  • I dispositivi che supportano la vibrazione di base (on/off), ovvero quelli manca il controllo dell'ampiezza.

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

Utilizzo di primitive aptiche

Android include diversi primitivi aptici che variano sia in ampiezza che in termini di frequenza. Puoi utilizzare una primitiva da sola o più primitive insieme per ottenere effetti aptici avanzati.

  • Utilizza ritardi di 50 ms o più per intervalli distinguibili tra due le primitive, tenendo conto anche della funzione primitiva durata se possibile.
  • Usa scale che differiscono di un rapporto di 1,4 o più, quindi la differenza viene percepita meglio.
  • Usa scale di 0,5, 0,7 e 1,0 per creare un punteggio basso, medio e alto di intensità di una primitiva.

Crea vibrazioni personalizzate

Gli schemi di vibrazione sono spesso utilizzati nella tecnologia aptica attenzionale, come le notifiche e suonerie. Il servizio Vibrator può riprodurre lunghi modelli di vibrazione che modificare l'ampiezza della vibrazione nel tempo. Questi effetti sono denominati forme d'onda.

Gli effetti delle forme d'onda sono facilmente percepibili, ma vibrazioni lunghe e improvvise spaventare l'utente se il gioco viene riprodotto in un ambiente silenzioso. Raggiungere un'ampiezza target troppo veloce potrebbe produrre anche rumori forti. Il consiglio per la progettazione di pattern di forme d'onda consiste nel livellare le transizioni di ampiezza per creare effetti di incremento e declino.

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 forma d'onda. in base al segmento.
  2. Amplitude: l'ampiezza desiderata della vibrazione per ogni durata specificata nel primo argomento, rappresentato da un valore intero compreso tra 0 e 255, con 0 che rappresenta il vibratore "disattivato" e 255 rappresenta il limite massimo di ampiezza.
  3. Ripeti indice: l'indice nell'array specificato nel primo argomento da inizia a ripetere la forma d'onda o -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 lampeggiano. Il primo impulso è una rampa uniforme fino all'ampiezza massima e il il secondo è una rampa veloce per mantenere l'ampiezza massima. L'arresto alla fine è definito per il valore dell'indice di ripetizione negativo.

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 ripetutamente fino all'annullamento. Il modo per creare la forma d'onda ripetuta è impostare un parametro di "ripetizione" non negativo. Quando riproduci un forma d'onda che si ripete, la vibrazione continua fino a quando non viene esplicitamente annullata 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();
}

È molto utile per eventi intermittenti che richiedono un'azione dell'utente per confermalo. Esempi di questi eventi includono le telefonate in arrivo e allarmi attivati.

Esempio: sequenza con riserva

Il controllo dell'ampiezza di una vibrazione è un funzionalità che dipende dall'hardware. Riproduzione di una forma d'onda su dispositivo di fascia bassa senza questa funzione fa vibrare al massimo ampiezza per ogni voce positiva nella matrice di ampiezza. Se la tua app deve compatibili con tali dispositivi, è consigliabile verificare che i tuoi modello non genera un effetto ronzio quando viene riprodotto in quella condizione oppure per progettare una sequenza ON/OFF più semplice che possa essere riprodotta come alternativa.

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 comporli in più lunghi e complessi e va oltre per esplorare i richiami tecnologia aptica utilizzando funzionalità hardware più avanzate. Puoi utilizzare combinazioni di effetti che variano ampiezza e frequenza per creare effetti aptici più complessi Sui dispositivi con attuatori aptici con una larghezza di banda in frequenza più ampia.

La procedura per creare la vibrazione personalizzata pattern, descritti in precedenza in questa pagina, spiega come controllare l'ampiezza della vibrazione per creare effetti fluidi aumentando e diminuendo. La tecnologia aptica avanzata migliora questo concetto esplorando il una gamma più ampia di frequenza della vibrazione del dispositivo per rendere l'effetto ancora più fluido. Queste forme d'onda sono particolarmente efficaci per creare un crescendo o un diminuendo effetto.

I primitivi della composizione descritti in precedenza in questa pagina, sono implementati dai il produttore del dispositivo. Emettono una vibrazione nitida, breve e piacevole in linea con i principi della tecnologia aptica per una tecnologia aptica chiara. Per ulteriori informazioni per maggiori dettagli su queste funzionalità e sul loro funzionamento, consulta Attuatori con vibrazione, di base.

Android non fornisce creatività di riserva per composizioni con elementi non supportati e i primitive. 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 usi.

  2. Disattiva l'insieme coerente di esperienze che non sono supportate, non solo effetti privi di una primitiva. Ulteriori informazioni su come controllare del dispositivo sono mostrate come segue.

Puoi creare effetti di vibrazione composti con VibrationEffect.Composition. Di seguito è riportato un esempio di un effetto che aumenta lentamente, seguito da un effetto clic netto:

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. Ciascuna primitive sono scalabili, quindi puoi controllare l'ampiezza della vibrazione generati da ciascuno di essi. La scala è definita come un valore compreso tra 0 e 1, dove 0 corrisponde effettivamente a un'ampiezza minima alla quale questa primitiva può essere percepito (a malapena) dall'utente.

Se vuoi creare una versione debole e potente della stessa primitiva, consigliato che le scale differiscono di un rapporto di 1,4 o più, quindi la differenza di intensità sia facile da percepire. Non provare a crearne più di tre livelli di intensità della stessa primitiva, perché non sono percepiti distinti. Ad esempio, usare scale di 0, 5, 0, 7 e 1,0 per creare un intervallo di valori e ad alta intensità di una primitiva.

La composizione può anche specificare ritardi da aggiungere tra uno spazio e l'altro e i primitive. Questo ritardo viene espresso in millisecondi dalla fine del precedente. In generale, è sufficiente un intervallo di 5-10 ms tra due primitive siano rilevabili. Prova a utilizzare un intervallo di 50 ms o più 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 seguenti API possono essere usate per verificare il supporto dei dispositivi per :

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 poi decidere quali 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: Resiste (con segni di graduazione bassi)

Puoi controllare l'ampiezza della vibrazione per trasmettere feedback utili per un'azione in corso. I valori di scala più ravvicinati possono essere utilizzato per creare un crescendo fluido di una primitiva. Il ritardo tra Anche i primitivi consecutivi possono essere impostati dinamicamente in base all'utente un'interazione. come illustrato nel seguente esempio di animazione di una visualizzazione controllati da un gesto di trascinamento e potenziati con tecnologia aptica.

Animazione di un cerchio che viene trascinato verso il basso
Traccia della forma d'onda della vibrazione in ingresso

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: Espandi (con aumento e caduta)

Esistono due primitivi per aumentare l'intensità della vibrazione percepita: PRIMITIVE_QUICK_RISE: e PRIMITIVE_SLOW_RISE. Entrambi raggiungono lo stesso target, ma con durate diverse. Ce n'è solo uno di base per la riduzione, PRIMITIVE_QUICK_FALL Queste primitive funzionano meglio insieme per creare un segmento di forma d'onda che cresce un'intensità sufficiente per poi spegnersi. Puoi allineare le primitive scalate per evitare improvvise salti di ampiezza tra loro, il che funziona bene anche per estendere l'ampiezza complessiva durata dell'effetto. Percettivamente, le persone notano sempre la parte in aumento più di la parte in discesa, quindi rendere la parte in aumento più breve di quella in calo per spostare l'enfasi sulla parte in calo.

Ecco un esempio di applicazione di questa composizione per l'espansione e che comprimi un cerchio. L'effetto aumento può migliorare la sensazione di espansione durante l'animazione. La combinazione di questi effetti contribuisce a mettere in risalto il che si comprime alla fine dell'animazione.

Animazione di un cerchio in espansione
Traccia della forma d'onda della vibrazione in ingresso

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: oscillazione (con rotazione)

Uno dei principali principi della tecnologia aptica è soddisfare le esigenze degli utenti. Un modo divertente per introdurre un piacevole effetto di vibrazione inaspettato consiste nell'utilizzare il PRIMITIVE_SPIN Questa primitiva è più efficace quando viene chiamata più di una volta. Più di uno gli spostamenti concatenati possono creare un effetto traballante e instabile, che può essere ulteriormente migliorata applicando una scala piuttosto casuale su ogni primitiva. Tu Puoi anche sperimentare il divario tra i primitivi di rotazione successivi. Due giri senza spazi vuoti (tra 0 ms) crea una forte sensazione di rotazione. In aumento il divario inter-spin da 10 a 50 ms porta a una sensazione di rotazione più lenta e può essere utilizzato per adattare la durata di un video o di un'animazione.

Sconsigliamo di utilizzare un intervallo superiore a 100 ms come gli spin non si integrano più bene e iniziano a sembrare effetti individuali.

Ecco un esempio di forma elastica che torna indietro dopo essere stata trascinata verso il basso e rilasciate. L'animazione viene migliorata con una coppia di effetti di rotazione, riprodotti con intensità variabili proporzionali allo spostamento del rimbalzo.

Animazione di una forma elastica che rimbalza
Traccia della forma d'onda della vibrazione in ingresso

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

Un'altra applicazione avanzata di effetti di vibrazione è quella di simulare le e interazioni. La PRIMITIVE_THUD può creare un effetto forte e riverbero, che può essere abbinato alla la visualizzazione di un impatto, ad esempio in un video o in un'animazione, per in generale.

Ecco un esempio di una semplice animazione di caduta di una palla migliorata con un effetto tonfo giocata ogni volta che la palla rimbalza sul fondo dello schermo:

Animazione di una palla caduta che rimbalza sul fondo dello schermo
Traccia della forma d&#39;onda della vibrazione in ingresso

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