In this blog post, we will delve into an exciting new tool: the AnchoredDraggable modifier. We’ll see how to use this modifier to make something really cool — swipe-to-action. It’s that feature you often see in modern apps where you swipe something and it reveals actions like delete, edit, and share etc...
AnchoredDraggable is designed to create components you can drag between specific states, just like those modal bottom sheets you’ve seen. The best part? This API takes over the role of Material’s Swipeable API, offering a more versatile approach.
This implementation is inspired by flutter_slidable.
What do we implement in this blog? Three variants of swipe-to-action
Behind Motion Swipe
Scroll Motion Swipe
Drawer Motion Swipe
The source code is available on GitHub
We are what we repeatedly do. Excellence, then, is not an act, but a habit. Try out Justly and start building your habits today!
Let’s first add a required dependency to use Anchored draggable API.
implementation("androidx.compose.foundation:foundation:1.6.0-alpha04")
Before we go further into implementation, let’s have a quick look at the AnchoredDraggable Modifier.
@ExperimentalFoundationApi
class AnchoredDraggableState<T>(
initialValue: T,
internal val positionalThreshold: (totalDistance: Float) -> Float,
internal val velocityThreshold: () -> Float,
val animationSpec: AnimationSpec<Float>,
internal val confirmValueChange: (newValue: T) -> Boolean = { true }
)
The AnchoredDraggableState
helps you manage and control draggable elements with anchor points in your app. Here's what you need to know:
Initial Position: You can set the starting position of a draggable element using the initialValue
.
Thresholds: It allows you to define two thresholds:
positionalThreshold
: This determines when the element should snap to a new position while you’re dragging it. It’s like saying, “Move it this far, and then snap to the closest anchor.”velocityThreshold
: This sets the speed at which the element needs to be moving when you release it for it to snap to a new position, even if it didn’t reach the positional thresholdAnimations: You can specify how the element should smoothly transition between states using the animationSpec
.
Control: There’s an optional callback called confirmValueChange
that gives you control over whether a state change should happen or not.
@ExperimentalFoundationApi
fun <T> Modifier.anchoredDraggable(
state: AnchoredDraggableState<T>,
orientation: Orientation,
enabled: Boolean = true,
reverseDirection: Boolean = false,
interactionSource: MutableInteractionSource? = null
) = draggable(
state = state.draggableState,
orientation = orientation,
enabled = enabled,
interactionSource = interactionSource,
reverseDirection = reverseDirection,
startDragImmediately = state.isAnimationRunning,
onDragStopped = { velocity -> launch { state.settle(velocity) } }
)
Key Parameters:
state
: Associates the draggable element with an AnchoredDraggableState, managing its position and animation.orientation
: Specifies the drag orientation as horizontal or vertical.enabled
: Determines whether the draggable behaviour is active or not.reverseDirection
: Optional parameter to reverse the drag direction.interactionSource
: An optional source for interaction events.How it Works:
AnchoredDraggableState
keeps track of the current anchor, allowing you to react to changes accordingly.Now let’s start the implementation of the first and simplest Swipe-to-action. To simplify we have given names to all three variants as it’s motion.
To keep the implementation simple I’m not showing the composable of Actions and content. You can find the full sample on GitHub.
We’ll use common DraggableItem
composable in all examples.
enum class DragAnchors {
Start,
Center,
End,
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun DraggableItem(
state: AnchoredDraggableState<DragAnchors>,
content: @Composable BoxScope.() -> Unit,
startAction: @Composable (BoxScope.() -> Unit)? = {},
endAction: @Composable (BoxScope.() -> Unit)? = {}
) {
Box(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
.height(100.dp)
.clip(RectangleShape)
) {
endAction?.let {
endAction()
}
startAction?.let {
startAction()
}
Box(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.CenterStart)
.offset {
IntOffset(
x = -state
.requireOffset()
.roundToInt(),
y = 0,
)
}
.anchoredDraggable(state, Orientation.Horizontal, reverseDirection = true),
content = content
)
}
}
Parameters:
state
: This parameter is of type AnchoredDraggableState<DragAnchors>, It controls how the item is dragged, anchored, and animated.content
: It represents the main content of the draggable item.startAction
and endAction
: These are optional parameters. It defines the swipe-to-action buttons that appear when the item is dragged to the start or end position.Here we’ll implement a swipe animation that reveals action as you drag. Let’s first set the AnchoredDraggableState
val density = LocalDensity.current
val defaultActionSize = 80.dp
val endActionSizePx = with(density) { (defaultActionSize * 2).toPx() }
val startActionSizePx = with(density) { defaultActionSize.toPx() }
val state = remember {
AnchoredDraggableState(
initialValue = DragAnchors.Center,
anchors = DraggableAnchors {
DragAnchors.Start at -startActionSizePx
DragAnchors.Center at 0f
DragAnchors.End at endActionSizePx
},
positionalThreshold = { distance: Float -> distance * 0.5f },
velocityThreshold = { with(density) { 100.dp.toPx() } },
animationSpec = tween(),
)
}
DraggableAnchors { ... }
: Defines the anchor points for the draggable element. It uses the DSL (domain-specific language) DraggableAnchors
to specify three anchor points:
startActionSizePx
pixels from the centre.endActionSizePx
pixels from the centre.Now, we just have to use this to animate DraggableItem
. Here’s how
Box(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.CenterStart)
.offset {
IntOffset(
x = -state
.requireOffset()
.roundToInt(),
y = 0,
)
}
.anchoredDraggable(state, Orientation.Horizontal, reverseDirection = true),
content = content
)
And we’re done. Pretty simple right? Here’s what we’ll have from the above implementation
The full source code of BehindMotionSwipe is available on GitHub.
To have a slideable effect as we drag we need to animate the actions' position.
Our AnchoredDraggableState would be the same as the above implementation.
Let’s first see the start action which is the Save Action.
DraggableItem(state = state,
startAction = {
Box(
modifier = Modifier
.fillMaxHeight()
.align(Alignment.CenterStart),
) {
SaveAction(
Modifier
.width(defaultActionSize)
.fillMaxHeight()
.offset {
IntOffset(
((-state
.requireOffset() - actionSizePx))
.roundToInt(), 0
)
}
)
}
}, endAction = { ... }, content = { ... }
((-state.requireOffset() - actionSizePx)).roundToInt()
: This calculation determines the X-coordinate of the SaveAction. Let's break it down:
state.requireOffset()
: This retrieves the current offset from the state object. The offset represents how far the draggable element has been dragged.- actionSizePx
: It subtracts the actionSizePx
from the offset. This is used to adjust the position of the SaveAction
relative to the draggable element's position.Now let’s see the end actions swipe animation.
endAction = {
Row(
modifier = Modifier
.fillMaxHeight()
.align(Alignment.CenterEnd)
.offset {
IntOffset(
(-state
.requireOffset() + endActionSizePx)
.roundToInt(), 0
)
}
)
{
// Action composables
}
}
Here -state.requireOffset() + endActionSizePx
calculation determines the X-coordinate of the Row and + endActionSizePx
adds endActionSizePx
to the offset. This adjustment is used to position the "end action" relative to the draggable element's position.
Now we’re done with the setup. Let’s see the output.
This swipe effect is quite similar to the previous one, with just a slight adjustment to one action’s horizontal position.
In this case, instead of altering the offset of the parent composable (as done in the previous implementation, where it was a Row), we’ll adjust the offset of each individual action.
Let’s first see the EditAction.
EditAction(
Modifier
.width(defaultActionSize)
.fillMaxHeight()
.offset {
IntOffset(
((-state
.requireOffset()) + actionSizePx)
.roundToInt(), 0
)
}
)
Nothing special just changing horizontal offset of Action based on the draggable state.
DeleteAction(
Modifier
.width(defaultActionSize)
.fillMaxHeight()
.offset {
IntOffset(
((-state
.requireOffset() * 0.5f) + actionSizePx)
.roundToInt(), 0
)
}
)
Like the ‘EditAction,’ the ‘DeleteAction’ is also positioned based on the current state offset. However, there’s a distinct difference in how it’s calculated. The ‘DeleteAction’ is intentionally designed to reduce its horizontal movement by half compared to the ‘EditAction’ making it appear like a drawer that smoothly slides out. And here’s the output.
By implementing swipe-to-reveal functionality using AnchoredDraggable, we’ve showcased how Compose’s composability and declarative approach can simplify complex interactions. As you experiment with this technique, you’ll find endless possibilities to enhance user experiences.
Happy coding!
The source code is available on GitHub.