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
:
- I use
awaitFirstDown(requireUnconsumed = false)
instead of default requireUnconsumed = true
to make sure we get even a consumed even
- I use my own
waitForUpOrCancellationInitial
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.
- Remove
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") }
}
}