SoFunction
Updated on 2025-05-23

Kotlin Compose Button implements long press and monitor and realizes animation effects (full code)

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 usedSurfaceCome and wrap the content and manage visual expressions uniformly.

2. InteractionSource

  • InteractionSourceIt is a mechanism in Compose to manage user interaction status (e.g.pressedhoveredfocused
  • ButtonPass it inSurface, used to respond and process ripple animations, etc.
  • andMutableInteractionSourceCooperate, you can observe the changes in the state of the component

3. ButtonDefaults

ButtonDefaultsIt is a tool class that provides default values, including:

  • elevation():returnButtonElevationObject, used to set the shadow height in different states
  • buttonColors():returnButtonColorsObject, 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)

ButtonofcontentIt's oneRowScopelambda allows you to freely combine subcomponents such as:

Button(onClick = { }) {
    Icon(imageVector = , contentDescription = null)
    Spacer(modifier = ())
    Text("Add to")
}

Because it'sRowScopeSo it can be usedSpacerThe 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 customizeButtonstyle, you can also use it directlySurface + RowImplement 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 throughanimateColorAsStateTo 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 passInteractionSourceCombinedcollectIsPressedAsState()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:passanimateColorAsState()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 thisanimatedColorApplied toSurfaceOr 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 passesanimateColorAsStateTo 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 (targetContainerColorandtargetContentColor). 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,animateColorAsStateAutomatic 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: () -&gt; Unit = {},
    onLongPress: () -&gt; Unit = {},
    onPressed: () -&gt; Unit = {},
    onReleased: () -&gt; Unit = {},
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    shape: Shape = ,
    colors: ButtonColors = (),
    border: BorderStroke? = null,
    shadowElevation: Dp = ,
    contentPadding: PaddingValues = ,
    interactionSource: MutableInteractionSource? = null,
    content: @Composable RowScope.() -&gt; 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 -&gt; disabledContainerColor
        isPressed -&gt; (alpha = 0.85f)
        else -&gt; defaultContainerColor
    }
    val targetContentColor = when {
        !enabled -&gt; disabledContentColor
        isPressed -&gt; (alpha = 0.9f)
        else -&gt; 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 -&gt;
                        // 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!