Questa pagina illustra gli esempi di come utilizzare diverse API aptiche per creare effetti personalizzati oltre alle forme d'onda di vibrazione standard in un'app per Android.
Questa pagina include i seguenti esempi:
- Modelli di vibrazione personalizzati
- Pattern di aumento graduale: un pattern che inizia in modo graduale.
- Pattern ripetuto: un pattern senza fine.
- Pattern con elemento di riserva: una dimostrazione dell'elemento di riserva.
- Composizioni 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 della vibrazione con inviluppi
- Molla rimbalzante: un effetto di rimbalzo elastico che utilizza effetti di base di busta.
- 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 un effetto personalizzato, tieni presente quanto segue:
- Quali funzionalità del dispositivo sono necessarie per l'effetto
- Cosa fare quando il dispositivo non è in grado di riprodurre l'effetto
Il riferimento all'API Android Haptics fornisce dettagli su come verificare il supporto dei componenti coinvolti nell'aptica, in modo che la tua app possa offrire un'esperienza complessiva coerente.
A seconda del caso d'uso, potresti voler disattivare gli effetti personalizzati o fornire effetti personalizzati alternativi in base a diverse potenziali funzionalità.
Pianifica le seguenti classi di funzionalità del dispositivo di alto livello:
Se utilizzi primitive aptiche: i dispositivi che supportano queste primitive necessarie per gli effetti personalizzati. (Per maggiori dettagli sui primitivi, vedi la sezione successiva.)
Dispositivi con controllo dell'ampiezza.
Dispositivi con supporto della vibrazione di base (on/off), ovvero quelli privi di controllo dell'ampiezza.
Se la scelta dell'effetto aptico dell'app tiene conto di queste categorie, l'esperienza utente aptica dovrebbe rimanere prevedibile per qualsiasi singolo dispositivo.
Utilizzo di primitive aptiche
Android include diverse primitive aptiche che variano sia in ampiezza che in 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 per un rapporto di 1,4 o più in modo che la differenza di intensità sia percepita meglio.
Utilizza le scale 0,5, 0,7 e 1,0 per creare una versione a bassa, media e alta intensità di una primitiva.
Creare pattern di vibrazione personalizzati
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 target potrebbe anche produrre ronzii udibili. Progetta pattern di forma d'onda per attenuare 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:
- Timings: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 dell'indice di ripetizione negativo.
Kotlin
val timings: LongArray = longArrayOf(
50, 50, 50, 50, 50, 100, 350, 25, 25, 25, 25, 200)
val amplitudes: IntArray = intArrayOf(
33, 51, 75, 113, 170, 255, 0, 38, 62, 100, 160, 255)
val repeatIndex = -1 // Don't repeat.
vibrator.vibrate(VibrationEffect.createWaveform(
timings, amplitudes, repeatIndex))
Java
long[] timings = new long[] {
50, 50, 50, 50, 50, 100, 350, 25, 25, 25, 25, 200 };
int[] amplitudes = new int[] {
33, 51, 75, 113, 170, 255, 0, 38, 62, 100, 160, 255 };
int repeatIndex = -1; // Don't repeat.
vibrator.vibrate(VibrationEffect.createWaveform(
timings, amplitudes, repeatIndex));
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 una
forma d'onda ripetuta, la vibrazione continua finché non viene annullata esplicitamente nel
servizio:
Kotlin
void startVibrating() {
val timings: LongArray = longArrayOf(50, 50, 100, 50, 50)
val amplitudes: IntArray = intArrayOf(64, 128, 255, 128, 64)
val repeat = 1 // Repeat from the second entry, index = 1.
VibrationEffect repeatingEffect = VibrationEffect.createWaveform(
timings, amplitudes, repeat)
// repeatingEffect can be used in multiple places.
vibrator.vibrate(repeatingEffect)
}
void stopVibrating() {
vibrator.cancel()
}
Java
void startVibrating() {
long[] timings = new long[] { 50, 50, 100, 50, 50 };
int[] amplitudes = new int[] { 64, 128, 255, 128, 64 };
int repeat = 1; // Repeat from the second entry, index = 1.
VibrationEffect repeatingEffect = VibrationEffect.createWaveform(
timings, amplitudes, repeat);
// repeatingEffect can be used in multiple places.
vibrator.vibrate(repeatingEffect);
}
void stopVibrating() {
vibrator.cancel();
}
Questa funzionalità è 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));
}
Creare composizioni di vibrazione
Questa sezione presenta modi per comporre vibrazioni in effetti personalizzati più lunghi e complessi e va oltre, esplorando l'aptica avanzata utilizzando funzionalità hardware più avanzate. Puoi utilizzare combinazioni di effetti che variano in ampiezza e frequenza per creare effetti aptici più complessi sui dispositivi con attuatori aptici con una larghezza di banda di frequenza più ampia.
La procedura per creare pattern di vibrazione personalizzati, descritta in precedenza in questa pagina, spiega come controllare l'ampiezza della vibrazione per creare effetti fluidi di aumento e diminuzione. La tecnologia aptica avanzata migliora questo concetto esplorando la gamma di frequenze più ampia del vibratore del dispositivo per rendere l'effetto ancora più fluido. Queste forme d'onda sono particolarmente efficaci per creare un effetto crescendo o diminuendo.
Le primitive di composizione, descritte in precedenza in questa pagina, sono implementate dal produttore del dispositivo. Forniscono una vibrazione nitida, breve e piacevole in linea con i principi aptici per un'aptica chiara. Per saperne di più su queste funzionalità e sul loro funzionamento, consulta Introduzione agli attuatori di vibrazione.
Android non fornisce fallback per le composizioni con primitive non supportate. Pertanto, segui questi passaggi:
Prima di attivare la risposta aptica avanzata, 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.
Ulteriori informazioni su come verificare il supporto del dispositivo sono riportate nelle sezioni seguenti.
Creare effetti di vibrazione composti
Puoi creare effetti di vibrazione composti con
VibrationEffect.Composition. Ecco un esempio di effetto di aumento lento
seguito da un effetto di clic deciso:
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 (appena) percepita dall'utente.
Creare varianti nelle primitive di vibrazione
Se vuoi creare una versione debole e una forte dello stesso primitivo, crea rapporti di forza pari o superiori a 1,4, in modo che la differenza di intensità possa essere facilmente percepita. 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.
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());
Controllare 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 selezionare 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 vibrazione, 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 la tecnologia aptica.
Figura 1. Questa forma d'onda rappresenta l'accelerazione di 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 di 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 traballante e instabile, 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
stretta. 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 di 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 caduta della palla 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 di 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 pattern di vibrazione personalizzati 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 busta. 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 busta 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 di \( 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 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 una vibrazione di 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 frequenza-uscita-accelerazione (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 rimbalzante
Un esempio precedente utilizza PRIMITIVE_THUD per simulare le interazioni di rimbalzo
fisico. L'API Basic Envelope offre un controllo
molto più preciso, consentendoti di personalizzare con precisione l'intensità e la nitidezza della vibrazione.
In questo modo, il feedback aptico segue in modo più preciso 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 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 desired 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'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 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 desired 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()
)
}