Reputation: 1479
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
Reputation: 67413
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
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
Reputation: 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
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
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
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
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
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
Reputation: 6461
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
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
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