Cette page présente des exemples d'utilisation de différentes API haptiques pour créer des effets personnalisés au-delà des formes d'onde de vibration standards dans une application Android.
Cette page inclut les exemples suivants :
- Types de vibration personnalisés
- Modèle d'augmentation : modèle qui commence en douceur.
- Motif répétitif : motif sans fin.
- Schéma avec remplacement : démonstration d'un remplacement.
- Compositions de vibrations
- Résistance : effet de déplacement avec une intensité dynamique.
- Développer : effet de montée puis de descente.
- Oscillation : effet d'oscillation à l'aide du primitif
SPIN. - Rebond : effet de rebond à l'aide de la primitive
THUD.
- Forme d'onde de vibration avec enveloppes
- Ressort rebondissant : effet de ressort rebondissant utilisant des effets d'enveloppe de base.
- Lancement de fusée : effet de lancement de fusée utilisant des effets d'enveloppe de forme d'onde.
Pour obtenir d'autres exemples, consultez Ajouter un retour haptique aux événements et suivez toujours les principes de conception haptique.
Utiliser des solutions de remplacement pour gérer la compatibilité des appareils
Lorsque vous implémentez un effet personnalisé, tenez compte des points suivants :
- Fonctionnalités de l'appareil requises pour l'effet
- Que faire lorsque l'appareil n'est pas capable de lire l'effet
La documentation de référence de l'API haptique Android explique comment vérifier la compatibilité des composants impliqués dans vos retours haptiques, afin que votre application puisse offrir une expérience globale cohérente.
Selon votre cas d'utilisation, vous pouvez désactiver les effets personnalisés ou en proposer d'autres en fonction des différentes capacités potentielles.
Planifiez les classes générales de capacités des appareils suivantes :
Si vous utilisez des primitives haptiques : les appareils compatibles avec les primitives requises par les effets personnalisés. (Pour en savoir plus sur les primitives, consultez la section suivante.)
Appareils dotés d'un contrôle de l'amplitude.
Appareils avec prise en charge des vibrations de base (activées/désactivées), c'est-à-dire ceux qui ne permettent pas de contrôler l'amplitude.
Si le choix de l'effet haptique de votre application tient compte de ces catégories, l'expérience utilisateur haptique devrait rester prévisible pour n'importe quel appareil.
Utilisation de primitives haptiques
Android inclut plusieurs primitives haptiques dont l'amplitude et la fréquence varient. Vous pouvez utiliser une seule primitive ou plusieurs primitives combinées pour obtenir des effets haptiques riches.
- Utilisez des délais de 50 ms ou plus pour créer des espaces visibles entre deux primitives, en tenant également compte de la durée de la primitive si possible.
- Utilisez des échelles dont le rapport est d'au moins 1,4 afin que la différence d'intensité soit mieux perçue.
Utilisez des échelles de 0,5, 0,7 et 1,0 pour créer une version à faible, moyenne et haute intensité d'une primitive.
Créer des schémas de vibration personnalisés
Les schémas de vibration sont souvent utilisés dans les haptiques attentionnelles, comme les notifications et les sonneries. Le service Vibrator peut lire de longs schémas de vibration qui modifient l'amplitude de la vibration au fil du temps. Ces effets sont appelés formes d'onde.
Les effets de forme d'onde sont généralement perceptibles, mais de longues vibrations soudaines peuvent surprendre l'utilisateur si elles sont jouées dans un environnement calme. Si vous augmentez trop rapidement l'amplitude cible, vous risquez également de produire des bourdonnements audibles. Concevez des formes d'onde pour lisser les transitions d'amplitude et créer des effets de montée et de descente.
Exemples de types de vibration
Les sections suivantes fournissent plusieurs exemples de schémas de vibration :
Modèle d'activation progressive
Les formes d'onde sont représentées sous la forme VibrationEffect avec trois paramètres :
- Timings : tableau des durées, en millisecondes, de chaque segment de forme d'onde.
- Amplitudes : amplitude de vibration souhaitée pour chaque durée spécifiée dans le premier argument, représentée par une valeur entière comprise entre 0 et 255, où 0 représente l'état "vibreur désactivé" et 255 l'amplitude maximale de l'appareil.
- Index de répétition : index du tableau spécifié dans le premier argument pour commencer à répéter la forme d'onde, ou -1 si le modèle ne doit être lu qu'une seule fois.
Voici un exemple de forme d'onde qui émet deux impulsions avec une pause de 350 ms entre les deux. La première impulsion est une rampe douce jusqu'à l'amplitude maximale, et la seconde est une rampe rapide pour maintenir l'amplitude maximale. L'arrêt à la fin est défini par la valeur d'index de répétition négative.
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));
Schéma répétitif
Les formes d'onde peuvent également être lues en boucle jusqu'à ce que vous les annuliez. Pour créer une forme d'onde répétée, définissez un paramètre repeat non négatif. Lorsque vous lisez une forme d'onde répétée, la vibration se poursuit jusqu'à ce qu'elle soit explicitement annulée dans le service :
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();
}
Cela est très utile pour les événements intermittents qui nécessitent une action de l'utilisateur pour être reconnus. Il peut s'agir, par exemple, d'appels téléphoniques entrants ou d'alarmes déclenchées.
Modèle avec création de remplacement
Le contrôle de l'amplitude d'une vibration est une fonctionnalité qui dépend du matériel. La lecture d'une forme d'onde sur un appareil d'entrée de gamme sans cette fonctionnalité entraîne la vibration de l'appareil à l'amplitude maximale pour chaque entrée positive du tableau d'amplitude. Si votre application doit s'adapter à de tels appareils, utilisez un schéma qui ne génère pas d'effet de bourdonnement lorsqu'il est lu dans ces conditions, ou concevez un schéma ON/OFF plus simple qui peut être lu en remplacement.
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));
}
Créer des compositions de vibrations
Cette section présente des méthodes pour composer des vibrations en effets personnalisés plus longs et plus complexes, et va au-delà pour explorer les haptiques riches à l'aide de fonctionnalités matérielles plus avancées. Vous pouvez combiner des effets qui font varier l'amplitude et la fréquence pour créer des effets haptiques plus complexes sur les appareils dotés d'actionneurs haptiques ayant une bande passante de fréquence plus large.
La procédure de création de schémas de vibration personnalisés, décrite précédemment sur cette page, explique comment contrôler l'amplitude des vibrations pour créer des effets de montée et de descente en douceur. Les retours haptiques riches améliorent ce concept en explorant la plage de fréquences plus large du vibreur de l'appareil pour rendre l'effet encore plus fluide. Ces formes d'onde sont particulièrement efficaces pour créer un effet crescendo ou diminuendo.
Les primitives de composition, décrites plus haut sur cette page, sont implémentées par le fabricant de l'appareil. Elles fournissent une vibration nette, courte et agréable qui s'aligne sur les principes haptiques pour des retours haptiques clairs. Pour en savoir plus sur ces fonctionnalités et leur fonctionnement, consultez Principes de base des actionneurs de vibration.
Android ne fournit pas de solutions de remplacement pour les compositions avec des primitives non compatibles. Par conséquent, procédez comme suit :
Avant d'activer les retours haptiques avancés, vérifiez qu'un appareil donné est compatible avec toutes les primitives que vous utilisez.
Désactivez l'ensemble cohérent d'expériences non compatibles, et pas seulement les effets pour lesquels il manque une primitive.
Pour savoir comment vérifier la compatibilité de l'appareil, consultez les sections suivantes.
Créer des effets de vibration composés
Vous pouvez créer des effets de vibration composés avec VibrationEffect.Composition. Voici un exemple d'effet de montée lente suivi d'un effet de clic net :
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());
Une composition est créée en ajoutant des primitives à lire en séquence. Chaque primitive est également évolutive, ce qui vous permet de contrôler l'amplitude de la vibration générée par chacune d'elles. L'échelle est définie comme une valeur comprise entre 0 et 1, où 0 correspond en fait à une amplitude minimale à laquelle cette primitive peut être (à peine) ressentie par l'utilisateur.
Créer des variantes dans les primitives de vibration
Si vous souhaitez créer une version faible et une version forte du même primitif, créez des ratios d'intensité de 1, 4 ou plus afin que la différence d'intensité soit facilement perceptible. N'essayez pas de créer plus de trois niveaux d'intensité pour la même primitive, car ils ne sont pas perceptuellement distincts. Par exemple, utilisez des facteurs de 0,5, 0,7 et 1,0 pour créer des versions à faible, moyenne et haute intensité d'une primitive.
Ajouter des pauses entre les primitives de vibration
La composition peut également spécifier des délais à ajouter entre les primitives consécutives. Ce délai est exprimé en millisecondes depuis la fin de la primitive précédente. En général, un écart de 5 à 10 ms entre deux primitives est trop court pour être détectable. Utilisez un intervalle de 50 ms ou plus si vous souhaitez créer un intervalle perceptible entre deux primitives. Voici un exemple de composition avec des délais :
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());
Vérifier les primitives compatibles
Les API suivantes peuvent être utilisées pour vérifier la compatibilité des appareils avec des primitives spécifiques :
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.
}
Il est également possible de vérifier plusieurs primitives, puis de décider lesquelles composer en fonction du niveau de compatibilité de l'appareil :
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);
Exemples de compositions de vibrations
Les sections suivantes fournissent plusieurs exemples de compositions de vibrations, tirés de l'application exemple haptique sur GitHub.
Résistance (avec peu de coches)
Vous pouvez contrôler l'amplitude de la vibration primitive pour transmettre un retour utile à une action en cours. Des valeurs de mise à l'échelle très rapprochées peuvent être utilisées pour créer un effet crescendo fluide d'une primitive. Le délai entre les primitives consécutives peut également être défini de manière dynamique en fonction de l'interaction de l'utilisateur. C'est ce qu'illustre l'exemple suivant d'animation de vue contrôlée par un geste de déplacement et augmentée avec des retours haptiques.
Figure 1. Cette forme d'onde représente l'accélération de sortie de la vibration sur un appareil.
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);
}
}
Développer (avec montée et descente)
Il existe deux primitives pour augmenter l'intensité de vibration perçue : PRIMITIVE_QUICK_RISE et PRIMITIVE_SLOW_RISE. Elles atteignent toutes les deux la même cible, mais avec des durées différentes. Il n'existe qu'une seule primitive pour la réduction, PRIMITIVE_QUICK_FALL. Ces primitives fonctionnent mieux ensemble pour créer un segment de forme d'onde dont l'intensité augmente, puis diminue. Vous pouvez aligner les primitives mises à l'échelle pour éviter les sauts d'amplitude soudains entre elles, ce qui fonctionne également bien pour prolonger la durée globale de l'effet.
Sur le plan perceptuel, les gens remarquent toujours davantage la partie ascendante que la partie descendante. Vous pouvez donc raccourcir la partie ascendante par rapport à la partie descendante pour mettre l'accent sur la partie descendante.
Voici un exemple d'application de cette composition pour développer et réduire un cercle. L'effet d'augmentation peut renforcer la sensation d'expansion pendant l'animation. La combinaison des effets de montée et de descente permet de mettre en évidence l'effondrement à la fin de l'animation.
Figure 2 : cette forme d'onde représente l'accélération de sortie de la vibration sur un appareil.
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;
}
}
Ondulation (avec rotations)
L'un des principes clés de l'haptique est de ravir les utilisateurs. Pour introduire un effet de vibration agréable et inattendu, vous pouvez utiliser PRIMITIVE_SPIN. Cette primitive est plus efficace lorsqu'elle est appelée plusieurs fois. La concaténation de plusieurs rotations peut créer un effet de vacillement et d'instabilité, qui peut être renforcé en appliquant une mise à l'échelle quelque peu aléatoire à chaque primitive. Vous pouvez également tester l'écart entre les primitives de rotation successives. Deux tours sans aucun intervalle (0 ms entre les deux) créent une sensation de rotation intense. Si vous augmentez l'intervalle entre les rotations de 10 à 50 ms, la sensation de rotation sera plus lente. Vous pouvez utiliser cette option pour faire correspondre la durée d'une vidéo ou d'une animation.
N'utilisez pas d'intervalle de plus de 100 ms, car les rotations successives ne s'intègrent plus bien et commencent à ressembler à des effets individuels.
Voici un exemple de forme élastique qui rebondit après avoir été déplacée vers le bas, puis relâchée. L'animation est améliorée par une paire d'effets de rotation, joués avec des intensités variables proportionnelles au déplacement du rebond.
Figure 3. Cette forme d'onde représente l'accélération de sortie de la vibration sur un appareil.
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)
}
}
Rebond (avec des bruits sourds)
Une autre application avancée des effets de vibration consiste à simuler des interactions physiques. PRIMITIVE_THUD peut créer un effet fort et retentissant, qui peut être associé à la visualisation d'un impact, dans une vidéo ou une animation par exemple, pour améliorer l'expérience globale.
Voici un exemple d'animation de chute de balle améliorée avec un effet de bruit sourd joué chaque fois que la balle rebondit en bas de l'écran :
Figure 4. Cette forme d'onde représente l'accélération de sortie de la vibration sur un appareil.
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;
}
}
});
}
}
Forme d'onde de vibration avec enveloppes
La procédure de création de schémas de vibration personnalisés vous permet de contrôler l'amplitude des vibrations pour créer des effets de montée et de descente en douceur. Cette section explique comment créer des effets haptiques dynamiques à l'aide d'enveloppes de forme d'onde qui permettent de contrôler précisément l'amplitude et la fréquence des vibrations au fil du temps. Cela vous permet de créer des expériences haptiques plus riches et plus nuancées.
À partir d'Android 16 (niveau d'API 36), le système fournit les API suivantes pour créer une enveloppe de forme d'onde de vibration en définissant une séquence de points de contrôle :
BasicEnvelopeBuilder: approche accessible pour créer des effets haptiques indépendants du matériel.WaveformEnvelopeBuilder: approche plus avancée pour créer des effets haptiques. Nécessite de connaître le matériel haptique.
Android ne fournit pas de solutions de remplacement pour les effets d'enveloppe. Si vous avez besoin de cette assistance, procédez comme suit :
- Vérifiez si un appareil donné est compatible avec les effets d'enveloppe à l'aide de
Vibrator.areEnvelopeEffectsSupported(). - Désactivez l'ensemble cohérent d'expériences non compatibles, ou utilisez des schémas de vibration personnalisés ou des compositions comme alternatives de secours.
Pour créer des effets d'enveloppe plus basiques, utilisez BasicEnvelopeBuilder avec les paramètres suivants :
- Valeur intensity dans la plage \( [0, 1] \), qui représente l'intensité perçue de la vibration. Par exemple, une valeur de \( 0.5 \)est perçue comme la moitié de l'intensité maximale globale pouvant être atteinte par l'appareil.
Valeur sharpness comprise dans la plage \( [0, 1] \), qui représente la netteté de la vibration. Les valeurs faibles correspondent à des vibrations plus douces, tandis que les valeurs élevées créent une sensation plus vive.
Valeur duration, qui représente le temps, en millisecondes, nécessaire pour passer du dernier point de contrôle (c'est-à-dire une paire intensité/netteté) au nouveau.
Voici un exemple de forme d'onde qui augmente l'intensité d'une vibration de basse à haute fréquence et à intensité maximale sur 500 ms, puis la diminue jusqu'à\( 0 \) (désactivée) sur 100 ms.
vibrator.vibrate(VibrationEffect.BasicEnvelopeBuilder()
.setInitialSharpness(0.0f)
.addControlPoint(1.0f, 1.0f, 500)
.addControlPoint(0.0f, 1.0f, 100)
.build()
)
Si vous avez des connaissances plus avancées sur le haptique, vous pouvez définir des effets d'enveloppe à l'aide de WaveformEnvelopeBuilder. Lorsque vous utilisez cet objet, vous pouvez accéder au mappage de la fréquence à l'accélération de sortie (FOAM) via VibratorFrequencyProfile.
- Valeur d'amplitude comprise dans la plage \( [0, 1] \), qui représente l'intensité de vibration réalisable à une fréquence donnée, telle que déterminée par le FOAM de l'appareil. Par exemple, une valeur de \( 0.5 \) génère la moitié de l'accélération de sortie maximale pouvant être atteinte à la fréquence donnée.
Valeur de fréquence, spécifiée en Hertz.
Valeur duration, qui représente le temps, en millisecondes, nécessaire pour passer du dernier point de contrôle au nouveau.
Le code suivant montre un exemple de forme d'onde qui définit un effet de vibration de 400 ms. Elle commence par une rampe d'amplitude de 50 ms, de l'arrêt à la pleine puissance, à une fréquence constante de 60 Hz. Ensuite, la fréquence passe à 120 Hz au cours des 100 ms suivantes et reste à ce niveau pendant 200 ms. Enfin, l'amplitude diminue jusqu'à \( 0 \)et la fréquence revient à 60 Hz au cours des 50 ms suivantes :
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()
)
Les sections suivantes fournissent plusieurs exemples de formes d'onde de vibration avec des enveloppes.
Ressort rebondissant
Un exemple précédent utilise PRIMITIVE_THUD pour simuler des interactions de rebond physique. L'API d'enveloppe de base offre un contrôle beaucoup plus précis, ce qui vous permet d'adapter précisément l'intensité et la netteté des vibrations.
Cela permet d'obtenir un retour haptique qui suit plus précisément les événements animés.
Voici un exemple de ressort en chute libre avec une animation améliorée par un effet d'enveloppe de base joué chaque fois que le ressort rebondit en bas de l'écran :
Figure 5. Graphique de forme d'onde d'accélération de sortie pour une vibration simulant un ressort rebondissant.
@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")
}
}
}
Lancement de fusée
Un exemple précédent montre comment utiliser l'API d'enveloppe de base pour simuler une réaction de ressort rebondissant. Le WaveformEnvelopeBuilder permet de contrôler précisément toute la gamme de fréquences de l'appareil, ce qui permet de créer des effets haptiques très personnalisés. En combinant ces données avec les données FOAM, vous pouvez adapter les vibrations à des capacités de fréquence spécifiques.
Voici un exemple de simulation de lancement de fusée utilisant un schéma de vibration dynamique. L'effet passe de la sortie d'accélération de fréquence minimale prise en charge, 0,1 G, à la fréquence de résonance, en conservant toujours une entrée d'amplitude de 10 %. Cela permet à l'effet de commencer avec une sortie raisonnablement forte et d'augmenter l'intensité et la netteté perçues, même si l'amplitude d'entraînement est la même. Une fois la résonance atteinte, la fréquence de l'effet redescend au minimum, ce qui est perçu comme une diminution de l'intensité et de la netteté. Cela crée une sensation de résistance initiale suivie d'un relâchement, imitant un lancement dans l'espace.
Cet effet n'est pas possible avec l'API d'enveloppe de base, car elle abstrait les informations spécifiques à l'appareil concernant sa fréquence de résonance et sa courbe d'accélération de sortie. Si vous augmentez la netteté, la fréquence équivalente peut dépasser la résonance, ce qui peut entraîner une baisse d'accélération involontaire.
Figure 6. Graphique de forme d'onde d'accélération de sortie pour une vibration simulant le lancement d'une fusée.
@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()
)
}