Questa pagina illustra esempi di come utilizzare diverse API di tecnologia aptica per creare effetti personalizzati oltre alle forme d'onda di vibrazione standard in un'app per Android.
Questa pagina include i seguenti esempi:
- Composizioni ancorate alla sequenza temporale con
VibrationEffect.Builder- Componi con preset: sequenza di sensazioni tattili predefinite.
- Componi con envelope e preset: combina envelope e preset lungo una sequenza temporale.
- Riutilizza e sposta gli eventi: sposta e riutilizza gli eventi di composizione esistenti.
- Composizioni ripetute: crea effetti di cronologia ripetuti.
- Modelli di vibrazione personalizzati
- Pattern di rampa: un pattern che inizia in modo graduale.
- Pattern ripetuto: un pattern senza fine.
- Pattern con riserva: una dimostrazione di riserva.
- Composizioni di primitive di vibrazione
- Resistenza: un effetto di trascinamento con intensità dinamica.
- Espandi: un effetto di aumento seguito da una diminuzione.
- Wobble: un effetto di oscillazione che utilizza la primitiva
SPIN. - Bounce: un effetto di rimbalzo che utilizza la primitiva
THUD.
- Forma d'onda della vibrazione con inviluppi
- Molla che rimbalza: un effetto di rimbalzo elastico che utilizza effetti di inviluppo di base.
- Lancio di un razzo: un effetto di lancio di un razzo che utilizza effetti di inviluppo della forma d'onda.
Per altri esempi, consulta Aggiungi il feedback aptico agli eventi e segui sempre i principi di progettazione aptica.
Utilizza i fallback per gestire la compatibilità dei dispositivi
Quando implementi effetti tattili personalizzati, la compatibilità del dispositivo e il comportamento di fallback dipendono dalla piattaforma API che scegli:
VibrationEffect.Builder(consigliato): a partire da Android 16 (26Q4), gli effetti creati utilizzandoVibrationEffect.Builderincludono il fallback automatico a livello di framework. Se un dispositivo non supporta in modo nativo unPresetrichiesto o unEnvelopedi base, il framework lo traduce automaticamente in un'alternativa appropriata al momento della riproduzione con il criterio del "best effort". Non è necessario controllare manualmente le funzionalità del dispositivo per ogni primitiva prima di riprodurre gli effetti composti conVibrationEffect.Builder.- Eccezione: gli envelope di forme d'onda avanzate creati con
WaveformEnvelopeBuildernon supportano il fallback automatico perché dipendono da mappature di frequenza hardware specifiche (FOAM). Se non sono supportati, non verranno riprodotti.
- Eccezione: gli envelope di forme d'onda avanzate creati con
VibrationEffect.Composition: le composizioni create con l'APIstartComposition()non hanno fallback automatici. Se una composizione contiene anche una sola primitiva non supportata, l'intera vibrazione non viene riprodotta. Devi controllare manualmente le funzionalità utilizzandovibrator.arePrimitivesSupported().- Forme d'onda con controllo dell'ampiezza:le ampiezze diverse da zero vengono arrotondate per eccesso al 100% sui dispositivi privi di controllo dell'ampiezza. Controlla
vibrator.hasAmplitudeControl()e, se necessario, utilizza un pattern ON/OFF progettato in modo esplicito.
Utilizzo di primitive aptiche
Android include diverse primitive di feedback aptico che variano sia in ampiezza che in frequenza. Puoi utilizzare una sola primitiva 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 di un rapporto di 1,4 o più, in modo che la differenza di intensità sia percepita meglio.
Utilizza scale di 0,5, 0,7 e 1,0 per creare una versione a bassa, media e alta intensità di una primitiva.
Crea sequenze di vibrazione personalizzate
I pattern di vibrazione vengono spesso utilizzati nella tecnologia aptica di attenzione, ad esempio per notifiche e suonerie. 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 della forma d'onda sono solitamente percepibili, ma vibrazioni lunghe e improvvise possono spaventare l'utente se riprodotte in un ambiente silenzioso. Anche l'aumento troppo rapido dell'ampiezza target potrebbe produrre ronzii udibili. Progetta pattern di forme d'onda per attenuare le transizioni di ampiezza e creare effetti di aumento e diminuzione.
Esempi di sequenze di vibrazione
Le sezioni seguenti forniscono diversi esempi di schemi di vibrazione:
Pattern di applicazione graduale
Le forme d'onda sono rappresentate come VibrationEffect con tre parametri:
- Timings: un array di durate, in millisecondi, per ogni segmento della forma d'onda.
- Amplitudes: l'ampiezza di vibrazione desiderata per ogni durata specificata nel primo argomento, rappresentata da un valore intero compreso tra 0 e 255, dove 0 rappresenta lo "stato di disattivazione" del vibratore e 255 l'ampiezza massima del dispositivo.
- 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 gli impulsi. Il primo impulso è un aumento graduale fino all'ampiezza massima, mentre il secondo è un aumento rapido per mantenere l'ampiezza massima. L'interruzione alla fine è definita dal valore negativo dell'indice di ripetizione.
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 ) )
Pattern ripetuto
Le forme d'onda possono anche essere riprodotte ripetutamente fino a quando non vengono annullate. Per creare una forma d'onda ripetuta, imposta un parametro repeat non negativo. Quando riproduci una forma d'onda ripetuta, la vibrazione continua finché non viene annullata esplicitamente nel servizio:
fun 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. val repeatingEffect: VibrationEffect = VibrationEffect.createWaveform( timings, amplitudes, repeat ) // repeatingEffect can be used in multiple places. vibrator.vibrate(repeatingEffect) } fun stopVibrating() { vibrator.cancel() }
Questa funzionalità è molto utile per gli eventi intermittenti che richiedono l'azione dell'utente per essere confermati. Esempi di questi eventi sono le chiamate in arrivo e le sveglie attivate.
Pattern con riserva
Il controllo dell'ampiezza di una vibrazione è una funzionalità che dipende dall'hardware. La riproduzione di una forma d'onda su un dispositivo di fascia bassa senza questa funzionalità fa vibrare il dispositivo con l'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 questa condizione o progetta un pattern ON/OFF più semplice che possa essere riprodotto come alternativa.
if (vibrator.hasAmplitudeControl()) { vibrator.vibrate( VibrationEffect.createWaveform( smoothTimings, amplitudes, smoothRepeatIdx ) ) } else { vibrator.vibrate( VibrationEffect.createWaveform( onOffTimings, onOffRepeatIdx ) ) }
Composizioni ancorate alla sequenza temporale con VibrationEffect.Builder
A partire da Android 16 (26Q4), VibrationEffect.Builder è l'API preferita per la creazione di composizioni ed effetti di vibrazione complessi. Ti consente di creare sensazioni aptiche espressive sequenziando elementi aptici discreti lungo una cronologia assoluta con startTimeMillis.
VibrationEffect.Builder supporta la combinazione di più tipi di vibrazione:
- Preset:impulsi aptici predefiniti (
VibrationEffect.Preset) come clic e tic. - Inviluppi:forme d'onda continue dinamiche, inclusi inviluppi di base indipendenti dall'hardware (
BasicEnvelopeBuilder) e inviluppi avanzati con modulazione di frequenza (WaveformEnvelopeBuilder). - VibrationEffects esistenti:composizioni primitive (
VibrationEffect.Composition), forme d'onda a gradini (VibrationEffect.createWaveform), effetti predefiniti (VibrationEffect.createPredefined) e one-shot (VibrationEffect.createOneShot) importati utilizzandoaddEvents()o la copia del costruttore. - Sequenze ripetute:pattern continui configurati con
setRepeatingEffect().
Le vibrazioni create con VibrationEffect.Builder hanno un fallback automatico integrato a livello di framework per tutti i loro elementi (tra cui preset, inviluppi di base ed effetti di vibrazione combinati), garantendo un'esperienza utente coerente su diversi dispositivi senza richiedere controlli manuali delle funzionalità. (Gli envelope di forme d'onda avanzati creati con
WaveformEnvelopeBuilder richiedono il supporto hardware e non hanno il supporto
di fallback automatico.)
Componi con i preset
Utilizza VibrationEffect.Preset per aggiungere a una composizione brevi impulsi aptici comuni e predefiniti (come PRESET_CLICK, PRESET_TICK o PRESET_LOW_TICK).
I preset vengono implementati dal produttore del dispositivo per fornire una vibrazione nitida, breve e piacevole che si allinea ai principi relativi alla tecnologia aptica per un'esperienza aptica chiara. Per maggiori dettagli su queste funzionalità e sul loro funzionamento, consulta la guida introduttiva agli attuatori di vibrazione.
I preset sostituiscono le primitive brevi dell'API VibrationEffect.Composition.
Per sensazioni tattili più lunghe o continue, come l'aumento e la diminuzione, utilizza invece le forme d'onda di inviluppo (PWLE).
A ogni preset può essere assegnata una scala facoltativa compresa tra 0.0f e 1.0f, che viene posizionata in un'ora di inizio esplicita (in millisecondi) dall'inizio della composizione.
Kotlin
val clickPreset = VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_CLICK, /* scale= */ 0.8f
)
val tickPreset = VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_TICK, /* scale= */ 0.5f
)
val effect = VibrationEffect.Builder()
.addPreset(/* startTimeMillis= */ 0L, clickPreset)
.addPreset(/* startTimeMillis= */ 100L, tickPreset)
.build()
vibrator.vibrate(effect)
Java
VibrationEffect.Preset clickPreset = VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_CLICK, /* scale= */ 0.8f
);
VibrationEffect.Preset tickPreset = VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_TICK, /* scale= */ 0.5f
);
VibrationEffect effect = new VibrationEffect.Builder()
.addPreset(/* startTimeMillis= */ 0L, clickPreset)
.addPreset(/* startTimeMillis= */ 100L, tickPreset)
.build();
vibrator.vibrate(effect);
Componi con inviluppi e preset
Puoi combinare facilmente le istanze VibrationEffect.Envelope (create utilizzando BasicEnvelopeBuilder o WaveformEnvelopeBuilder) con i preset per creare pattern tattili ricchi e multisegmento.
Ecco un esempio che riproduce un inviluppo con aumento e dissolvenza graduali, seguito da un preset di clic netto:
Kotlin
val basicEnvelope = VibrationEffect.Envelope.create(
VibrationEffect.BasicEnvelopeBuilder()
.setInitialSharpness(0.0f)
.addControlPoint(1.0f, 1.0f, 300L)
.addControlPoint(0.0f, 0.5f, 100L)
)
val clickPreset = VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_CLICK, 0.9f
)
val mixedEffect = VibrationEffect.Builder()
.addEnvelope(/* startTimeMillis= */ 0L, basicEnvelope)
.addPreset(/* startTimeMillis= */ 450L, clickPreset)
.build()
vibrator.vibrate(mixedEffect)
Java
VibrationEffect.Envelope basicEnvelope = VibrationEffect.Envelope.create(
new VibrationEffect.BasicEnvelopeBuilder()
.setInitialSharpness(0.0f)
.addControlPoint(1.0f, 1.0f, 300L)
.addControlPoint(0.0f, 0.5f, 100L)
);
VibrationEffect.Preset clickPreset = VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_CLICK, 0.9f
);
VibrationEffect mixedEffect = new VibrationEffect.Builder()
.addEnvelope(/* startTimeMillis= */ 0L, basicEnvelope)
.addPreset(/* startTimeMillis= */ 450L, clickPreset)
.build();
vibrator.vibrate(mixedEffect);
Riutilizza e sposta gli eventi esistenti
Per riutilizzare o concatenare un VibrationEffect esistente (incluso
VibrationEffect.Composition), recupera il relativo elenco di
oggetti VibrationEffect.Event utilizzando getEvents() e aggiungili con
un offset utilizzando addEvents(startTimeShiftMillis, events) (o passa l'effetto
direttamente al costruttore VibrationEffect.Builder(effect)). Quando importi le istanze VibrationEffect.Composition in questo modo, il framework converte automaticamente le relative primitive in preset, abilitando il supporto del fallback di runtime.
Kotlin
val existingEffect = VibrationEffect.Builder()
.addPreset(
0L,
VibrationEffect.Preset.create(VibrationEffect.Preset.PRESET_CLICK)
)
.addPreset(
80L,
VibrationEffect.Preset.create(VibrationEffect.Preset.PRESET_TICK)
)
.build()
// Shift and append the existing events 200ms into the new composition.
val combinedEffect = VibrationEffect.Builder()
.addEvents(/* startTimeShiftMillis= */ 200L, existingEffect.events)
.build()
vibrator.vibrate(combinedEffect)
Java
VibrationEffect existingEffect = new VibrationEffect.Builder()
.addPreset(
0L,
VibrationEffect.Preset.create(VibrationEffect.Preset.PRESET_CLICK)
)
.addPreset(
80L,
VibrationEffect.Preset.create(VibrationEffect.Preset.PRESET_TICK)
)
.build();
// Shift and append the existing events 200ms into the new composition.
VibrationEffect combinedEffect = new VibrationEffect.Builder()
.addEvents(/* startTimeShiftMillis= */ 200L, existingEffect.getEvents())
.build();
vibrator.vibrate(combinedEffect);
Crea composizioni ripetute
Utilizza setRepeatingEffect(startTimeMillis, repeatingEffect, durationMillis) per aggiungere un pattern ripetuto a una composizione:
Kotlin
val repeatingPattern = VibrationEffect.Builder()
.addPreset(
0L,
VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_CLICK, 1.0f
)
)
.addPreset(
150L,
VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_LOW_TICK, 0.6f
)
)
.build()
val repeatingEffect = VibrationEffect.Builder()
.setRepeatingEffect(
/* startTimeMillis= */ 0L,
/* effect= */ repeatingPattern,
/* durationMillis= */ 300L
)
.build()
vibrator.vibrate(repeatingEffect)
Java
VibrationEffect repeatingPattern = new VibrationEffect.Builder()
.addPreset(
0L,
VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_CLICK, 1.0f
)
)
.addPreset(
150L,
VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_LOW_TICK, 0.6f
)
)
.build();
VibrationEffect repeatingEffect = new VibrationEffect.Builder()
.setRepeatingEffect(
/* startTimeMillis= */ 0L,
/* effect= */ repeatingPattern,
/* durationMillis= */ 300L
)
.build();
vibrator.vibrate(repeatingEffect);
Tempistica, convalida e gestione della deviazione
Quando progetti composizioni con VibrationEffect.Builder, tieni presente le seguenti regole di tempistica e convalida:
- Orari di inizio in ordine rigorosamente crescente:ogni elemento aggiunto al builder deve avere un
startTimeMillisrigorosamente maggiore o uguale all'orario di inizio dell'elemento precedente. - Convalida in fase di creazione:il builder esegue la convalida best-effort al momento della creazione di
build()utilizzando le durate degli elementi note (o un minimo di 1 ms per i preset). Se viene rilevata una sovrapposizione impossibile, viene generato un erroreIllegalArgumentException. - Spostamento sequenziale della riproduzione:se un elemento di vibrazione precedente è ancora in fase di riproduzione fisica quando arriva l'ora di inizio dell'elemento successivo, il framework sposta automaticamente l'elemento successivo nella prima fascia oraria disponibile. In questo modo, gli eventi non si sovrappongono e non vengono perse vibrazioni, anche se potrebbe verificarsi una leggera deviazione temporale se gli eventi sono programmati troppo vicini tra loro. Per ridurre al minimo la deviazione, lascia un tempo sufficiente (ad es. 50 ms o più) tra eventi aptici consecutivi.
Crea composizioni di primitive di vibrazione
Questa sezione illustra come comporre le vibrazioni utilizzando VibrationEffect.Composition. Le primitive di composizione, descritte in precedenza in questa pagina, sono implementate dal produttore del dispositivo. Forniscono una vibrazione nitida, breve e piacevole che si allinea ai principi relativi alla tecnologia aptica per un'esperienza aptica chiara. Per maggiori dettagli su queste funzionalità e sul loro funzionamento, consulta la guida introduttiva agli attuatori di vibrazione.
A differenza di VibrationEffect.Builder, l'API VibrationEffect.Composition non dispone di fallback automatici per le primitive non supportate. Quindi:
Prima di attivare l'aptica avanzata, verifica che un determinato dispositivo supporti tutte le primitive che stai utilizzando.
Disattiva l'insieme coerente di esperienze non supportate, non solo gli effetti privi di una primitiva.
- Kotlin:
val fallbackEffect = VibrationEffect.Builder(compositionEffect).build()(o.addEvents(0L, compositionEffect.events)) - Java:
VibrationEffect fallbackEffect =new VibrationEffect.Builder(compositionEffect).build();(o.addEvents(0L, compositionEffect.getEvents()))
Quando viene creato con VibrationEffect.Builder, il framework converte le primitive di composizione in preset e fornisce automaticamente un fallback di runtime se una primitiva non è supportata sul dispositivo dell'utente.
Effetti di vibrazione composti
Puoi creare effetti di vibrazione composti con
VibrationEffect.Composition. Ecco un esempio di un effetto di aumento lento seguito da un effetto di clic netto:
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 primitiva è anche scalabile, quindi puoi controllare l'ampiezza della vibrazione generata da ciascuna. La scala è definita come un valore compreso tra 0 e 1, dove 0 corrisponde in realtà a un'ampiezza minima alla quale questa primitiva può essere (appena) percepita dall'utente.
Crea 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à possa essere percepita facilmente. Non provare a creare più di tre livelli di intensità della stessa primitiva, perché non sono percettivamente distinti. Ad esempio, utilizza scale di 0,5, 0,7 e 1,0 per creare versioni a bassa, media e alta intensità di una primitiva.
Aggiungi intervalli tra le primitive di vibrazione
La composizione può anche specificare i ritardi da aggiungere tra primitive consecutive. Questo ritardo è espresso in millisecondi dalla fine della primitiva precedente. In genere, un intervallo di 5-10 ms tra due primitive è troppo breve per essere rilevabile. Utilizza un intervallo di circa 50 ms o più se vuoi creare un intervallo distinguibile tra due primitive. Ecco un esempio di composizione con ritardi:
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() )
Controlla quali primitive sono supportate
Le seguenti API possono essere utilizzate per verificare il supporto del dispositivo per primitive specifiche:
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. }
È anche possibile controllare più primitive e poi decidere quali comporre in base al livello di supporto del dispositivo:
val primitives: IntArray = intArrayOf( VibrationEffect.Composition.PRIMITIVE_LOW_TICK, VibrationEffect.Composition.PRIMITIVE_TICK, VibrationEffect.Composition.PRIMITIVE_CLICK ) val supported: BooleanArray = vibrator.arePrimitivesSupported(*primitives)
Esempi di composizioni di vibrazioni
Le sezioni seguenti forniscono diversi esempi di composizioni di vibrazione, tratti dall'app di esempio di feedback aptico su GitHub.
Resistenza (con tick bassi)
Puoi controllare l'ampiezza della vibrazione primitiva per trasmettere un feedback utile a un'azione in corso. I valori di scala ravvicinati possono essere utilizzati per creare un effetto di crescendo uniforme di una primitiva. Il ritardo tra primitive consecutive può anche essere impostato dinamicamente in base all'interazione dell'utente. Questo è illustrato nel seguente esempio di animazione di una visualizzazione controllata da un gesto di trascinamento e migliorata con l'aptica.
Figura 1. Questa forma d'onda rappresenta l'accelerazione di output della vibrazione su un dispositivo.
@Composable fun ResistScreen() { // Control variables for the dragging of the indicator. var isDragging by remember { mutableStateOf(false) } var dragOffset by remember { mutableFloatStateOf(0f) } // Only vibrates while the user is dragging LaunchedEffect(isDragging) { // Continuously run the effect for vibration to occur even when the view // is not being drawn, when user stops dragging midway through gesture. while (isDragging) { // 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() ) } } 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) } }
Espandi (con aumento e diminuzione)
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, 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 sbalzi improvvisi di ampiezza tra di loro, il che funziona bene anche per estendere la durata complessiva dell'effetto.
Dal punto di vista percettivo, le persone notano sempre di più la parte ascendente rispetto a quella discendente, quindi rendere la parte ascendente più breve di quella discendente può essere utilizzato per spostare l'enfasi verso la parte discendente.
Ecco un esempio di applicazione di questa composizione per espandere e comprimere un cerchio. L'effetto di sollevamento può migliorare la sensazione di espansione durante l'animazione. La combinazione di effetti di ascesa e discesa aiuta a enfatizzare il collasso alla fine dell'animazione.
Figura 2.Questa forma d'onda rappresenta l'accelerazione di output della vibrazione su un dispositivo.
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) Column( Modifier .clickable( onClick = { 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) } }
Wobble (con rotazioni)
Uno dei principi chiave della tecnologia aptica è soddisfare gli utenti. Un modo divertente per introdurre un piacevole effetto di vibrazione inaspettato è utilizzare PRIMITIVE_SPIN. Questa primitiva è più efficace quando viene chiamata più di una volta. Più rotazioni concatenate possono creare un effetto oscillante e instabile, che può essere ulteriormente migliorato applicando una scalabilità un po' casuale su ogni primitiva. Puoi anche sperimentare con l'intervallo tra le primitive di rotazione successive. Due rotazioni senza intervallo (0 ms tra l'una e l'altra) creano una sensazione di rotazione intensa. Aumentando l'intervallo tra le rotazioni da 10 a 50 ms, si ottiene una sensazione di rotazione più lenta, che può essere utilizzata 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 singoli.
Ecco un esempio di forma elastica che rimbalza dopo essere stata trascinata verso il basso e poi rilasciata. L'animazione è migliorata con un paio di effetti di rotazione, riprodotti con intensità variabili proporzionali allo spostamento del rimbalzo.
Figura 3. Questa forma d'onda rappresenta l'accelerazione di output della vibrazione su un dispositivo.
@Composable fun WobbleScreen() { // Control variables for the dragging and animating state of the elastic. var dragDistance by remember { mutableFloatStateOf(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 ), // The spring ends on its target. An end value of 0f means that the // shape is back at rest, so the wobble is over. finishedListener = { endValue -> if (endValue == 0f) isWobbling = false }, ) LaunchedEffect(isWobbling) { while (isWobbling) { 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 = { // Wobble only if the shape is far enough from rest to vibrate. // It then springs back to 0f, and the finishedListener ends // the wobble. val displacement = dragDistanceAnimated / MAX_DRAG_DISTANCE isWobbling = displacement > SPIN_MIN_DISPLACEMENT dragDistance = 0f }, orientation = Orientation.Vertical, state = rememberDraggableState { delta -> isWobbling = false dragDistance += delta } ) ) { // Draw the wobbling shape using the animated spring-like value. The // lambda defers the read to the draw phase, so the animation doesn't // recompose this screen on every frame. 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) }
Rimbalzo (con tonfi)
Un'altra applicazione avanzata degli effetti di vibrazione è la simulazione di interazioni fisiche. PRIMITIVE_THUD può creare un effetto forte e riverberante, 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 animazione di una palla che cade, migliorata con un effetto sonoro di un tonfo riprodotto ogni volta che la palla rimbalza sulla parte inferiore dello schermo:
Figura 4. Questa forma d'onda rappresenta l'accelerazione di output della vibrazione su un dispositivo.
enum class BallPosition { Start, End } @Composable fun BounceScreen() { // Control variable for the state of the ball. var ballPosition by remember { mutableStateOf(BallPosition.Start) } // Plain holder for the bounce count. It's read only in the effect and in // the click handler, never in composition, so it needs no snapshot state. val bounceCount = remember { intArrayOf(0) } // Animation for the bouncing ball. val transitionData = updateTransitionData(ballPosition) val collisionData = updateCollisionData(transitionData) // Ball is about to contact floor. The effect runs only when the collision // state changes, so it vibrates once per collision. Nothing here suspends, // so a keyed SideEffect does the job without launching a coroutine. SideEffect(collisionData.collisionWithFloor) { if (collisionData.collisionWithFloor) { val vibrationScale = 0.7.pow(bounceCount[0]++).toFloat() vibrator.vibrate( VibrationEffect.startComposition().addPrimitive( VibrationEffect.Composition.PRIMITIVE_THUD, vibrationScale ).compose() ) } } Box( Modifier .fillMaxSize() .clickable { if (transitionData.isAtStart) { ballPosition = BallPosition.End } else { ballPosition = BallPosition.Start bounceCount[0] = 0 } }, ) { // Build the ball UI based on the current state. BouncingBall(transitionData) } }
Forma d'onda della vibrazione con envelope
La procedura per creare sequenze di vibrazione personalizzate ti consente di controllare l'ampiezza della vibrazione per creare effetti fluidi di aumento e diminuzione. Questa sezione spiega come creare effetti tattili dinamici utilizzando gli inviluppi delle forme d'onda che consentono un controllo preciso dell'ampiezza e della frequenza della vibrazione nel tempo. In questo modo puoi creare esperienze tattili più ricche e sfumate.
A partire da Android 16 (livello API 36), il sistema fornisce le seguenti API per creare un inviluppo della forma d'onda della vibrazione definendo una sequenza di punti di controllo:
BasicEnvelopeBuilder: un approccio accessibile alla creazione di effetti tattili indipendenti dall'hardware.WaveformEnvelopeBuilder: un approccio più avanzato alla creazione di effetti tattili; richiede familiarità con l'hardware tattile.
Android non fornisce opzioni di riserva per gli effetti di inviluppo. Se hai bisogno di questa assistenza, completa i seguenti passaggi:
- Verifica che un determinato dispositivo supporti gli effetti di inviluppo utilizzando
Vibrator.areEnvelopeEffectsSupported(). - Disattiva l'insieme coerente di esperienze non supportate o utilizza pattern di vibrazione personalizzati o composizioni come alternative di riserva.
Per creare effetti di inviluppo più semplici, utilizza BasicEnvelopeBuilder con questi parametri:
- Un valore di intensità nell'intervallo \( [0, 1] \), che rappresenta la forza percepita della vibrazione. Ad esempio, un valore pari a \( 0.5 \)viene percepito come la metà dell'intensità massima globale che può essere raggiunta dal dispositivo.
Un valore di nitidezza nell'intervallo \( [0, 1] \), che rappresenta la nitidezza della vibrazione. Valori più bassi si traducono in vibrazioni più fluide, mentre valori più alti creano una sensazione più netta.
Un valore duration, che rappresenta il tempo, in millisecondi, impiegato per passare dall'ultimo punto di controllo (ovvero una coppia di intensità e nitidezza) a quello nuovo.
Ecco un esempio di forma d'onda che aumenta l'intensità da una vibrazione a bassa frequenza a una vibrazione ad alta frequenza e massima intensità in 500 ms, per poi tornare a\( 0 \) (off) in 100 ms.
vibrator.vibrate( VibrationEffect.BasicEnvelopeBuilder() .setInitialSharpness(0.0f) .addControlPoint(1.0f, 1.0f, 500) .addControlPoint(0.0f, 1.0f, 100) .build() )
Se hai una conoscenza più avanzata della tecnologia aptica, puoi definire gli effetti di inviluppo utilizzando WaveformEnvelopeBuilder. Quando utilizzi questo oggetto, puoi accedere alla
mappatura della frequenza all'accelerazione di output (FOAM) tramite
VibratorFrequencyProfile.
- Un valore di ampiezza nell'intervallo \( [0, 1] \), che rappresenta l'intensità di vibrazione raggiungibile a una determinata frequenza, come determinato dal FOAM del dispositivo. Ad esempio, un valore pari a \( 0.5 \) genera la metà dell'accelerazione di output massima che può essere ottenuta alla frequenza specificata.
Un valore di frequenza, specificato in Hertz.
Un valore duration, che rappresenta il tempo, in millisecondi, impiegato per passare dall'ultimo punto di controllo a quello nuovo.
Il seguente codice mostra una forma d'onda di esempio che definisce un effetto di vibrazione di 400 ms. Inizia con una rampa di ampiezza di 50 ms, da spento a pieno, a una frequenza costante di 60 Hz. Poi, la frequenza sale a 120 Hz nei successivi 100 ms e rimane a quel livello per 200 ms. Infine, l'ampiezza scende a \( 0 \)e la frequenza torna a 60 Hz negli ultimi 50 ms:
vibrator.vibrate( VibrationEffect.WaveformEnvelopeBuilder() .addControlPoint(1.0f, 60f, 50) .addControlPoint(1.0f, 120f, 100) .addControlPoint(1.0f, 120f, 200) .addControlPoint(0.0f, 60f, 50) .build() )
Le sezioni seguenti forniscono diversi esempi di forme d'onda di vibrazione con inviluppi.
Molla che rimbalza
Un esempio precedente utilizza PRIMITIVE_THUD per simulare interazioni di rimbalzo fisico. L'API di base per l'inviluppo offre un controllo notevolmente più preciso, consentendoti di personalizzare con precisione l'intensità e la nitidezza della vibrazione.
In questo modo, il feedback aptico segue più accuratamente gli eventi animati.
Ecco un esempio di una molla in caduta libera con l'animazione migliorata con un effetto di inviluppo di base riprodotto ogni volta che la molla rimbalza sulla parte inferiore dello schermo:
Figura 5. Un grafico della forma d'onda dell'accelerazione di output per una vibrazione che simula una molla che rimbalza.
@Composable fun BouncingSpringAnimation() { // The spring's height above the floor: 1f is the drop height, 0f is the floor. val springHeight = remember { Animatable(1f) } var isAnimating by remember { mutableStateOf(true) } LaunchedEffect(isAnimating) { if (!isAnimating) return@LaunchedEffect springHeight.snapTo(1f) // Each bounce reaches a fraction of the previous height. var bounceHeight = 1f while (bounceHeight > MIN_BOUNCE_HEIGHT) { // Fall to the floor, speeding up like a falling object. A fall from // a greater height takes longer. val fallDurationMs = (DROP_DURATION_MS * sqrt(bounceHeight)).toInt() springHeight.animateTo(0f, tween(fallDurationMs, easing = EaseInQuad)) // The spring hits the floor: play a "boing" envelope effect that fades // out. A higher bounce gives a stronger and sharper vibration. vibrator.vibrate( VibrationEffect.BasicEnvelopeBuilder() // Starting from zero sharpness here, will simulate a smoother // "boing" effect. .setInitialSharpness(0f) // Add a control point to reach the target intensity and // sharpness very quickly. .addControlPoint(bounceHeight, bounceHeight * MAX_SHARPNESS, 20L) // Add a control point to fade out the vibration intensity while // maintaining sharpness. .addControlPoint(0f, bounceHeight * MAX_SHARPNESS, fallDurationMs.toLong()) .build() ) // Bounce back up to a lower height, slowing down like a thrown object. // When the next bounce is too small, the spring stays on the floor. bounceHeight *= BOUNCE_DAMPING if (bounceHeight > MIN_BOUNCE_HEIGHT) { val riseDurationMs = (DROP_DURATION_MS * sqrt(bounceHeight)).toInt() springHeight.animateTo(bounceHeight, tween(riseDurationMs, easing = EaseOutQuad)) } } isAnimating = false } Box( modifier = Modifier .fillMaxSize() .clickable(enabled = !isAnimating) { isAnimating = true } .drawBehind { // Read the height in the draw phase, and scale it to the distance // between the top of the box and the floor. val floorTop = size.height - FLOOR_HEIGHT.toPx() val dropDistance = floorTop - SPRING_HEIGHT.toPx() drawSpring(topY = dropDistance * (1f - springHeight.value)) drawFloor(top = floorTop) } ) { if (!isAnimating) { Text("Tap to restart", Modifier.align(Alignment.Center)) } } }
Lancio di un razzo
Un esempio precedente mostra come utilizzare l'API di inviluppo di base per simulare una reazione di molla rimbalzante. WaveformEnvelopeBuilder consente un controllo preciso sull'intera gamma di frequenze del dispositivo, permettendo di creare effetti aptici altamente personalizzati. Combinando questi dati con quelli di FOAM, puoi personalizzare le vibrazioni in base a funzionalità di frequenza specifiche.
Ecco un esempio che mostra una simulazione di lancio di un razzo utilizzando un pattern di vibrazione dinamico. L'effetto va dall'output di accelerazione della frequenza minima supportata, 0,1 G, alla frequenza di risonanza, mantenendo sempre un input di ampiezza del 10%. In questo modo, l'effetto inizia con un output ragionevolmente forte e aumenta l'intensità e la nitidezza percepite, anche se l'ampiezza di guida è la stessa. Una volta raggiunta la risonanza, la frequenza dell'effetto torna al minimo, il che viene percepito come un'intensità e una nitidezza decrescenti. Questo crea una sensazione di resistenza iniziale seguita da un rilascio, imitando un lancio nello spazio.
Questo effetto non è possibile con l'API di base per l'inviluppo, in quanto astrae le informazioni specifiche del dispositivo sulla sua frequenza di risonanza e sulla curva di accelerazione dell'output. L'aumento della nitidezza può spingere la frequenza equivalente oltre la risonanza, causando potenzialmente un calo involontario dell'accelerazione.
Figura 6. Un grafico della forma d'onda dell'accelerazione di uscita per una vibrazione che simula il lancio di un razzo.
@Composable fun RocketLaunchAnimation() { var isLaunched by remember { mutableStateOf(false) } val animation = remember { Animatable(0f) } val animationDuration = 3000 LaunchedEffect(isLaunched) { if (isLaunched) { animation.animateTo( 1.2f, // Overshoot so that the rocket goes off the screen. animationSpec = tween( durationMillis = animationDuration, // Applies an easing curve with a slow start and rapid acceleration // towards the end. easing = CubicBezierEasing(1f, 0f, 0.75f, 1f) ) ) animation.snapTo(0f) isLaunched = false } } Canvas( modifier = Modifier .fillMaxSize() .clickable { if (!isLaunched) { // Play vibration with same duration as the animation, using 70% of // the time for the rise of the vibration, to match the easing curve // defined previously. playVibration(vibrator, animationDuration.toLong(), 0.7f) isLaunched = true } } .background(colorResource(R.color.background)) ) { // Read the animation in the draw phase, and scale it to the canvas height. drawRocket(size.height * animation.value) } } private fun playVibration( vibrator: Vibrator, totalDurationMs: Long, riseBias: Float, minOutputAccelerationGs: Float = 0.1f, ) { require(riseBias in 0f..1f) { "Rise bias must be between 0 and 1." } if (!vibrator.areEnvelopeEffectsSupported()) { return } val resonantFrequency = vibrator.resonantFrequency if (resonantFrequency.isNaN()) { // Device doesn't have or expose a resonant frequency. return } val startFrequency = vibrator.frequencyProfile?.getFrequencyRange(minOutputAccelerationGs)?.lower ?: return if (startFrequency >= resonantFrequency) { // Vibrator can't generate the minimum required output at lower frequencies. return } val minDurationMs = vibrator.envelopeEffectInfo.minControlPointDurationMillis val rampUpDurationMs = (riseBias * totalDurationMs).toLong() - minDurationMs val rampDownDurationMs = totalDurationMs - rampUpDurationMs - minDurationMs vibrator.vibrate( VibrationEffect.WaveformEnvelopeBuilder() // Quickly reach the target output at the start frequency .addControlPoint(0.1f, startFrequency, minDurationMs) .addControlPoint(0.1f, resonantFrequency, rampUpDurationMs) .addControlPoint(0.1f, startFrequency, rampDownDurationMs) // Controlled ramp down to zero to avoid ringing after the vibration. .addControlPoint(0.0f, startFrequency, minDurationMs) .build() ) }
LavaBeats
Come nell'esempio Lancio del razzo, l'API WaveformEnvelopeBuilder consente di progettare molti effetti tattili complessi consentendo il controllo dei segmenti di ampiezza e frequenza in una vibrazione. Un altro esempio di questo tipo di design è l'emulazione di sensazioni fisiche più astratte, come la "vivacità".
Ciò può essere ottenuto rappresentando i biomarcatori di un tipico segnale di elettrocardiogramma (ECG) con segmenti di vibrazione di determinate ampiezze e frequenze. LavaBeats è un esempio in cui due segmenti caratteristici di una registrazione ECG sono rappresentati come due impulsi separati da un ritardo. Il primo impulso caratteristico è il complesso QRS, che si presenta come un picco netto di ampiezza elevata e breve durata. Il secondo impulso è l'onda T, che ha un'ampiezza inferiore, una durata maggiore e una forma più uniforme (vedi Figura 7).
Utilizza WaveformEnvelopeBuilder per creare varie ripetizioni di questi due impulsi separati da un ritardo fisso tra il primo e il secondo impulso. Il primo impulso può essere un segnale chirp che inizia a una frequenza bassa e termina a una frequenza più alta per un breve periodo di tempo. Il secondo impulso può essere rappresentato come un singolo periodo di una sinusoide a bassa frequenza. Possiamo comporre i due impulsi in un battito e ripetere la composizione più volte con un ritardo tra un battito e l'altro, seguendo una frequenza tipica di battiti al minuto (bpm). Il risultato è un effetto tattile che ricorda un battito cardiaco.
Puoi provare LavaBeats nella nostra app di esempio di tecnologia aptica su GitHub e sentire l'effetto accompagnato da una visualizzazione di una lampada lava che pulsa allo stesso ritmo dell'effetto aptico. Puoi anche modificare le impostazioni dell'effetto per creare
sensazioni di battito diverse modificando le ampiezze, le frequenze, le durate
e i ritardi dei due impulsi.
Figura 7. Un segmento di una registrazione ECG con il complesso QRS e l'onda T
private fun createEnvelopeEffect( beatParameters: List<BeatParameter> ): VibrationEffect = VibrationEffect.WaveformEnvelopeBuilder() .apply { repeat(beatParameters.getNumBeats()) { // First pulse chirp addControlPoint( beatParameters.getFirstPulseAmplitude(), beatParameters.getFirstPulseStartFreq(), ENVELOPE_RAMP_DURATION_MILLIS, ) addControlPoint( beatParameters.getFirstPulseAmplitude(), beatParameters.getFirstPulseEndFreq(), beatParameters.getFirstPulseDurationMillis().toLong(), ) addControlPoint( 0f, beatParameters.getFirstPulseEndFreq(), ENVELOPE_RAMP_DURATION_MILLIS, ) // Delay between first and second pulse addControlPoint( 0f, beatParameters.getFirstPulseEndFreq(), beatParameters.getFirstToSecondPulseDelayMillis().toLong(), ) // Second pulse addControlPoint( beatParameters.getSecondPulseAmplitude(), beatParameters.getSecondPulseFreq(), ENVELOPE_RAMP_DURATION_MILLIS, ) addControlPoint( beatParameters.getSecondPulseAmplitude(), beatParameters.getSecondPulseFreq(), (1_000 / (2f * beatParameters.getSecondPulseFreq())).toLong(), ) addControlPoint( 0f, beatParameters.getSecondPulseFreq(), ENVELOPE_RAMP_DURATION_MILLIS, ) addControlPoint( 0f, beatParameters.getSecondPulseFreq(), beatParameters.getBeatDelayMillis().toLong(), ) } } .build() /** A parameter of a haptic beat effect that represents an ECG signal parameter */ data class BeatParameter( val description: String = "", val value: Float = 0f, val range: ClosedFloatingPointRange<Float> = 0f..1f, val steps: Int = 0, val isFrequencyType: Boolean = false, )