I want to implement the function of long pressing the button to start recording and releasing the sending function. I found that if the Button control listens to these presses, releases, long presses and other events, it will be found that it will not trigger. The reason is that Button has consumed these events in advance, so these listening cannot be triggered. Therefore, in order to implement these functions, you need to write a Button yourself to solve the problem.
Button implementation principle
In Jetpack Compose, Button is a highly encapsulated composable function (Composable). Its underlying layer is composed of multiple UI components. The key components include: Surface, Text, Row, InteractionSource, etc.
Source code
@Composable fun Button( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, shape: Shape = , colors: ButtonColors = (), elevation: ButtonElevation? = (), border: BorderStroke? = null, contentPadding: PaddingValues = , interactionSource: MutableInteractionSource? = null, content: @Composable RowScope.() -> Unit ) { @Suppress("NAME_SHADOWING") val interactionSource = interactionSource ?: remember { MutableInteractionSource() } val containerColor = (enabled) val contentColor = (enabled) val shadowElevation = elevation?.shadowElevation(enabled, interactionSource)?.value ?: Surface( onClick = onClick, modifier = { role = }, enabled = enabled, shape = shape, color = containerColor, contentColor = contentColor, shadowElevation = shadowElevation, border = border, interactionSource = interactionSource ) { ProvideContentColorTextStyle( contentColor = contentColor, textStyle = ) { Row( ( minWidth = , minHeight = ) .padding(contentPadding), horizontalArrangement = , verticalAlignment = , content = content ) } } }
1. The role of Surface (key)
Surface is a common container in Compose, responsible for:
- Provide background color (from ButtonColors)
- Provide elevation (shadow)
- Provide click behavior (via onClick)
- Provide shape (rounded corners, cropping, etc.)
- Provide ripple effect (using rememberRipple() automatically through indication)
- Use to achieve interactive response
Note: Almost all Material components will be usedSurface
Come and wrap the content and manage visual expressions uniformly.
2. InteractionSource
-
InteractionSource
It is a mechanism in Compose to manage user interaction status (e.g.pressed
、hovered
、focused
) -
Button
Pass it inSurface
, used to respond and process ripple animations, etc. - and
MutableInteractionSource
Cooperate, you can observe the changes in the state of the component
3. ButtonDefaults
ButtonDefaults
It is a tool class that provides default values, including:
-
elevation()
:returnButtonElevation
Object, used to set the shadow height in different states -
buttonColors()
:returnButtonColors
Object, used to set background and text colors in normal/disabled state -
ContentPadding
: The default margin of content 4. Content Slot(RowScope.() -> Unit
)
4. Content Slot(RowScope.() -> Unit)
Button
ofcontent
It's oneRowScope
lambda allows you to freely combine subcomponents such as:
Button(onClick = { }) { Icon(imageVector = , contentDescription = null) Spacer(modifier = ()) Text("Add to") }
Because it'sRowScope
So it can be usedSpacer
The layout function arranges the subitems horizontally.
Key points | illustrate |
---|---|
Surface |
Provides unified packaging for background, shadow, rounded corners, clicks, and ripple effects |
InteractionSource |
Used to collect user interaction status (click, hover, etc.) |
ButtonDefaults |
Provide default color, shadow, Padding and other parameters |
Row + Text
|
Content layout, allowing for flexible combination of icons + text |
Modifier |
Control size, shape, margins, click response, etc. |
If you want to customizeButton
style, you can also use it directlySurface
+ Row
Implement a "button" yourself and just assemble it according to the official method.
@Suppress("DEPRECATION_ERROR") @OptIn(ExperimentalMaterial3Api::class) @Composable fun Button( onClick: () -> Unit = {}, onLongPress: () -> Unit = {}, onPressed: () -> Unit = {}, onReleased: () -> Unit = {}, modifier: Modifier = Modifier, enabled: Boolean = true, shape: Shape = , colors: ButtonColors = (), border: BorderStroke? = null, shadowElevation: Dp = , contentPadding: PaddingValues = , content: @Composable RowScope.() -> Unit = { Text("LongButton") } ) { val containerColor = val contentColor = Surface( modifier = modifier .minimumInteractiveComponentSize() .pointerInput(enabled) { detectTapGestures( onPress = { offset -> onPressed() tryAwaitRelease() onReleased() }, onTap = { onClick() }, onLongPress = { onLongPress() } ) } .semantics { role = }, shape = shape, color = containerColor, contentColor = contentColor, shadowElevation = shadowElevation, border = border, ) { CompositionLocalProvider( LocalContentColor provides contentColor, LocalTextStyle provides (), ) { Row( Modifier .defaultMinSize(, ) .padding(contentPadding), horizontalArrangement = , verticalAlignment = , content = content ) } } }
Button's animation implementation
To allow the button to provide natural visual feedback when pressed, Compose usually uses state-driven animations. The most common way is throughanimateColorAsState
To achieve a smooth transition of colors, such as the background color or text color slightly darkened when the button is pressed, and then restored when it is released.
The key points of this animation implementation are:
-
Interaction status: For example, whether to press or disable, you can pass
InteractionSource
CombinedcollectIsPressedAsState()
Listen to the current status in real time. - Determine the target color based on the status: When the state changes (such as pressing -> Release), we will set a new target color.
-
Drive state changes using animation:pass
animateColorAsState()
Turn color changes into state changes with transitional effects, rather than mutations.
This approach conforms to Compose's declarative programming model, and does not require manual animation writing, but allows state-driven UI animation.
The following is a code snippet of the button color animation part, which only displays the relevant state monitoring and animation logic. How to apply it in the UI will be implemented in the subsequent implementation:
@Composable fun AnimatedButtonColors( enabled: Boolean, interactionSource: InteractionSource, defaultContainerColor: Color, pressedContainerColor: Color, disabledContainerColor: Color ): State<Color> { val isPressed by () val targetColor = when { !enabled -> disabledContainerColor isPressed -> pressedContainerColor else -> defaultContainerColor } // Returns a state-driven animation color val animatedColor by animateColorAsState(targetColor, label = "containerColorAnimation") return rememberUpdatedState(animatedColor) }
It is worth mentioning that the animation type used by Button is ripple (ripples effect)
This code is only responsible for calculating the current button background color and making it smooth transitions through animation. It does not directly control the button's click or layout logic, but instead provides an animated color state for the final UI.
You can then use thisanimatedColor
Applied toSurface
Or on the background Modifier, complete the overall button appearance animation.
Complete animation code
// 1. Make sure interactionSource is not emptyval interaction = interactionSource ?: remember { MutableInteractionSource() } // 2. Listen to press statusval isPressed by () // 4. Select the target value by statusval defaultContainerColor = val disabledContainerColor = val defaultContentColor = val disabledContentColor = val targetContainerColor = when { !enabled -> disabledContainerColor isPressed -> (alpha = 0.85f) else -> defaultContainerColor } val targetContentColor = when { !enabled -> disabledContentColor isPressed -> (alpha = 0.9f) else -> defaultContentColor } // 5. Animationval containerColorAni by animateColorAsState(targetContainerColor, label = "containerColor") val contentColorAni by animateColorAsState(targetContentColor, label = "contentColor") // Ripple effect// Choose whether to use ripple() of the new version of Material3 or return to the old version of rememberRipple() implementation based on the current environmentval ripple = if () { rememberRipple(true, , ) } else { ripple(true, , ) } // 6. Surface + Manually send PressInteractionSurface( modifier = modifier .minimumInteractiveComponentSize() .pointerInput(enabled) { detectTapGestures( onPress = { offset -> // Initiate PressInteraction for collectIsPressedAsState to listen val press = (offset) val scope = CoroutineScope(coroutineContext) { (press) } // User onPressed onPressed() // Wait for your finger to lift or cancel tryAwaitRelease() // Send ReleaseInteraction { ((press)) } // User onReleased onReleased() }, onTap = { onClick() }, onLongPress = { onLongPress() } ) } .indication(interaction, ripple) .semantics { role = }, shape = shape, color = containerColorAni, contentColor = contentColorAni, shadowElevation = shadowElevation, border = border, ) {...}
The animation part of this Button is mainly reflected in the color transition in the pressed state. It passesanimateColorAsState
To achieve dynamic changes in background color and text color.
When the button is pressed, it will be used()
Listen in real time whether it is in Pressed state, and then dynamically calculate the target color (targetContainerColor
andtargetContentColor
). When pressed, the color will reduce transparency (background alpha = 0.85, text alpha = 0.9), forming visual feedback on pressing.
The gradient of color is not a mutation, but with transition animation,animateColorAsState
Automatic drive. It smoothly transitions to the target value through the internal animation interpolator when the target color changes, and the user does not need to manually control the animation process.
useby animateColorAsState(...)
What I got isState<Color>
A value of type, which will automatically reorganize when the color changes, making the entire button appear more natural in the interaction.
This method is simpler, more declarative than traditional manual animation, and easier to integrate with Compose's state system.
Complete code
// .material3: 1.3.0 import import import import import import import import import import import import import import import . import . import . import .material3.ExperimentalMaterial3Api import . import . import . import . import . import . import . import . import import import import import import import import import import import import import import import import import @Suppress("DEPRECATION_ERROR") @OptIn(ExperimentalMaterial3Api::class) @Composable fun Button( onClick: () -> Unit = {}, onLongPress: () -> Unit = {}, onPressed: () -> Unit = {}, onReleased: () -> Unit = {}, modifier: Modifier = Modifier, enabled: Boolean = true, shape: Shape = , colors: ButtonColors = (), border: BorderStroke? = null, shadowElevation: Dp = , contentPadding: PaddingValues = , interactionSource: MutableInteractionSource? = null, content: @Composable RowScope.() -> Unit = { Text("LongButton") } ) { // 1. Make sure interactionSource is not empty val interaction = interactionSource ?: remember { MutableInteractionSource() } // 2. Listen to press status val isPressed by () // 4. Select the target value by status val defaultContainerColor = val disabledContainerColor = val defaultContentColor = val disabledContentColor = val targetContainerColor = when { !enabled -> disabledContainerColor isPressed -> (alpha = 0.85f) else -> defaultContainerColor } val targetContentColor = when { !enabled -> disabledContentColor isPressed -> (alpha = 0.9f) else -> defaultContentColor } // 5. Animation val containerColorAni by animateColorAsState(targetContainerColor, label = "containerColor") val contentColorAni by animateColorAsState(targetContentColor, label = "contentColor") // Ripple effect // Choose whether to use ripple() of the new version of Material3 or return to the old version of rememberRipple() implementation based on the current environment val ripple = if () { rememberRipple(true, , ) } else { ripple(true, , ) } // 6. Surface + Manually send PressInteraction Surface( modifier = modifier .minimumInteractiveComponentSize() .pointerInput(enabled) { detectTapGestures( onPress = { offset -> // Initiate PressInteraction for collectIsPressedAsState to listen val press = (offset) val scope = CoroutineScope(coroutineContext) { (press) } // User onPressed onPressed() // Wait for your finger to lift or cancel tryAwaitRelease() // Send ReleaseInteraction { ((press)) } // User onReleased onReleased() }, onTap = { onClick() }, onLongPress = { onLongPress() } ) } .indication(interaction, ripple) .semantics { role = }, shape = shape, color = containerColorAni, contentColor = contentColorAni, shadowElevation = shadowElevation, border = border, ) { CompositionLocalProvider( LocalContentColor provides contentColorAni, LocalTextStyle provides (), ) { Row( Modifier .defaultMinSize(, ) .padding(contentPadding), horizontalArrangement = , verticalAlignment = , content = content ) } } }
This is the article about Kotlin Compose Button implementing long press monitoring and achieving animation effects. For more related Kotlin Compose Button, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!