Questa pagina illustra esempi di come utilizzare diverse API aptiche per creare effetti personalizzati oltre alle forme d'onda di vibrazione standard in un'app Android.
Questa pagina include i seguenti esempi:
- Composizioni ancorate alla cronologia con
VibrationEffect.Builder- Componi con i preset: sequenzia sensazioni aptiche predefinite.
- Componi con inviluppi e preset: Combina inviluppi e preset lungo una sequenza temporale.
- Riutilizzare e spostare gli eventi: Sposta e riutilizza gli eventi di composizione esistenti.
- Composizioni ripetute: crea effetti della sequenza temporale ripetuti.
- Sequenze di vibrazione personalizzate
- Pattern di aumento: un pattern che inizia in modo graduale.
- Pattern ripetuto: un pattern senza fine.
- Pattern con fallback: una dimostrazione del fallback.
- Composizioni di primitive di vibrazione
- Resistenza: un effetto di trascinamento con intensità dinamica.
- Espandi: un effetto di aumento e poi diminuzione.
- Oscillazione: un effetto di oscillazione che utilizza la primitiva
SPIN. - Rimbalzo: un effetto di rimbalzo che utilizza la primitiva
THUD.
- Forma d'onda di 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, vedi Aggiungere il feedback aptico agli eventi e segui sempre i principi di progettazione aptica.
Utilizzare i fallback per gestire la compatibilità dei dispositivi
Quando implementi effetti aptici personalizzati, la compatibilità del dispositivo e il comportamento di fallback dipendono dalla superficie 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 unPreseto unEnvelopedi base richiesto, il framework lo traduce automaticamente in un'alternativa appropriata al momento della riproduzione con il massimo impegno. Non è necessario controllare manualmente le funzionalità di ogni dispositivo prima di riprodurre gli effetti composti conVibrationEffect.Builder.- Eccezione:le buste di forma d'onda avanzate create con
WaveformEnvelopeBuildernon supportano il fallback automatico perché dipendono da mappature di frequenza hardware specifiche (FOAM). Se non sono supportati, non verranno riprodotti.
- Eccezione:le buste di forma d'onda avanzate create con
VibrationEffect.Composition: le composizioni create con l'APIstartComposition()non hanno fallback automatici. Se una composizione contiene anche un solo elemento primitivo non supportato, 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 al
100% sui dispositivi che non dispongono del controllo dell'ampiezza. Controlla
vibrator.hasAmplitudeControl()e torna a un pattern ON/OFF progettato in modo esplicito, se necessario.
Utilizzo di primitive aptiche
Android include diversi primitive feedback aptici che variano in ampiezza e frequenza. Puoi utilizzare una sola primitiva o più primitive in combinazione per ottenere effetti aptici avanzati.
- Utilizza ritardi di 50 ms o più per creare 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 le scale 0,5, 0,7 e 1,0 per creare una versione a bassa, media e alta intensità di una primitiva.
Creare sequenze di vibrazione personalizzate
I pattern di vibrazione vengono spesso utilizzati nell'aptica attentiva, ad esempio per notifiche
e suonerie. Il servizio Vibrator può riprodurre pattern di vibrazione lunghi
che cambiano 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 riprodotte in un ambiente silenzioso. L'aumento troppo rapido dell'ampiezza potrebbe anche produrre ronzii udibili. Progetta pattern di forme d'onda per uniformare le transizioni di ampiezza per creare effetti di aumento e diminuzione.
Esempi di pattern di vibrazione
Le sezioni seguenti forniscono diversi esempi di pattern di vibrazione:
Pattern di applicazione graduale
Le forme d'onda sono rappresentate come VibrationEffect con tre parametri:
- Timing:un array di durate, in millisecondi, per ogni segmento della forma d'onda.
- Ampiezze: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 spegnimento" 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 le pulsazioni. Il primo impulso è un aumento graduale fino all'ampiezza massima, mentre il secondo è un aumento rapido per mantenere l'ampiezza massima. L'arresto alla fine è definito dal valore negativo dell'indice di ripetizione.
Kotlin
val timings: LongArray = longArrayOf(
50, 50, 50, 50, 50, 100, 350, 25, 25, 25, 25, 200)
val amplitudes: IntArray = intArrayOf(
33, 51, 75, 113, 170, 255, 0, 38, 62, 100, 160, 255)
val repeatIndex = -1 // 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));
Pattern ripetitivo
Le forme d'onda possono anche essere riprodotte ripetutamente fino all'annullamento. Il modo per creare una
forma d'onda ripetuta è impostare un parametro repeat non negativo. Quando riproduci un'onda
ripetitiva, 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();
}
Ciò è molto utile per gli eventi intermittenti che richiedono l'intervento dell'utente per confermarli. Esempi di questi eventi includono chiamate in arrivo e allarmi attivati.
Pattern con fallback
Il controllo dell'ampiezza di una vibrazione è una funzionalità dipendente dall'hardware. La riproduzione di una forma d'onda su un dispositivo di fascia bassa senza questa funzionalità fa vibrare il dispositivo alla massima ampiezza 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 vibrazione quando viene riprodotto in queste condizioni oppure progetta un pattern ON/OFF più semplice che possa essere riprodotto come fallback.
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));
}
Composizioni ancorate alla sequenza temporale con VibrationEffect.Builder
A partire da Android 16 (26Q4), VibrationEffect.Builder è l'API preferita per creare effetti e composizioni di vibrazione complessi. Consente
di creare sensazioni aptiche espressive sequenziando elementi aptici discreti
lungo una sequenza temporale assoluta con startTimeMillis.
VibrationEffect.Builder supporta la combinazione di più tipi di vibrazione:
- Preset:impulsi aptici predefiniti (
VibrationEffect.Preset) come clic e segni di spunta. - Inviluppi:forme d'onda continue dinamiche, inclusi inviluppi di base indipendenti dall'hardware (
BasicEnvelopeBuilder) e inviluppi avanzati modulati in 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 in tutti i loro elementi (inclusi preset, inviluppi di base ed effetti di vibrazione combinati), garantendo un'esperienza utente coerente su diversi dispositivi senza richiedere controlli manuali delle funzionalità. (Le buste di forma d'onda avanzate create con
WaveformEnvelopeBuilder richiedono il supporto hardware e non hanno il supporto
di fallback automatico.)
Comporre con i preset
Utilizza VibrationEffect.Preset per aggiungere impulsi aptici brevi, comuni e predefiniti
(come PRESET_CLICK, PRESET_TICK o PRESET_LOW_TICK) a una composizione.
I preset vengono implementati dal produttore del dispositivo per fornire una vibrazione nitida, breve e piacevole in linea con i principi aptici per un'aptica chiara. Per ulteriori dettagli su queste funzionalità e sul loro funzionamento, consulta
Introduzione agli attuatori di vibrazione.
I preset sostituiscono le primitive brevi dell'API VibrationEffect.Composition.
Per sensazioni aptiche più lunghe o continue, come l'aumento e la diminuzione graduali, utilizza
invece le forme d'onda envelope (PWLE).
A ogni preset può essere assegnata una scala facoltativa compresa tra 0.0f e 1.0f e viene
posizionato a 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);
Comporre con inviluppi e preset
Puoi combinare senza problemi le istanze VibrationEffect.Envelope (create
utilizzando BasicEnvelopeBuilder o WaveformEnvelopeBuilder) con
preset per creare pattern aptici ricchi e multi-segmento.
Ecco un esempio che riproduce un inviluppo di aumento e dissolvenza graduali seguito da un preset di clic nitido:
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);
Riutilizzare e spostare 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
VibrationEffect.Composition istanze in questo modo, il framework converte automaticamente
le relative primitive in preset, consentendo il supporto di fallback in fase 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);
Creare 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 deriva
Quando progetti composizioni con VibrationEffect.Builder, tieni presente le seguenti
regole di temporizzazione e convalida:
- Orari di inizio strettamente crescenti:ogni elemento aggiunto allo strumento di creazione deve
avere un
startTimeMillisstrettamente maggiore o uguale all'orario di inizio dell'elemento precedente. - Convalida in fase di compilazione:il builder esegue la convalida al meglio
in fase di
build()utilizzando durate degli elementi note (o un minimo di 1 ms per i preset). Se viene rilevata una sovrapposizione impossibile, viene generato unIllegalArgumentException. - Spostamento sequenziale della riproduzione:se un elemento di vibrazione precedente è ancora in riproduzione fisica quando arriva l'ora di inizio dell'elemento successivo, il framework sposta automaticamente l'elemento successivo nel primo intervallo di tempo disponibile. In questo modo, gli eventi non si sovrappongono e non vengono eliminate vibrazioni, anche se potrebbe verificarsi una leggera deriva della sincronizzazione se gli eventi sono programmati troppo vicini tra loro. Per ridurre al minimo la deriva, lascia un tempo sufficiente (ad es. 50 ms o più) tra eventi aptici consecutivi.
Crea composizioni di primitive di vibrazione
Questa sezione mostra come comporre vibrazioni utilizzando
VibrationEffect.Composition. Le primitive di composizione, descritte
in precedenza in questa pagina, vengono implementate dal produttore del dispositivo. Forniscono una
vibrazione nitida, breve e piacevole in linea con i principi aptici
per un feedback aptico chiaro. Per maggiori dettagli su queste funzionalità e sul loro funzionamento,
consulta Introduzione agli attuatori di vibrazione.
A differenza di VibrationEffect.Builder, l'API VibrationEffect.Composition
non dispone di fallback automatici per i primitivi non supportati. Quindi:
Prima di attivare il feedback aptico avanzato, verifica che un determinato dispositivo supporti tutti i primitivi che stai utilizzando.
Disattiva il set coerente di esperienze non supportate, non solo gli effetti a cui manca 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()))
Se costruito con VibrationEffect.Builder, il framework converte i
primitivi 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 effetto
in lento aumento seguito da un effetto di 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 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 a un'ampiezza minima alla quale questa primitiva può essere percepita (appena) dall'utente.
Creare varianti nelle primitive di vibrazione
Se vuoi creare una versione debole e una forte dello stesso esercizio primitivo, crea rapporti di forza pari o superiori a 1,4, in modo che la differenza di intensità possa essere facilmente percepita. Non tentare di 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.
Aggiungere spazi tra le primitive di vibrazione
La composizione può anche specificare ritardi da aggiungere tra primitive consecutive. Questo ritardo è espresso in millisecondi dalla fine della primitiva precedente. In generale, un intervallo di 5-10 ms tra due primitive è troppo breve per essere rilevabile. Utilizza un intervallo 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());
Controlla quali primitive sono supportate
Le seguenti API possono essere utilizzate per verificare il supporto del dispositivo per primitive specifiche:
Kotlin
val primitive = VibrationEffect.Composition.PRIMITIVE_LOW_TICK
if (vibrator.areAllPrimitivesSupported(primitive)) {
vibrator.vibrate(VibrationEffect.startComposition()
.addPrimitive(primitive).compose())
} else {
// Play a predefined effect or custom pattern as a fallback.
}
Java
int primitive = VibrationEffect.Composition.PRIMITIVE_LOW_TICK;
if (vibrator.areAllPrimitivesSupported(primitive)) {
vibrator.vibrate(VibrationEffect.startComposition()
.addPrimitive(primitive).compose());
} else {
// Play a predefined effect or custom pattern as a fallback.
}
È anche possibile controllare più primitive e poi 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 vibrazione
Le sezioni seguenti forniscono diversi esempi di composizioni di vibrazioni, tratti dall'app di esempio Haptics su GitHub.
Resistenza (con pochi segni)
Puoi controllare l'ampiezza della vibrazione primitiva per fornire 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 della visualizzazione controllata da un gesto di trascinamento e arricchita con feedback aptico.
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 salita e discesa)
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 cresce di intensità e poi si attenua.
Puoi allineare le primitive scalate per evitare salti improvvisi di ampiezza tra
loro, il che funziona bene anche per estendere la durata complessiva dell'effetto.
A livello percettivo, le persone notano sempre di più la parte in aumento rispetto a quella in diminuzione, quindi rendere la parte in aumento più breve di quella in diminuzione può essere utilizzato per spostare l'enfasi sulla parte in diminuzione.
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 degli effetti di salita e discesa contribuisce a enfatizzare il collasso alla fine dell'animazione.
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 è quello di deliziare gli utenti. Un modo divertente per
introdurre un piacevole effetto di vibrazione inatteso è utilizzare
PRIMITIVE_SPIN. Questa primitiva è più efficace quando viene chiamata più
di una volta. La concatenazione di più rotazioni può creare un effetto di oscillazione e instabilità, che può essere ulteriormente migliorato applicando una scalatura un po' casuale a ogni primitiva. Puoi anche sperimentare con lo spazio tra le primitive di rotazione
successive. Due rotazioni senza intervallo (0 ms tra una rotazione 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, in quanto le rotazioni successive non si integrano più bene e iniziano a sembrare effetti individuali.
Ecco un esempio di forma elastica che rimbalza dopo essere stata trascinata verso il basso e poi rilasciata. L'animazione è migliorata con una coppia di effetti di rotazione, riprodotti con intensità variabili proporzionali allo spostamento del rimbalzo.
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 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 sordo riprodotto ogni volta che la palla rimbalza sul fondo dello schermo:
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;
}
}
});
}
}
Forma d'onda di vibrazione con inviluppi
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 aptici dinamici utilizzando inviluppi di forma d'onda che consentono un controllo preciso dell'ampiezza e della frequenza della vibrazione nel tempo. In questo modo puoi creare esperienze aptiche 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 di vibrazione definendo una sequenza di punti di controllo:
BasicEnvelopeBuilder: un approccio accessibile per la creazione di effetti aptici indipendenti dall'hardware.WaveformEnvelopeBuilder: un approccio più avanzato alla creazione di effetti aptici; richiede familiarità con l'hardware aptico.
Android non fornisce fallback per gli effetti di inviluppo. Se hai bisogno di questo supporto, completa i seguenti passaggi:
- Controlla se un determinato dispositivo supporta gli effetti busta utilizzando
Vibrator.areEnvelopeEffectsSupported(). - Disattiva il set 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ù nitida.
Un valore di durata, che rappresenta il tempo, in millisecondi, impiegato per la transizione 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 un tono basso a un tono alto, con vibrazione alla massima intensità per 500 ms, per poi diminuire di nuovo fino a\( 0 \) (off) per 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 dell'aptica, puoi definire gli effetti di inviluppo
utilizzando WaveformEnvelopeBuilder. Quando utilizzi questo oggetto, puoi accedere alla
mappatura dell'accelerazione frequenza-output (FOAM) tramite
VibratorFrequencyProfile.
- Un valore di ampiezza nell'intervallo \( [0, 1] \), che rappresenta la forza di vibrazione raggiungibile a una determinata frequenza, come determinato dalla schiuma del dispositivo. Ad esempio, un valore di \( 0.5 \) genera la metà dell'accelerazione dell'output massima che può essere raggiunta alla frequenza specificata.
Un valore di frequenza, specificato in Hertz.
Un valore di durata, che rappresenta il tempo, in millisecondi, impiegato per la transizione dall'ultimo punto di controllo a quello nuovo.
Il seguente codice mostra un esempio di forma d'onda 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 aumenta fino a 120 Hz nei successivi 100 ms e rimane a questo livello per 200 ms. Infine, l'ampiezza diminuisce fino 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 Basic Envelope offre un controllo molto più preciso, consentendoti di personalizzare con esattezza l'intensità e la nitidezza della vibrazione.
In questo modo, il feedback aptico segue in modo più accurato gli eventi animati.
Ecco un esempio di molla in caduta libera con l'animazione migliorata con un effetto di inviluppo di base riprodotto ogni volta che la molla rimbalza sul fondo 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() {
var springX by remember { mutableStateOf(SPRING_WIDTH) }
var springY by remember { mutableStateOf(SPRING_HEIGHT) }
var velocityX by remember { mutableFloatStateOf(INITIAL_VELOCITY) }
var velocityY by remember { mutableFloatStateOf(INITIAL_VELOCITY) }
var sharpness by remember { mutableFloatStateOf(INITIAL_SHARPNESS) }
var intensity by remember { mutableFloatStateOf(INITIAL_INTENSITY) }
var multiplier by remember { mutableFloatStateOf(INITIAL_MULTIPLIER) }
var bottomBounceCount by remember { mutableIntStateOf(0) }
var animationStartTime by remember { mutableLongStateOf(0L) }
var isAnimating by remember { mutableStateOf(false) }
val (screenHeight, screenWidth) = getScreenDimensions(context)
LaunchedEffect(isAnimating) {
animationStartTime = System.currentTimeMillis()
isAnimating = true
while (isAnimating) {
velocityY += GRAVITY
springX += velocityX.dp
springY += velocityY.dp
// Handle bottom collision
if (springY > screenHeight - FLOOR_HEIGHT - SPRING_HEIGHT / 2) {
// Set the spring's y-position to the bottom bounce point, to keep it
// above the floor.
springY = screenHeight - FLOOR_HEIGHT - SPRING_HEIGHT / 2
// Reverse the vertical velocity and apply damping to simulate a bounce.
velocityY *= -BOUNCE_DAMPING
bottomBounceCount++
// Calculate the fade-out duration of the vibration based on the
// vertical velocity.
val fadeOutDuration =
((abs(velocityY) / GRAVITY) * FRAME_DELAY_MS).toLong()
// Create a "boing" envelope vibration effect that fades out.
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(intensity, sharpness, 20L)
// Add a control point to fade out the vibration intensity while
// maintaining sharpness.
.addControlPoint(0f, sharpness, fadeOutDuration)
.build()
)
// Decrease the intensity and sharpness of the vibration for subsequent
// bounces, and reduce the multiplier to create a fading effect.
intensity *= multiplier
sharpness *= multiplier
multiplier -= 0.1f
}
if (springX > screenWidth - SPRING_WIDTH / 2) {
// Prevent the spring from moving beyond the right edge of the screen.
springX = screenWidth - SPRING_WIDTH / 2
}
// Check for 3 bottom bounces and then slow down.
if (bottomBounceCount >= MAX_BOTTOM_BOUNCE &&
System.currentTimeMillis() - animationStartTime > 1000) {
velocityX *= 0.9f
velocityY *= 0.9f
}
delay(FRAME_DELAY_MS) // Control animation speed.
// Determine if the animation should continue based on the spring's
// position and velocity.
isAnimating = (springY < screenHeight + SPRING_HEIGHT ||
springX < screenWidth + SPRING_WIDTH)
&& (velocityX >= 0.1f || velocityY >= 0.1f)
}
}
Box(
modifier = Modifier
.fillMaxSize()
.noRippleClickable {
if (!isAnimating) {
resetAnimation()
}
}
.width(screenWidth)
.height(screenHeight)
) {
DrawSpring(mutableStateOf(springX), mutableStateOf(springY))
DrawFloor()
if (!isAnimating) {
DrawText("Tap to restart")
}
}
}
Lancio di un razzo
Un esempio precedente mostra come utilizzare l'API envelope di base per
simulare la reazione di una molla elastica. Il WaveformEnvelopeBuilder sblocca
il controllo preciso dell'intera gamma di frequenze del dispositivo, consentendo effetti aptici altamente
personalizzati. Combinando questi dati con quelli di FOAM, puoi personalizzare
le vibrazioni in base a capacità 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'accelerazione minima supportata della frequenza di uscita, 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 scende di nuovo al minimo, che viene percepito come intensità e nitidezza decrescenti. In questo modo si crea una sensazione di resistenza iniziale seguita da un rilascio, che simula un lancio nello spazio.
Questo effetto non è possibile con l'API envelope di base, in quanto astrae le informazioni specifiche del dispositivo sulla sua frequenza di risonanza e sulla curva di accelerazione di uscita. L'aumento della nitidezza può spingere la frequenza equivalente oltre la risonanza, causando potenzialmente un calo di accelerazione involontario.
Figura 6. Un grafico della forma d'onda dell'accelerazione di output per una vibrazione che simula il lancio di un razzo.
@Composable
fun RocketLaunchAnimation() {
val context = LocalContext.current
val screenHeight = remember { mutableFloatStateOf(0f) }
var rocketPositionY by remember { mutableFloatStateOf(0f) }
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)
)
) {
rocketPositionY = screenHeight.floatValue * value
}
animation.snapTo(0f)
rocketPositionY = 0f;
isLaunched = false;
}
}
Box(
modifier = Modifier
.fillMaxSize()
.noRippleClickable {
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, 0.7f)
isLaunched = true
}
}
.background(Color(context.getColor(R.color.background)))
.onSizeChanged { screenHeight.floatValue = it.height.toFloat() }
) {
drawRocket(rocketPositionY)
}
}
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 - rampUpDuration - 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 di Lancio di un razzo, l'API WaveformEnvelopeBuilder
consente di progettare molti effetti aptici 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
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 temporale. Il
primo impulso caratteristico è il complesso QRS, che si presenta come un picco acuto di
ampiezza elevata e breve durata. Il secondo impulso è l'onda T, che ha
un'ampiezza inferiore, una durata maggiore e una forma più regolare (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 acustico che inizia a una frequenza bassa e termina a una frequenza più alta
in 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 aptico che
assomiglia a un battito cardiaco.
Puoi provare LavaBeats nella nostra app di esempio di feedback aptico su GitHub e sentire l'effetto accompagnato da una visualizzazione di una lampada di lava che batte 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
@RequiresApi(Build.VERSION_CODES.BAKLAVA)
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 */
@Stable
data class BeatParameter(
val description: String = "",
val value: Float = 0f,
val range: ClosedFloatingPointRange<Float> = 0f..1f,
val steps: Int = 0,
val isFrequencyType: Boolean = false,
)