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'ondes de vibration standards dans une application Android.
Cette page comprend les exemples suivants:
- Modèles de vibration personnalisés
- Modèle d'accélération: modèle qui commence de manière fluide.
- Modèle répétitif: modèle sans fin.
- Modèle avec remplacement: démonstration du remplacement.
- Compositions de vibrations
- Résistance: effet de glissement avec une intensité dynamique.
- Développer: effet de montée puis de baisse.
- Ondulation: effet d'ondulation à l'aide de la primitive
SPIN
. - Rebondissement: effet de rebondissement à l'aide de la primitive
THUD
.
- Forme d'onde de vibration avec enveloppes
- Ressort qui rebondit: effet de rebond élastique à l'aide d'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 ne peut pas lire l'effet ?
La documentation de référence de l'API haptique Android explique comment vérifier la prise en charge des composants impliqués dans vos haptiques afin que votre application puisse fournir une expérience globale cohérente.
Selon votre cas d'utilisation, vous pouvez désactiver les effets personnalisés ou en fournir d'autres en fonction des différentes fonctionnalités potentielles.
Planifiez les classes de fonctionnalités d'appareils générales suivantes:
Si vous utilisez des primitives haptiques: appareils compatibles avec ces primitives requises par les effets personnalisés. (Pour en savoir plus sur les primitives, consultez la section suivante.)
Appareils avec contrôle de l'amplitude
Appareils compatibles avec les vibrations de base (marche/arrêt), c'est-à-dire ceux qui ne disposent pas de contrôle de l'amplitude.
Si le choix de l'effet haptique de votre application tient compte de ces catégories, son expérience utilisateur haptique devrait rester prévisible pour chaque appareil.
Utilisation de primitives haptiques
Android inclut plusieurs primitives haptiques qui varient à la fois en amplitude et en fréquence. Vous pouvez utiliser une primitive seule ou plusieurs primitives combinées pour obtenir des effets haptiques riches.
- Utilisez des délais de 50 ms ou plus pour les écarts perceptibles entre deux primitives, en tenant également compte de la durée de la primitive, si possible.
- Utilisez des échelles qui diffèrent d'un ratio 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 à intensité faible, moyenne et élevée d'une primitive.
Créer des modèles de vibration personnalisés
Les modèles de vibration sont souvent utilisés dans les haptiques d'attention, comme les notifications et les sonneries. Le service Vibrator
peut diffuser de longs motifs de vibration qui modifient l'amplitude des vibrations au fil du temps. Ces effets sont appelés "formes d'onde".
Les effets de forme d'onde sont généralement perceptibles, mais des vibrations soudaines et prolongées peuvent effrayer l'utilisateur s'ils sont diffusés dans un environnement calme. Une montée trop rapide vers une amplitude cible peut également produire des bourdonnements audibles. Concevez des modèles de forme d'onde pour lisser les transitions d'amplitude afin de créer des effets de montée et de descente.
Exemples de modèles de vibration
Les sections suivantes fournissent plusieurs exemples de modèles 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 de durées, en millisecondes, pour 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 "arrêt" du vibreur et 255 l'amplitude maximale de l'appareil.
- Indice de répétition:indice 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 impulsions. Le premier impulsion est une montée progressive jusqu'à l'amplitude maximale, et le second est une montée rapide pour maintenir l'amplitude maximale. L'arrêt à la fin est défini par la valeur négative de l'indice de répétition.
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 de manière répétée jusqu'à ce qu'elles soient annulées. Pour créer une forme d'onde répétitive, 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();
}
Cette fonctionnalité est très utile pour les événements intermittents qui nécessitent une action de l'utilisateur pour être confirmés. Ces événements incluent les appels téléphoniques entrants et les alarmes déclenchées.
Modèle avec remplacement
Le contrôle de l'amplitude d'une vibration est une fonctionnalité dépendant du matériel. Si vous lisez une forme d'onde sur un appareil d'entrée de gamme sans cette fonctionnalité, l'appareil vibre à l'amplitude maximale pour chaque entrée positive dans le tableau d'amplitude. Si votre application doit prendre en charge de tels appareils, utilisez un modèle qui ne génère pas d'effet de bourdonnement lorsqu'il est lu dans cette condition, ou concevez un modèle ON/OFF plus simple qui peut être lu à la place.
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 vibration
Cette section présente des façons de composer des vibrations en effets personnalisés plus longs et plus complexes, et va plus loin pour explorer les retours haptiques riches à l'aide de fonctionnalités matérielles plus avancées. Vous pouvez utiliser des combinaisons d'effets qui varient l'amplitude et la fréquence pour créer des effets haptiques plus complexes sur les appareils dotés d'actionneurs haptiques dont la bande passante de fréquence est plus large.
Le processus de création de modèles de vibration personnalisés, décrit 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 fluides. La haptique riche améliore 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. Ils offrent une vibration nette, courte et agréable qui s'aligne sur les principes haptiques pour une haptique claire. Pour en savoir plus sur ces fonctionnalités et leur fonctionnement, consultez Présentation des actionneurs à 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 non seulement les effets pour lesquels une primitive est manquante.
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 dans l'ordre. Chaque primitive est également évolutive, ce qui vous permet de contrôler l'amplitude des vibrations générées 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 de la même primitive, créez des rapports de force d'au moins 1, 4 afin que la différence d'intensité puisse être facilement perçue. N'essayez pas de créer plus de trois niveaux d'intensité de la même primitive, car ils ne sont pas distincts visuellement. Par exemple, utilisez des échelles de 0,5, 0,7 et 1,0 pour créer des versions à faible, moyenne et haute intensité d'une primitive.
Ajouter des espaces entre les primitives de vibration
La composition peut également spécifier les retards à 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 écart de l'ordre de 50 ms ou plus si vous souhaitez créer un écart perceptible entre deux primitives. Voici un exemple de composition avec des retards:
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
Vous pouvez utiliser les API suivantes pour vérifier la prise en charge de primitives spécifiques par l'appareil:
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 des "tic" faibles)
Vous pouvez contrôler l'amplitude de la vibration primitive pour transmettre des commentaires utiles à une action en cours. Vous pouvez utiliser des valeurs d'échelle espacées de manière rapprochée 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. Vous trouverez un exemple dans l'animation de vue suivante, contrôlée par un geste de glissement et enrichie par la technologie haptique.

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é des vibrations perçues : 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 permet également d'étendre la durée globale de l'effet.
Perceptuellement, les utilisateurs remarquent toujours plus la partie ascendante que la partie descendante. Vous pouvez donc réduire la partie ascendante 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 de montée peut renforcer la sensation d'expansion pendant l'animation. La combinaison des effets de montée et de descente permet de mettre en avant le repliement à 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 des rotations)
L'un des principes haptiques clés est de ravir les utilisateurs. Pour ajouter un effet de vibration inattendu et agréable, vous pouvez utiliser PRIMITIVE_SPIN
. Cette primitive est la plus efficace lorsqu'elle est appelée plusieurs fois. La concatenaison de plusieurs rotations peut créer un effet de balancement et d'instabilité, qui peut être encore amélioré en appliquant une mise à l'échelle quelque peu aléatoire à chaque primitive. Vous pouvez également tester l'espace entre les primitives de rotation successives. Deux rotations sans aucun espace (0 ms entre les deux) créent une sensation de rotation serrée. Augmenter l'intervalle entre les rotations de 10 à 50 ms entraîne une sensation de rotation plus lâche et peut être utilisé pour adapter la durée d'une vidéo ou d'une animation.
N'utilisez pas d'espace supérieur à 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é tirée vers le bas, puis relâchée. L'animation est améliorée par une paire d'effets de rotation, lus avec des intensités variables proportionnelles au déplacement de 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 est la simulation d'interactions physiques. PRIMITIVE_THUD
peut créer un effet fort et réverbérant, 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 choc 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
Le processus de création de modèles de vibration personnalisés vous permet de contrôler l'amplitude des vibrations pour créer des effets fluides de montée et de descente. 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. Vous pouvez ainsi 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 une connaissance du 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 d'
Vibrator.areEnvelopeEffectsSupported()
. - Désactivez l'ensemble cohérent d'expériences non compatibles, ou utilisez des modèles de vibration personnalisés ou des compositions comme solutions de remplacement.
Pour créer des effets d'enveloppe plus basiques, utilisez BasicEnvelopeBuilder
avec ces paramètres:
- Valeur d'intensité comprise 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 que l'appareil peut atteindre.
Valeur clarté comprise dans la plage \( [0, 1] \), qui représente la netteté de la vibration. Les valeurs inférieures correspondent à des vibrations plus fluides, tandis que les valeurs plus élevées créent une sensation plus nette.
Valeur duration, qui représente le temps, en millisecondes, nécessaire pour passer du dernier point de contrôle (c'est-à-dire une paire d'intensité et de netteté) au nouveau.
Voici un exemple de forme d'onde qui augmente l'intensité d'une vibration basse à une vibration haute et de force maximale sur 500 ms, puis diminue à\( 0 \) (arrêt) 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 la 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 la mousse 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 obtenue à 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 amplitude, à une fréquence constante de 60 Hz. Ensuite, la fréquence passe à 120 Hz au cours des 100 ms suivants et reste à ce niveau pendant 200 ms. Enfin, l'amplitude diminue jusqu'à \( 0 \), et la fréquence revient à 60 Hz au cours des 50 dernières 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()
)
Les sections suivantes fournissent plusieurs exemples de formes d'ondes de vibration avec des enveloppes.
Ressort de rebond
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'ajuster précisément l'intensité et la netteté des vibrations.
Le retour haptique suit ainsi 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 sur le bas de l'écran:
@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. WaveformEnvelopeBuilder
offre un contrôle précis sur la plage de fréquences complète de l'appareil, ce qui permet d'obtenir des effets haptiques hautement personnalisés. En combinant cela aux données FOAM, vous pouvez adapter les vibrations à des capacités de fréquence spécifiques.
Voici un exemple qui montre une simulation de lancement de fusée à l'aide d'un modèle de vibration dynamique. L'effet va de la sortie d'accélération de fréquence minimale prise en charge, 0,1 G, à la fréquence de résonance, en maintenant 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 de pilotage est la même. Une fois la résonance atteinte, la fréquence de l'effet redescend à son 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'une libération, imitant un lancement dans l'espace.
Cet effet n'est pas possible avec l'API d'enveloppe de base, car elle élimine les informations spécifiques à l'appareil concernant sa fréquence de résonance et sa courbe d'accélération de sortie. L'augmentation de la netteté peut pousser la fréquence équivalente au-delà de la résonance, ce qui peut entraîner une baisse involontaire de l'accélération.
@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()
)
}