SoftwareGuy
SoftwareGuy

Reputation: 1479

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")
        },
    ),
)

Upvotes: 68

Views: 49506

Answers (11)

Thracian
Thracian

Reputation: 67413

enter image description here

You can do it with passing InteractionSource and collecting it as

val context = LocalContext.current
val interactionSource = remember { MutableInteractionSource() }
val viewConfiguration = LocalViewConfiguration.current

LaunchedEffect(interactionSource) {
    var isLongClick = false

    interactionSource.interactions.collectLatest { interaction ->
        when (interaction) {
            is PressInteraction.Press -> {
                isLongClick = false
                delay(viewConfiguration.longPressTimeoutMillis)
                isLongClick = true
                Toast.makeText(context, "Long click", Toast.LENGTH_SHORT).show()
            }

            is PressInteraction.Release -> {
                if (isLongClick.not()) {
                    Toast.makeText(context, "click", Toast.LENGTH_SHORT).show()
                }
            }
        }
    }
}

Button(
    onClick = {},
    interactionSource = interactionSource
) {
    Text("Some button")
}

Upvotes: 14

Bakunya
Bakunya

Reputation: 11

Because combinedClickable is still an experimental API, You can use detectTapGestures within pointerInput(Unit) on Modifier, then detects the time used from the time the user presses until releasing it. For example:

.pointerInput(Unit) {
    detectTapGestures(
        onPress = {
            val startTime = System.currentTimeMillis()
            tryAwaitRelease()
            val endTime = System.currentTimeMillis()

            if (endTime - startTime > 1000) {
                println("Long Press Detected")
            } else {
                println("Short Press Detected")
            }
        }
    )
}

Upvotes: 1

The way I've done it is making a button via a Canvas and with the modifiers give the canvas the LongClick functionality, keep in mind that a Click action is necessary, altough you can leave it empty. Hope this solution helps.

    @OptIn(ExperimentalFoundationApi::class)
    @Composable
    fun LongPressButton(runViewModel: RunActivityScreenViewModel, context: Context) {
        var actionState by remember { mutableStateOf(ActivityState.PLAY) }
        val buttonColorPlay = MaterialTheme.colors.primary
        val buttonColorStop = MaterialTheme.colors.secondaryVariant
        val shadow = Color.Black.copy(.5f)
        val stopIcon: ImageBitmap = ImageBitmap.imageResource(id = R.drawable.stop_icon)
        val playIcon: ImageBitmap = ImageBitmap.imageResource(id = R.drawable.play_icon)
        Canvas(
            modifier = Modifier
                .size(100.dp)
                .combinedClickable(
                    interactionSource = MutableInteractionSource(),
                    onClick = {
                       // Your Click Action
                    },
                    onLongClick = {
                       // Your Long Press Action
                    },
                    indication = rememberRipple(
                        bounded = false,
                        radius = 20.dp,
                        color = MaterialTheme.colors.onSurface
                    ),
                ),
        ) {
            drawCircle(
                color = shadow,
                radius = 100f,
                center = Offset(
                    center.x + 2f,
                    center.y + 2f
                )
            )
            drawCircle(
                color = buttonColorStop,
                radius = 100f
            )
            scale(scale = 2.5f) {
                drawImage(
                    image = stopIcon,
                    topLeft = Offset(
                        center.x - (stopIcon.width / 2),
                        center.y - (stopIcon.width / 2)
                    ),
                    alpha = 1f,
                )
            }
        }
    }

Upvotes: 0

Andrey Orel
Andrey Orel

Reputation: 115

For some reasons it doesn't work with Button, but you can recreate button with Card composable and apply combinedClickable modifier to it.

ElevatedCard(
    modifier = modifier
        .fillMaxHeight()
        .padding(4.dp)
        .clip(RoundedCornerShape(20.dp))
        .combinedClickable(
            interactionSource = remember {
                MutableInteractionSource()
            },
            indication = rememberRipple(bounded = true),
            onClick = onClick,
            onLongClick = onLongClick)
) {
    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        content()
    }
}

Upvotes: 10

Paolo Minel
Paolo Minel

Reputation: 91

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

Upvotes: 4

Koch
Koch

Reputation: 594

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

Upvotes: 1

Santosh Pillai
Santosh Pillai

Reputation: 8663

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..
                     }
               )
            }
                        

Upvotes: 32

Mehranjp73
Mehranjp73

Reputation: 521

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 */ }
        )
    }

Upvotes: 12

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.

Upvotes: 108

  Meegoo
Meegoo

Reputation: 417

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.

Upvotes: 3

adneal
adneal

Reputation: 30814

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")
}

Upvotes: 20

Related Questions