User interface components give feedback to the device user by the way they respond to user interactions. Every component has its own way of responding to interactions, which helps the user know what their interactions are doing. For example, if a user touches a button on a device's touchscreen, the button is likely to change in some way, perhaps by adding a highlight color. This change lets the user know that they touched the button. If the user didn't want to do that, they'll know to drag their finger away from the button before releasing–otherwise, the button will activate.
The Compose Gestures documentation covers how Compose components handle low-level pointer event, such as pointer moves and clicks. Out of the box, Compose abstracts those low-level events into higher-level interactions–for example, a series of pointer events might add up to a button press-and-release. Understanding those higher-level abstractions can help you customize how your UI responds to the user. For example, you might want to customize how a component's appearance changes when the user interacts with it, or maybe you just want to maintain a log of those user actions. This document gives you the information you need to modify the standard UI elements, or design your own.
Interactions
In many cases, you don't need to know just how your Compose component is
interpreting user interactions. For example, Button
relies on
Modifier.clickable
to figure out whether the user clicked the button. If you're adding a typical
button to your app, you can define the button's onClick
code, and
Modifier.clickable
runs that code when appropriate. That means you don't need
to know whether the user tapped the screen or selected the button with a
keyboard; Modifier.clickable
figures out that the user performed a click, and
responds by running your onClick
code.
However, if you want to customize your UI component's response to user behavior, you may need to know more of what's going on under the hood. This section gives you some of that information.
When a user interacts with a UI component, the system represents their behavior
by generating a number of
Interaction
events. For example, if a user touches a button, the button generates
PressInteraction.Press
.
If the user lifts their finger inside the button, it generates a
PressInteraction.Release
,
letting the button know that the click was finished. On the other hand, if the
user drags their finger outside the button, then lifts their finger, the button
generates
PressInteraction.Cancel
,
to indicate that the press on the button was canceled, not completed.
These interactions are unopinionated. That is, these low-level interaction events don't intend to interpret the meaning of the user actions, or their sequence. They also don't interpret which user actions might take priority over other actions.
These interactions generally come in pairs, with a start and an end. The second
interaction contains a reference to the first one. For example, if a user
touches a button then lifts their finger, the touch generates a
PressInteraction.Press
interaction, and the release generates a
PressInteraction.Release
;
the Release
has a press
property identifying the initial
PressInteraction.Press
.
You can see the interactions for a particular component by observing its
InteractionSource
. InteractionSource
is built on top of Kotlin
flows, so you can collect the interactions from it the same way
you'd work with any other flow.
Interaction state
You might want to extend the built-in functionality of your components by also
tracking the interactions yourself. For example, perhaps you want a button to
change color when it's pressed. The simplest way to track the interactions is to
observe the appropriate interaction state. InteractionSource
offers a number
of methods that reveal various interaction statuses as state. For example, if
you want to see whether a particular button is pressed, you can call its
InteractionSource.collectIsPressedAsState()
method:
val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
Button(
onClick = { /* do something */ },
interactionSource = interactionSource) {
Text(if (isPressed) "Pressed!" else "Not pressed")
}
Besides collectIsPressedAsState()
, Compose also provides
collectIsFocusedAsState()
, collectIsDraggedAsState()
, and
collectIsHoveredAsState()
. These methods are actually convenience methods
built on top of lower-level InteractionSource
APIs. In some cases, you may
want to use those lower-level functions directly.
For example, suppose you need to know whether a button is being pressed, and
also whether it's being dragged. If you use both collectIsPressedAsState()
and collectIsDraggedAsState()
, Compose does a lot of duplicate work, and
there's no guarantee you'll get all the interactions in the right order. For
situations like this, you might want to work directly with the
InteractionSource
. The following section describes how you can track the
interactions yourself, getting just the information you need.
Work with InteractionSource
If you need low-level information about interactions with a component, you can
use standard flow APIs for that component's InteractionSource
.
For example, suppose you want to maintain a list of the press and drag
interactions for an InteractionSource
. This code does half the job, adding
the new presses to the list as they come in:
val interactionSource = remember { MutableInteractionSource() }
val interactions = remember { mutableStateListOf<Interaction>() }
LaunchedEffect(interactionSource) {
interactionSource.interactions.collect { interaction ->
when (interaction) {
is PressInteraction.Press -> {
interactions.add(interaction)
}
is DragInteraction.Start -> {
interactions.add(interaction)
}
}
}
}
But besides adding the new interactions, you also have to remove interactions when they end (for example, when the user lifts their finger back off the component). That's easy to do, since the end interactions always carry a reference to the associated start interaction. This code shows how you'd remove the interactions that have ended:
val interactions = remember { mutableStateListOf<Interaction>() }
LaunchedEffect(interactionSource) {
interactionSource.interactions.collect { interaction ->
when (interaction) {
is PressInteraction.Press -> {
interactions.add(interaction)
}
is PressInteraction.Release -> {
interactions.remove(interaction.press)
}
is PressInteraction.Cancel -> {
interactions.remove(interaction.press)
}
is DragInteraction.Start -> {
interactions.add(interaction)
}
is DragInteraction.Stop -> {
interactions.remove(interaction.start)
}
is DragInteraction.Cancel -> {
interactions.remove(interaction.start)
}
}
}
}
Now, if you want to know if the component is currently being pressed or dragged,
all you have to do is check whether interactions
is empty:
val isPressedOrDragged = interactions.isNotEmpty()
If you want to know what the most recent interaction was, just look at the last item in the list. For example, this is how the Compose ripple implementation figures out the appropriate state overlay to use for the most recent interaction:
val lastInteraction = when (interactions.lastOrNull()) {
is DragInteraction.Start -> "Dragged"
is PressInteraction.Press -> "Pressed"
else -> "No state"
}
Working through an example
To see how you can build components with a custom response to input, here's an example of a modified button. In this case, suppose you want a button that responds to presses by changing its appearance:
To do this, build a custom composable based on Button
, and have it take an
additional icon
parameter to draw the icon (in this case, a shopping cart). You
call collectIsPressedAsState()
to track whether the user is hovering over the
button; when they are, you add the icon. Here's what the code looks like:
@Composable
fun PressIconButton(
onClick: () -> Unit,
icon: @Composable () -> Unit,
text: @Composable () -> Unit,
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource =
remember { MutableInteractionSource() },
) {
val isPressed by interactionSource.collectIsPressedAsState()
Button(onClick = onClick, modifier = modifier,
interactionSource = interactionSource) {
AnimatedVisibility(visible = isPressed) {
if (isPressed) {
Row {
icon()
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
}
}
}
text()
}
}
And here's what it looks like to use that new composable:
PressIconButton(
onClick = {},
icon = { Icon(Icons.Filled.ShoppingCart, contentDescription = null) },
text = { Text("Add to cart") }
)
Because this new PressIconButton
is built on top of the existing Material
Button
, it reacts to user interactions in all the usual ways. When the user
presses the button, it changes its opacity slightly, just like an ordinary
Material Button
. In addition, thanks to the new code, the HoverIconButton
dynamically responds to the hover by adding an icon.
Recommended for you
- Note: link text is displayed when JavaScript is off
- Understand gestures
- Kotlin for Jetpack Compose
- Material Components and layouts