Crea effetti aptici personalizzati

Questa pagina illustra esempi di come utilizzare diverse API di aptica per creare effetti personalizzati oltre alle forme d'onda di vibrazione standard in un'app Android.

Questa pagina include i seguenti esempi:

Per altri esempi, vedi Aggiungere il feedback aptico agli eventi e segui sempre i principi di progettazione aptica.

Utilizzare i valori di riserva per gestire la compatibilità dei dispositivi

Quando implementi un effetto personalizzato, tieni presente quanto segue:

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

La pagina di riferimento dell'API Android Haptics fornisce dettagli su come verificare il supporto dei componenti coinvolti nella tecnologia aptica, in modo che la tua app possa offrire un'esperienza complessiva coerente.

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

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

  • Se utilizzi primitive aptica: i dispositivi che supportano queste primitive necessarie per gli effetti personalizzati. (Per informazioni dettagliate sulle primitive, consulta la sezione successiva).

  • Dispositivi con controllo dell'ampiezza.

  • Dispositivi con supporto di base della vibrazione (on/off), in altre parole quelli che non dispongono del controllo dell'ampiezza.

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

Utilizzo di primitive aptica

Android include diverse primitive di aptica che variano sia in termini di ampiezza che di frequenza. Puoi utilizzare una primitiva da sola o più primitive in combinazione per ottenere effetti tattili avanzati.

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

Creare pattern di vibrazione personalizzati

Gli schemi di vibrazione vengono spesso utilizzati nel feedback aptico per attirare l'attenzione, ad esempio per le notifiche e i suoni di chiamata. Il servizio Vibrator può riprodurre pattern di vibrazione lunghi che modificano l'ampiezza della vibrazione nel tempo. Questi effetti sono chiamati forme d'onda.

Gli effetti di forma d'onda sono generalmente percepibili, ma vibrazioni lunghe e improvvise possono spaventare l'utente se riprodotti in un ambiente silenzioso. Anche l'aumento troppo rapido dell'ampiezza target può produrre ronzii udibili. Progetta pattern di forme d'onda per uniformare le transizioni di ampiezza e creare effetti di aumento e diminuzione.

Esempi di pattern di vibrazione

Le seguenti sezioni forniscono diversi esempi di pattern di vibrazione:

Schema 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 della forma d'onda.
  2. Amplitudini: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 lo "stato off" del vibratore e 255 è l'ampiezza massima del dispositivo.
  3. Indice di ripetizione:l'indice nell'array specificato nel primo argomento per iniziare a ripetere la forma d'onda o -1 se il pattern deve essere riprodotto una sola 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 è una rampa graduale fino all'ampiezza massima, mentre il secondo è una rampa rapida per mantenere l'ampiezza massima. L'interruzione alla fine è definita dal 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 // Don't 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; // Don't repeat.

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

Schema ripetitivo

Le forme d'onda possono anche essere riprodotte ripetutamente finché non vengono annullate. Per creare un'oscillazione ripetuta, imposta un parametro repeat non negativo. Quando riproduci una forma d'onda ripetuta, la vibrazione continua finché 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();
}

Questa opzione è molto utile per gli eventi intermittenti che richiedono un'azione dell'utente per confermarli. Esempi di questi eventi includono chiamate in arrivo e allarmi attivati.

Pattern con elemento di riserva

Il controllo dell'ampiezza di una vibrazione è una funzionalità dipendente dall'hardware. La riproduzione di un'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, utilizza un pattern che non generi un effetto di ronzio quando viene riprodotto in queste condizioni oppure progetta un pattern ON/OFF più semplice che possa essere riprodotto come opzione di 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));
}

Creare composizioni di vibrazioni

Questa sezione illustra i modi per comporre le vibrazioni in effetti personalizzati più lunghi e complessi e va oltre per esplorare le funzionalità di aptica avanzate utilizzando funzionalità hardware più avanzate. Puoi utilizzare combinazioni di effetti che variano l'ampiezza e la frequenza per creare effetti aptico più complessi su dispositivi con attuatori aptico che hanno una larghezza di banda di frequenza più ampia.

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

Le primitive di composizione, descritte in precedenza in questa pagina, vengono implementate dal produttore del dispositivo. Offrono una vibrazione chiara, breve e piacevole in linea con i principi dell'aptica per un'esperienza aptica chiara. Per ulteriori dettagli su queste funzionalità e sul loro funzionamento, consulta la guida introduttiva agli attuatori vibrazionali.

Android non fornisce valori di riserva per le composizioni con primitive non supportate. Pertanto, svolgi i seguenti passaggi:

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

  2. Disattiva l'insieme coerente di esperienze non supportate, non solo gli effetti a cui manca un elemento primitivo.

Ulteriori informazioni su come verificare il supporto del dispositivo sono riportate nelle seguenti sezioni.

Creare effetti di vibrazione composti

Puoi creare effetti di vibrazione composti con VibrationEffect.Composition. Ecco un esempio di un effetto in graduale aumento 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. Ogni primitivo è inoltre scalabile, quindi puoi controllare l'ampiezza della vibrazione generata da ciascuno. La scala è definita come un valore compreso tra 0 e 1, dove 0 corrisponde a un'ampiezza minima alla quale questa primitiva può essere percepita (a malapena) dall'utente.

Creare varianti nelle primitive di vibrazione

Se vuoi creare una versione debole e una forte della stessa primitiva, crea rapporti di intensità pari o superiori a 1,4, in modo che la differenza di intensità sia facilmente percepibile. Non cercare di creare più di tre livelli di intensità della stessa primitiva, perché non sono distinguibili percettivamente. Ad esempio, utilizza le scale 0,5, 0,7 e 1,0 per creare versioni di intensità bassa, media e alta di una primitiva.

Aggiungere spazi tra le primitive di vibrazione

La composizione può anche specificare i ritardi da aggiungere tra le primitive consecutive. Questo ritardo è espresso in millisecondi dalla fine del primitivo precedente. In genere, un intervallo di 5-10 ms tra due primitive è troppo breve per essere rilevato. Utilizza un intervallo dell'ordine di 50 ms o più se vuoi creare un intervallo 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());

Controllare quali primitive sono supportate

Per verificare il supporto del dispositivo per primitive specifiche, puoi utilizzare le seguenti API:

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

Esempi di composizioni di vibrazioni

Le sezioni seguenti forniscono diversi esempi di composizioni di vibrazioni, tratte dall'app di esempio per l'aptica su GitHub.

Resistenza (con pochi tick)

Puoi controllare l'ampiezza della vibrazione primitiva per trasmettere un feedback utile su un'azione in corso. I valori di scala con spaziatura ravvicinata possono essere utilizzati per creare un effetto di crescendo graduale di una primitiva. Anche il ritardo tra primitivi consecutivi può essere impostato dinamicamente in base all'interazione dell'utente. Questo è illustrato nell'esempio seguente di un'animazione della 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 in ingresso.

Figura 1. Questa forma d'onda rappresenta l'accelerazione in uscita della vibrazione su un dispositivo.

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

Espandi (con aumento e calo)

Esistono due primitive per aumentare l'intensità della vibrazione percepita: PRIMITIVE_QUICK_RISE e PRIMITIVE_SLOW_RISE. Entrambi raggiungono lo stesso target, ma con durate diverse. Esiste una sola primitiva per la riduzione graduale, PRIMITIVE_QUICK_FALL. Queste primitive funzionano meglio insieme per creare un segmento di forma d'onda che aumenta di intensità e poi si attenua. Puoi allineare le primitive scalate per evitare salti improvvisi di ampiezza tra di loro, il che funziona bene anche per estendere la durata complessiva dell'effetto. Percettivamente, le persone notano sempre di più la parte in aumento rispetto alla parte in calo, quindi rendere la parte in aumento più breve di quella in calo può essere utilizzata per spostare l'attenzione sulla parte in calo.

Ecco un esempio di applicazione di questa composizione per espandere e comprimere un cerchio. L'effetto di aumento può migliorare la sensazione di espansione durante l'animazione. La combinazione di effetti di aumento e diminuzione contribuisce a sottolineare il collapse alla fine dell'animazione.

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

Figura 2. Questa forma d'onda rappresenta l'accelerazione in uscita della vibrazione su un dispositivo.

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

Oscillazione (con rotazioni)

Uno dei principi fondamentali dell'aptica è soddisfare gli utenti. Un modo divertente per inserire un effetto di vibrazione piacevole e inaspettato è utilizzare PRIMITIVE_SPIN. Questa primitiva è più efficace se viene chiamata più volte. Più rotazioni concatenate possono creare un effetto tremante e instabile, che può essere ulteriormente migliorato applicando una scalabilità un po' casuale a ogni primitiva. Puoi anche sperimentare con l'intervallo tra le primitive di rotazione successive. Due rotazioni senza spazi (0 ms tra una e l'altra) creano una sensazione di rotazione rapida. Aumentare l'intervallo tra una rotazione e l'altra da 10 a 50 ms genera una sensazione di rotazione più lenta e può essere utilizzato per adattarsi alla durata di un video o di un'animazione.

Non utilizzare un intervallo superiore a 100 ms, poiché le rotazioni successive 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 poi rilasciata. L'animazione è migliorata con una coppia di effetti di rotazione, riprodotti con intensità diverse proporzionali allo spostamento del rimbalzo.

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

Figura 3. Questa forma d'onda rappresenta l'accelerazione in uscita della vibrazione su un dispositivo.

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 the range [-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 the range [-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)
    }
}

Rimbalzo (con colpi sordi)

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

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

Animazione di una palla che cade e rimbalza sulla parte inferiore dello schermo.
Grafico della forma d&#39;onda della vibrazione in ingresso.

Figura 4. Questa forma d'onda rappresenta l'accelerazione in uscita della vibrazione su un dispositivo.

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