Reputation: 142
The below code is for Jetbrains Desktop Compose. It shows a card with a button on it, right now if you click the card "clicked card" will be echoed to console. If you click the button it will echo "Clicked button"
However, I'm looking for a way for the card to detect the click on the button. I'd like to do this without changing the button so the button doesn't need to know about the card it's on. I wish to do this so the card knows something on it's surface is handled and for example show a differently colored border..
The desired result is that when you click on the button the log will echo both the "Card clicked" and "Button clicked" lines. I understand why mouseClickable
isn't called, button declares the click handled. So I'm expecting that I'd need to use another mouse method than mouseClickable
. But I can't for the life of me figure out what I should be using.
@OptIn(ExperimentalComposeUiApi::class, androidx.compose.foundation.ExperimentalDesktopApi::class)
@Composable
fun example() {
Card(
modifier = Modifier
.width(150.dp).height(64.dp)
.mouseClickable { println("Clicked card") }
) {
Column {
Button({ println("Clicked button")}) { Text("Click me") }
}
}
}
Upvotes: 7
Views: 11685
Reputation: 88407
When button finds tap event, it marks it as consumed, which prevents other views from receiving it. This is done with consumeDownChange()
, you can see detectTapAndPress
method where this is done with Button
here
To override the default behaviour, you had to reimplement some of gesture tracking. List of changes comparing to system detectTapAndPress
:
awaitFirstDown(requireUnconsumed = false)
instead of default requireUnconsumed = true
to make sure we get even a consumed evenwaitForUpOrCancellationInitial
instead of waitForUpOrCancellation
: here I use awaitPointerEvent(PointerEventPass.Initial)
instead of awaitPointerEvent(PointerEventPass.Main)
, in order to get the event even if an other view will get it.up.consumeDownChange()
to allow the button to process the touch.Final code:
suspend fun PointerInputScope.detectTapAndPressUnconsumed(
onPress: suspend PressGestureScope.(Offset) -> Unit = NoPressGesture,
onTap: ((Offset) -> Unit)? = null
) {
val pressScope = PressGestureScopeImpl(this)
forEachGesture {
coroutineScope {
pressScope.reset()
awaitPointerEventScope {
val down = awaitFirstDown(requireUnconsumed = false).also { it.consumeDownChange() }
if (onPress !== NoPressGesture) {
launch { pressScope.onPress(down.position) }
}
val up = waitForUpOrCancellationInitial()
if (up == null) {
pressScope.cancel() // tap-up was canceled
} else {
pressScope.release()
onTap?.invoke(up.position)
}
}
}
}
}
suspend fun AwaitPointerEventScope.waitForUpOrCancellationInitial(): PointerInputChange? {
while (true) {
val event = awaitPointerEvent(PointerEventPass.Initial)
if (event.changes.fastAll { it.changedToUp() }) {
// All pointers are up
return event.changes[0]
}
if (event.changes.fastAny { it.consumed.downChange || it.isOutOfBounds(size) }) {
return null // Canceled
}
// Check for cancel by position consumption. We can look on the Final pass of the
// existing pointer event because it comes after the Main pass we checked above.
val consumeCheck = awaitPointerEvent(PointerEventPass.Final)
if (consumeCheck.changes.fastAny { it.positionChangeConsumed() }) {
return null
}
}
}
P.S. you need to add implementation("androidx.compose.ui:ui-util:$compose_version")
for Android Compose or implementation(compose("org.jetbrains.compose.ui:ui-util"))
for Desktop Compose into your build.gradle.kts
to use fastAll
/fastAny
.
Usage:
Card(
modifier = Modifier
.width(150.dp).height(64.dp)
.clickable { }
.pointerInput(Unit) {
detectTapAndPressUnconsumed(onTap = {
println("tap")
})
}
) {
Column {
Button({ println("Clicked button") }) { Text("Click me") }
}
}
Upvotes: 18