This page covers the examples of how to use different haptics APIs to create custom effects in an Android application. As much of the information on this page relies on a good knowledge of the workings of a vibration actuator, we recommend reading the Vibration actuator primer.
This page includes the following examples.
- Custom vibration patterns
- Ramp up pattern: A pattern that begins smoothly.
- Repeating pattern: A pattern with no end.
- Pattern with fallback: A fallback demonstration.
- Vibration compositions
For additional examples, see Add haptic feedback to events, and always follow haptics design principles.
Use fallbacks to handle device compatibility
When implementing any custom effect, consider the following:
- Which device capabilities are required for the effect
- What to do when the device is not capable of playing the effect
The Android haptics API reference provides details on how to check for support for components involved in your haptics, so that your app can provide a consistent overall experience.
Depending on your use case, you might want to disable custom effects or to provide alternative custom effects based on different potential capabilities.
Plan for the following high-level classes of device capability:
If you're using haptic primitives: devices supporting those primitives needed by the custom effects. (See the next section for details on primitives.)
Devices with amplitude control.
Devices with basic vibration support (on/off)—in other words, those lacking amplitude control.
If your app's haptic effect choice accounts for these categories, then its haptic user experience should remain predictable for any individual device.
Usage of haptic primitives
Android includes several haptics primitives that vary in both amplitude and frequency. You may use one primitive alone or multiple primitives in combination to achieve rich haptic effects.
- Use delays of 50 ms or longer for discernible gaps between two primitives, also taking into account the primitive duration if possible.
- Use scales that differ by a ratio of 1.4 or more so the difference in intensity is better perceived.
Use scales of 0.5, 0.7 and 1.0 to create a low, medium and high intensity version of a primitive.
Create custom vibration patterns
Vibration patterns are often used in attentional haptics, such as notifications
and ringtones. The Vibrator
service can play long vibration patterns that
change the vibration amplitude over time. Such effects are named waveforms.
Waveform effects can be easily perceivable, but sudden long vibrations can startle the user if played in a quiet environment. Ramping to a target amplitude too fast might also produce audible buzzing noises. The recommendation for designing waveform patterns is to smooth the amplitude transitions to create ramp up and down effects.
Sample: Ramp-up pattern
Waveforms are represented as VibrationEffect
with three parameters:
- Timings: an array of durations, in milliseconds, for each waveform segment.
- Amplitudes: the desired vibration amplitude for each duration specified in the first argument, represented by an integer value from 0 to 255, with 0 representing the vibrator "off" and 255 being the device's maximum amplitude.
- Repeat index: the index in the array specified in the first argument to start repeating the waveform, or -1 if it should play the pattern only once.
Here is an example waveform that pulses twice with a pause of 350 ms in between pulses. The first pulse is a smooth ramp up to the maximum amplitude, and the second is a quick ramp to hold maximum amplitude. Stopping at the end is defined by the negative repeat index value.
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 // Do not 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; // Do not repeat. vibrator.vibrate(VibrationEffect.createWaveform(timings, amplitudes, repeatIndex));
Sample: Repeating pattern
Waveforms can also be played repeatedly until cancelled. The way to create a repeating waveform is to set a non-negative ‘repeat’ parameter. When you play a repeating waveform, the vibration continues until it's explicitly cancelled in the 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(); }
This is very useful for intermittent events that require user action to acknowledge it. Examples of such events include incoming phone calls and triggered alarms.
Sample: Pattern with fallback
Controlling the amplitude of a vibration is a hardware-dependent capability. Playing a waveform on a low-end device without this capability causes it to vibrate at the maximum amplitude for each positive entry in the amplitude array. If your app needs to accommodate such devices then the recommendation is to make sure that your pattern doesn't generate a buzzing effect when played in that condition, or to design a simpler ON/OFF pattern that can be played as a fallback instead.
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)); }
Create vibration compositions
This section presents ways to compose them into longer and more complex custom effects, and goes beyond that to explore rich haptics using more advanced hardware capabilities. You can use combinations of effects that vary amplitude and frequency to create more complex haptic effects on devices with haptic actuators that have a wider frequency bandwidth.
The process for creating custom vibration patterns, described previously on this page, explains how to control the vibration amplitude to create smooth effects of ramping up and down. Rich haptics improves on this concept by exploring the wider frequency range of the device vibrator to make the effect even smoother. These waveforms are especially effective at creating a crescendo or diminuendo effect.
The composition primitives, described earlier on this page, are implemented by the device manufacturer. They provide a crisp, short and pleasant vibration that aligns with Haptics principles for clear haptics. For more details about these capabilities and how they work, see Vibration actuators primer.
Android doesn't provide fallbacks for compositions with unsupported primitives. We recommend that you perform the following steps:
Before activating your advanced haptics, check that a given device supports all the primitives you're using.
Disable the consistent set of experiences that are unsupported, not just the effects that are missing a primitive. More information on how to check the device’s support is shown as follows.
You can create composed vibration effects with VibrationEffect.Composition
.
Here is an example of a slowly rising effect followed by a sharp click effect:
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());
A composition is created by adding primitives to be played in sequence. Each primitive is also scalable, so you can control the amplitude of the vibration generated by each of them. The scale is defined as a value between 0 and 1, where 0 actually maps to a minimum amplitude at which this primitive can be (barely) felt by the user.
If you’d like to create a weak and strong version of the same primitive, it is recommended that the scales differ by a ratio of 1.4 or more, so the difference in intensity can be easily perceived. Don't try to create more than three intensity levels of the same primitive, because they aren't perceptually distinct. For example, use scales of 0.5, 0.7, and 1.0 to create a low, medium, and high intensity version of a primitive.
The composition can also specify delays to be added in between consecutive primitives. This delay is expressed in milliseconds since the end of the previous primitive. In general, a 5 to 10 ms gap between two primitives is too short to be detectable. Consider using a gap on the order of 50 ms or longer if you want to create a discernible gap between two primitives. Here is an example of a composition with delays:
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());
The following APIs can be used to verify the device support for specific primitives:
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. }
It's also possible to check multiple primitives and then decide which ones to compose based on the device support level:
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);
Sample: Resist (with low ticks)
You can control the amplitude of the primitive vibration to convey useful feedback to an action in progress. Closely-spaced scale values can be used to create a smooth crescendo effect of a primitive. The delay between consecutive primitives can also be dynamically set based on the user interaction. This is illustrated in the following example of a view animation controlled by a drag gesture and augmented with haptics.
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); } }
Sample: Expand (with rise and fall)
There are two primitives for ramping up the perceived vibration intensity: the
PRIMITIVE_QUICK_RISE
and
PRIMITIVE_SLOW_RISE
.
They both reach the same target, but with different durations. There is only one
primitive for ramping down, the
PRIMITIVE_QUICK_FALL
.
These primitives work better together to create a waveform segment that grows in
intensity and then dies off. You can align scaled primitives to prevent sudden
jumps in amplitude between them, which also works well for extending the overall
effect duration. Perceptually, people always notice the rising portion more than
the falling portion, so making the rising portion shorter than the falling can
be used to shift the emphasis towards the falling portion.
Here is an example of an application of this composition for expanding and collapsing a circle. The rise effect can enhance the feeling of expansion during the animation. The combination of rise and fall effects helps emphasize the collapsing at the end of the animation.
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; } }
Sample: Wobble (with spins)
One of the key haptics principles is to delight users. A fun way
to introduce a pleasant unexpected vibration effect is to use the
PRIMITIVE_SPIN
.
This primitive is most effective when it is called more than once. Multiple
spins concatenated can create a wobbling and unstable effect, which can be
further enhanced by applying a somewhat random scaling on each primitive. You
can also experiment with the gap between successive spin primitives. Two spins
without any gap (0 ms in between) creates a tight spinning sensation. Increasing
the inter-spin gap from 10 to 50 ms leads to a looser spinning sensation, and
can be used to match the duration of a video or animation.
We don't recommended using a gap that is longer than 100 ms, as the successive spins no longer integrate well and begin to feel like individual effects.
Here is an example of a elastic shape that bounces back after being dragged down and then released. The animation is enhanced with a pair of spin effects, played with varying intensities that are proportional to the bounce displacement.
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 [-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 [-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) } }
Sample: Bounce (with thuds)
Another advanced application of vibration effects is to simulate physical
interactions. The
PRIMITIVE_THUD
can create a strong and reverberating effect, which can be paired with the
visualization of an impact, in a video or animation for example, to augment the
overall experience.
Here is an example of a simple ball drop animation enhanced with a thud effect played each time the ball bounces off the bottom of the screen:
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; } } }); } }