'Button Long Press Listener in Android jetpack compose

I am having an Android Composable UI with a Button.

How can I track button long press events? I got it working for the Text long press, but for Button, It is not working. Same way like below if I apply a modifier to the button, it is not working.

Text(
    text = view.text,
    fontSize = view.textFontSize.toInt().sp,
    fontWeight = FontWeight(view.textFontWeight.toInt()),
    color = Color(android.graphics.Color.parseColor(view.textColor)),
    modifier = Modifier.clickable(
        onClick = {
            println("Single Click")
        }, 
        onLongClick = {
            println("Long Click")
        }, 
        onDoubleClick = {
            println("Double Tap")
        },
    ),
)


Solution 1:[1]

The best way to handle this is to roll your own Button. The Material Button is basically just a Surface and a Row. The reason adding your own Modifier.clickable doesn't work is because one is already set.

So, if you'd like to add onLongPress, etc you can copy/paste the default implementation and pass those lambdas in.

@Composable
@OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class)
fun Button(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    onLongClick: (() -> Unit)? = null,
    onDoubleClick: (() -> Unit)? = null,
    enabled: Boolean = true,
    interactionState: InteractionState = remember { InteractionState() },
    elevation: ButtonElevation? = ButtonDefaults.elevation(),
    shape: Shape = MaterialTheme.shapes.small,
    border: BorderStroke? = null,
    colors: ButtonColors = ButtonDefaults.buttonColors(),
    contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
    content: @Composable RowScope.() -> Unit
) {
    val contentColor by colors.contentColor(enabled)
    Surface(
        shape = shape,
        color = colors.backgroundColor(enabled).value,
        contentColor = contentColor.copy(alpha = 1f),
        border = border,
        elevation = elevation?.elevation(enabled, interactionState)?.value ?: 0.dp,
        modifier = modifier.combinedClickable(
            onClick = onClick,
            onDoubleClick = onDoubleClick,
            onLongClick = onLongClick,
            enabled = enabled,
            role = Role.Button,
            interactionState = interactionState,
            indication = null
        )
    ) {
        Providers(LocalContentAlpha provides contentColor.alpha) {
            ProvideTextStyle(
                value = MaterialTheme.typography.button
            ) {
                Row(
                    Modifier
                        .defaultMinSizeConstraints(
                            minWidth = ButtonDefaults.MinWidth,
                            minHeight = ButtonDefaults.MinHeight
                        )
                        .indication(interactionState, rememberRipple())
                        .padding(contentPadding),
                    horizontalArrangement = Arrangement.Center,
                    verticalAlignment = Alignment.CenterVertically,
                    content = content
                )
            }
        }
    }
}

Usage:

Button(
    onClick = {},
    onLongClick = {},
    onDoubleClick = {}
) {
    Text(text = "I'm a button")
}

Solution 2:[2]

You can use combinedClickable like the following:

Modifier
    .combinedClickable(
        onClick = { },
        onLongClick = { },
    )

Warning: with Compose 1.0.1 this method is marked as @ExperimentalFoundationApi so this answer may get outdated in the future releases.

Solution 3:[3]

https://developer.android.com/jetpack/compose/gestures can be used as well.

for example:

 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.foundation.gestures.detectTapGestures

 modifier = Modifier
           .weight(2f)
           .pointerInput(Unit){
               detectTapGestures(
                     onLongPress = {
                             // perform some action here..
                     }
               )
            }
                        

Solution 4:[4]

According to documentation

Modifier.pointerInput(Unit) {
        detectTapGestures(
            onPress = { /* Called when the gesture starts */ },
            onDoubleTap = { /* Called on Double Tap */ },
            onLongPress = { /* Called on Long Press */ },
            onTap = { /* Called on Tap */ }
        )
    }

Solution 5:[5]

5 months later, the accepted answer doesn't work because of API changes. detectTapGestures() on Button didn't work for me either (i guess .clickable() steals the event?).

Surface now has two public constructors. First one is not clickable and explicitly overrides .pointerInput(Unit) to be empty

Surface(
...
    clickAndSemanticsModifier = Modifier
        .semantics(mergeDescendants = false) {}
        .pointerInput(Unit) { detectTapGestures { } }
)

Second one (that is used by Button) is clickable and explicitly sets Modifier.clickable(). And if Button with detectTapGestures() doesn't work for you, this one won't work either.

There is a third private constructor that doesn't override your click events. So I ended up just stealing that and putting it next to custom LongPressButton.

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LongPressButton(
    modifier: Modifier = Modifier,
    onClick: () -> Unit = {},
    onLongPress: () -> Unit = {},
    onDoubleClick: () -> Unit = {},
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    elevation: ButtonElevation? = ButtonDefaults.elevation(),
    shape: Shape = MaterialTheme.shapes.small,
    border: BorderStroke? = null,
    colors: ButtonColors = ButtonDefaults.buttonColors(),
    contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
    content: @Composable RowScope.() -> Unit
) {
    val contentColor by colors.contentColor(enabled)
    Surface(
        modifier = modifier,
        shape = shape,
        color = colors.backgroundColor(enabled).value,
        contentColor = contentColor.copy(alpha = 1f),
        border = border,
        elevation = elevation?.elevation(enabled, interactionSource)?.value ?: 0.dp,
        clickAndSemanticsModifier = Modifier.combinedClickable(
            interactionSource = interactionSource,
            indication = rememberRipple(),
            enabled = enabled,
            role = Role.Button,
            onClick = onClick,
            onDoubleClick = onDoubleClick,
            onLongClick = onLongPress,
        )
    ) {
        CompositionLocalProvider(LocalContentAlpha provides contentColor.alpha) {
            ProvideTextStyle(
                value = MaterialTheme.typography.button
            ) {
                Row(
                    Modifier
                        .defaultMinSize(
                            minWidth = ButtonDefaults.MinWidth,
                            minHeight = ButtonDefaults.MinHeight
                        )
                        .padding(contentPadding),
                    horizontalArrangement = Arrangement.Center,
                    verticalAlignment = Alignment.CenterVertically,
                    content = content
                )
            }
        }
    }
}


@Composable
private fun Surface(
    modifier: Modifier,
    shape: Shape,
    color: Color,
    contentColor: Color,
    border: BorderStroke?,
    elevation: Dp,
    clickAndSemanticsModifier: Modifier,
    content: @Composable () -> Unit
) {
    val elevationOverlay = LocalElevationOverlay.current
    val absoluteElevation = LocalAbsoluteElevation.current + elevation
    val backgroundColor = if (color == MaterialTheme.colors.surface && elevationOverlay != null) {
        elevationOverlay.apply(color, absoluteElevation)
    } else {
        color
    }
    CompositionLocalProvider(
        LocalContentColor provides contentColor,
        LocalAbsoluteElevation provides absoluteElevation
    ) {
        Box(
            modifier
                .shadow(elevation, shape, clip = false)
                .then(if (border != null) Modifier.border(border, shape) else Modifier)
                .background(
                    color = backgroundColor,
                    shape = shape
                )
                .clip(shape)
                .then(clickAndSemanticsModifier),
            propagateMinConstraints = true
        ) {
            content()
        }
    }
}

If there is a better way that works, please share. Because current solution is ugly.

Solution 6:[6]

I tried @adneal answer and for some reason it wouldn't pick up the "onLongClick".

After some research I updated as follow to make it work :

@OptIn(ExperimentalMaterialApi::class, androidx.compose.foundation.ExperimentalFoundationApi::class)
@Composable
fun ButtonWithLongPress(
    onClick: () -> Unit,
    onDoubleClick:()->Unit = {},
    onLongClick: () -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    elevation: ButtonElevation? = ButtonDefaults.elevation(),
    shape: Shape = MaterialTheme.shapes.small,
    border: BorderStroke? = null,
    colors: ButtonColors = ButtonDefaults.buttonColors(),
    contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
    content: @Composable RowScope.() -> Unit
) {
    val contentColor by colors.contentColor(enabled)
Surface(
    onClick = { },
    modifier = modifier
        .combinedClickable(
            interactionSource,
        rememberRipple(),
        true,
        null,
        Role.Button,
        null,
        onClick = { onClick() },
        onLongClick = { onLongClick() },
        onDoubleClick = {onDoubleClick()}),
    enabled = enabled,
    shape = shape,
    color = colors.backgroundColor(enabled).value,
    contentColor = contentColor.copy(alpha = 1f),
    border = border,
    elevation = elevation?.elevation(enabled, interactionSource)?.value ?: 0.dp,
    interactionSource = interactionSource,
) {
    CompositionLocalProvider(LocalContentAlpha provides contentColor.alpha) {
        ProvideTextStyle(
            value = MaterialTheme.typography.button
        ) {
            Row(
                Modifier
                    .defaultMinSize(
                        minWidth = ButtonDefaults.MinWidth,
                        minHeight = ButtonDefaults.MinHeight
                    )
                    .padding(contentPadding)
                    .combinedClickable(interactionSource,
                        null,
                        true,
                        null,
                        Role.Button,
                        null,
                        onClick = { onClick() },
                        onLongClick = { onLongClick() },
                        onDoubleClick = { onDoubleClick() }),
                horizontalArrangement = Arrangement.Center,
                verticalAlignment = Alignment.CenterVertically,
                content = content
            )
        }
    }
}}

Now it works the way it should, and setting up a double-click is optional if necessary

Solution 7:[7]

This is my solution

@OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class)
@Composable
fun MyButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    onLongClick: () -> Unit = {},
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    elevation: ButtonElevation? = ButtonDefaults.elevation(),
    shape: Shape = MaterialTheme.shapes.small,
    border: BorderStroke? = null,
    colors: ButtonColors = ButtonDefaults.buttonColors(),
    contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
    content: @Composable RowScope.() -> Unit
) {
    val contentColor by colors.contentColor(enabled)
    var tapped by remember { mutableStateOf(false) }
    Surface(
        modifier = modifier
            .clip(shape)
            .indication(interactionSource, LocalIndication.current)
            .pointerInput(Unit) {
            detectTapGestures(
                onPress = { offset ->
                    tapped = true
                    val press = PressInteraction.Press(offset)
                    interactionSource.emit(press)
                    tryAwaitRelease()
                    interactionSource.emit(PressInteraction.Release(press))
                    tapped = false
                },
                onTap = { onClick() },
                onLongPress = { onLongClick() }
            )
        }
        ,
        shape = shape,
        color = colors.backgroundColor(enabled).value,
        contentColor = contentColor.copy(alpha = 1f),
        border = border,
        elevation = elevation?.elevation(enabled, interactionSource)?.value ?: 0.dp,
    ) {
        CompositionLocalProvider(LocalContentAlpha provides contentColor.alpha) {
            ProvideTextStyle(
                value = MaterialTheme.typography.button
            ) {
                Row(
                    Modifier
                        .defaultMinSize(
                            minWidth = ButtonDefaults.MinWidth,
                            minHeight = ButtonDefaults.MinHeight
                        )
                        .padding(contentPadding),
                    horizontalArrangement = Arrangement.Center,
                    verticalAlignment = Alignment.CenterVertically,
                    content = content
                )
            }
        }
    }
}

Simply override default button and use it when you need to catch click or longClick event

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Piotr Aleksander Chmielowski
Solution 3 Johann
Solution 4 Mehranjp73
Solution 5 Meegoo
Solution 6 Koch
Solution 7 Paolo Minel