Skip to content

Instantly share code, notes, and snippets.

@luboganev
Created December 5, 2023 18:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save luboganev/2b7e32c287d323f5d98e5fb21652bc15 to your computer and use it in GitHub Desktop.
Save luboganev/2b7e32c287d323f5d98e5fb21652bc15 to your computer and use it in GitHub Desktop.
Jetpack Compose custom focus and other interaction states
/**
* A convenience wrapper around the [Interaction] types
*/
@Immutable
data class CustomInteraction(
val enabled: Boolean = false,
val hovered: Boolean = false,
val pressed: Boolean = false,
val focused: Boolean = false,
)
/**
* A custom handler for [MutableInteractionSource], which supports custom visualizing
* pressed, focused, hover and enabled states of an interactive UI component.
*/
@Composable
fun rememberCustomInteractionState(
enabled: Boolean,
interactionSource: MutableInteractionSource,
): State<CustomInteraction> {
val customInteractionState = remember { mutableStateOf(CustomInteraction()) }
val interactions = remember { mutableStateListOf<Interaction>() }
LaunchedEffect(interactionSource) {
interactionSource.interactions.collect { interaction ->
when (interaction) {
is HoverInteraction.Enter -> {
interactions.add(interaction)
}
is HoverInteraction.Exit -> {
interactions.remove(interaction.enter)
}
is FocusInteraction.Focus -> {
interactions.add(interaction)
}
is FocusInteraction.Unfocus -> {
interactions.remove(interaction.focus)
}
is PressInteraction.Press -> {
interactions.add(interaction)
}
is PressInteraction.Release -> {
delay(KEEP_PRESSED_STATE_DELAY_MILLIS)
interactions.remove(interaction.press)
}
is PressInteraction.Cancel -> {
delay(KEEP_PRESSED_STATE_DELAY_MILLIS)
interactions.remove(interaction.press)
}
}
}
}
if (!enabled) {
customInteractionState.value = CustomInteraction()
} else {
val isFocused = interactions.any { it is FocusInteraction.Focus }
val isPressed = interactions.any { it is PressInteraction.Press }
val isHovered = interactions.any { it is HoverInteraction.Enter }
customInteractionState.value = CustomInteraction(
enabled = true,
hovered = isHovered,
pressed = isPressed,
focused = isFocused,
)
}
return customInteractionState
}
// Without this, the change of state when just
// tapping is so fast, you can't even see a visual change in the UI.
// Remember, the ripple is an animation that starts when you tap but runs also
// after you've tapped, while simple color changes are instant.
private const val KEEP_PRESSED_STATE_DELAY_MILLIS = 100L
val interactionSource = remember { MutableInteractionSource() }
val customInteractionState by rememberCustomInteractionState(enabled = true, interactionSource = interactionSource)
val color by remember {
derivedStateOf {
if (customInteractionState.focused) Color.Blue else Color.Green
}
}
Box(
modifier = Modifier
.background(color)
.clickable(
interactionSource = interactionSource,
indication = null, // That one's important to prevent the default ripple
onClick = {},
)
) {
Text(
text = "Some content",
style = MaterialTheme.typography.h1
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment