- ⚠️
AnchoredDraggableStateoften starts animations too soon, even when a user hasn't finished moving their finger. - 🎯 Using
awaitRelease()lets developers fully control when things happen after a swipe ends. - 📏 Adjusting drag limits and speed cuts down on swipes that happen by accident.
- 🎮 Custom gesture finding with
pointerInputlets you wait to change states for a better user experience. - 🛠️
confirmValueChangecan stop state changes from happening too early in these components.
Swiping is a basic action in mobile UI/UX. But getting the interaction right can be hard, especially with Jetpack Compose. If your swipeable components move too fast, jumping to their final spots before the user even lifts their finger, you are likely seeing problems with how Compose deals with gestures normally. This guide shows how to make swipes work better using AnchoredDraggableState. It also covers changing how pointer input events work, and good ways to make interactions smooth and natural on Android.
Common Problem: Premature Swipe Trigger
Jetpack Compose is strong but has its own way of doing things. By default, swipe gestures using AnchoredDraggableState can finish by itself—even while the user is still dragging. For example, think of a bottom sheet. It might close fast the moment you drag it far enough, even if you are still moving your finger. This isn't really a bug. It's how the drag limits work in Compose.
Why It’s a UX Concern
This works, but it often goes against what users expect. Users think the screen will “wait” until they finish their swipe. When the part jumps too soon, users feel less in control. And they don't get the right physical response. This can make apps less pleasing to use and harder to interact with. This is true especially for things like side drawers, carousels, and cards you can swipe away.
Understanding AnchoredDraggableState
A main part of Compose’s swipe tools is the strong, but sometimes too eager, AnchoredDraggableState. This state system controls swipeable parts. You can drag these between set anchor points.
Key Concepts
- Anchors: Specific spots the part can be in (e.g., Expanded, Collapsed).
- Thresholds: Limits that decide when to animate to a new anchor.
- Velocity & Direction: Compose figures out how fast you swipe and which way you mean to go. It uses this to pick the final anchor spot.
Once users go past a limit (combined with swipe speed), Compose figures out the next normal anchor to stop at. And then it starts an animation to jump to that state. This happens even if the swipe isn’t officially over.
Default Behavior vs. Developer Expectations
By design, AnchoredDraggableState starts state changes before you lift your finger. It assumes that going past a drag limit with some speed means the user wants it to happen. But in many apps, developers want to fully account for the whole swipe. They wait for onRelease before changing anything.
The Gesture Process in Jetpack Compose
Jetpack Compose uses gesture finding based on coroutines. This gives good accuracy but needs careful control.
Using the Modifier.pointerInput, developers can change the normal way gestures are found.
Key Process Steps
awaitFirstDown()– Find when the finger first touches the screen.drag()– Begin following finger movement and its path.- Velocity Tracking – Compose keeps figuring out finger speed.
- Threshold Crossing – If the limit and speed conditions are right, the normal way of doing things starts an anchor state change.
awaitRelease()– Find the exact point when the finger leaves the screen.
Only steps 1–4 are used in most normal Compose components that use AnchoredDraggableState. What's more, skipping awaitRelease() means decisions are made only by how far the swipe goes, not by a final OK.
Why Does Jumping Happen Early?
Compose’s swipe logic is made to feel fast and quick to respond from the start. It assumes going past a limit means the action is done. It tries to give sharp animations. But…
⚠️ Compose doesn’t wait for the user to lift their finger—it acts too soon once it thinks a real gesture goal is met.
This can make things jump in the middle of a swipe. It works fast, but it misses the small details of what the user means to do. A diagonal swipe or light touch can cause unwanted changes. Moreover, this is a problem especially when:
- Swipeable components see partial drags as finished actions.
- Users try to swipe back mid-way but Compose has already acted.
- Quick swipes over sensitive spots cause state changes to happen by themselves.
How to Delay Actions Until Finger Release
To stop early changes, you need more control over gestures using pointerInput.
Modifier.pointerInput(Unit) {
awaitPointerEventScope {
val down = awaitFirstDown()
drag(down.id) {
// Track drag deltas here manually
}
awaitRelease() // Only act after this point
// Manually trigger animation
}
}
By waiting for awaitRelease(), you can wait to act until the user has fully finished their swipe. Furthermore, use this with speed and drag distance logic. This lets you check what the user truly meant to do by hand.
Implementing Custom Swipes: DIY Gesture Control
Sometimes it's best to get rid of AnchoredDraggableState and build your own. This is true especially if you want very specific gestures.
How to Build Custom Swipes
- Manual Drag Capture
- Use
pointerInputanddrag()to figure out changes in X and Y positions.
- Use
- Track Progress
- Keep a
Floatoffset by hand usingAnimatable.
- Keep a
- Apply Threshold Logic
- Wait until the drag goes past a certain distance.
- Finalize on Release
- Use
awaitRelease()before animating to the end state.
- Use
Sample Implementation
val offsetX = remember { Animatable(0f) }
Modifier.pointerInput(Unit) {
awaitPointerEventScope {
val down = awaitFirstDown()
drag(down.id) { change ->
val delta = change.positionChange().x
launch {
offsetX.snapTo(offsetX.value + delta)
}
change.consume()
}
awaitRelease()
if (offsetX.value > threshold) {
// Animate to expanded
launch { offsetX.animateTo(maxOffset) }
} else {
// Animate to collapsed
launch { offsetX.animateTo(0f) }
}
}
}
This gives you very precise, detailed control over swipe gestures.
Using awaitRelease() to Extend Control
awaitRelease() is a main tool for gestures. Whether you use AnchoredDraggableState or custom gestures, following when the finger lifts lets you make better choices, ones that fit what the user wants.
Benefits of awaitRelease()
- Stops accidental fast swipes from starting changes.
- Lets you check:
- Duration
- Distance
- Velocity
- Direction
- Makes the screen feel “alive” and respond to you, not just like a machine.
Adapting AnchoredDraggableState Behavior
You don't have to build everything again. AnchoredDraggableState lets you change some of its normal ways.
confirmValueChange
The confirmValueChange lambda starts before jumping to a new value. This is a good spot to check user gestures.
val hasReleasedFinger = remember { mutableStateOf(false) }
val state = rememberAnchoredDraggableState(
initialValue = DrawerValue.Closed,
anchors = mapOf(0f to DrawerValue.Closed, 500f to DrawerValue.Open),
confirmValueChange = { target ->
if (!hasReleasedFinger.value) return@rememberAnchoredDraggableState false
true
}
)
Combine this with gesture finding that sets hasReleasedFinger.value = true after awaitRelease(). This gives you the best of both: Compose's normal behavior, but guided by your own logic and a human's OK.
Avoiding Unintended Gestures with Threshold Tuning
Many changes that happen too early come from safe, normal limits.
What to Tune
- Drag Thresholds
- Increase pixel count before a change is looked at.
- Resistance Parameters
- Make it feel like there's some drag at the edges:
AnchoredDraggableDefaults.resistance.
- Make it feel like there's some drag at the edges:
- Velocity Sensitivity
- Make it less likely to jump based on speed.
val state = rememberAnchoredDraggableState(
threshold = { from, to -> 0.5f }, // 50% drag before slide decision
resistance = AnchoredDraggableDefaults.resistanceFactorAtEdges(0.4f)
)
These minor changes can make swipes feel much better.
Velocity and Direction Handling Tips
Swipes are actions with many parts. They have direction, how strong they are, and how they finish. Getting them wrong makes for a bad user experience.
Best Practices
- Use Directional Locks
Lock the way things are oriented. This stops diagonal or accidental touches:orientation = Orientation.Horizontal - Minimum Velocity Triggers
Set a minimum swipe speed to start changes. - Vector Math Checks
Compare cosine/sine values if the gesture needs to go in a very specific direction.
Case Study: Fixing a Swipe Panel
Imagine a card you swipe to get rid of. Users scroll up and down. But side-to-side swipes are often taken the wrong way. You only want a swipe to dismiss if:
- User swipes far enough
- Speed is mostly side-to-side
- User lifts their finger
Fix Strategy
- Use custom drag +
awaitRelease() - Figure out the difference in X versus Y.
- Use a direction filter:
if (abs(deltaX) > abs(deltaY)) { // This is a horizontal swipe } - Only call
dismiss()if the movement goes past the limit and the direction is right.
UX and Interaction Design Best Practices
Swipe UX needs to back up what the user means to do. And it must feel natural and quick to react.
Guidelines
- ☝️ Always wait to make changes until the finger lifts (
awaitRelease()). - 🎨 Show clear signs of progress. For example, change the card as you drag it.
- 🌀 Easing Animations — slow start, fast end for natural movement (
tween()). - 👋 Swipes should feel physical. They should match how you move your finger.
Follow the Material Design movement rules to meet what users expect.
Testing Gesture Behavior in Compose
Gesture logic might fail on different devices and in various situations. Thorough testing makes sure everyone gets the same experience.
Test Strategies
- ✅ Compose UI Test API
- Act out gestures using
performGesture { swipeDown() }
- Act out gestures using
- 🔬 Manual Vector Drag Testing
- Act out swipes for specific axes.
- 📊 Tests for Unusual Situations
- Quick flicks
- Drags that are stopped midway
- Changing direction during a swipe
- Using multiple fingers at once
Check not just if changes happen, but when and why.
Fine-Tuning for Natural Swipe Interactions
Jetpack Compose swipe gestures can be fully changed once you understand the gesture process with tools like awaitRelease() and confirmValueChange. When you respect how users interact, especially by not acting before they let go, you make screens that respond well. They feel less like machines working on their own and more like working together. Whether you're making Jetpack Compose swipe mechanics better or building your own, meaning what you do and being exact are important.
References
- Google. (2022). Material movement. Material Design. https://m3.material.io/foundations/motion/overview
- Android Developers. (2023). Swipeable and Anchored Draggable. Android Developers. https://developer.android.com/jetpack/compose/gestures
- Android Performance Team. (2021). Avoiding sluggish gestures. Android Studio Performance Patterns.
- UX Collective. (2023). Why your swipe UX is broken. UX Collective on Medium.