Reputation: 6962
Consider following Composable
.
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Blue)
.clickable {
// parent click listener
Timber.d("parent click")
}
) {
Box(
modifier = Modifier
.size(50.dp)
.background(Color.Red)
) {
// children
}
// ...
}
When clicking on the red Box
the parent Composable
consumes the click event even though the red box is in front of it.
This looks like a weird default behavior. Different from the View
system.
However when the red Box
has a click listener of its own the behavior is correct and the red Box
click listener is triggered.
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Blue)
.clickable {
// parent click listener
Timber.d("parent click")
}
) {
Box(
modifier = Modifier
.size(50.dp)
.background(Color.Red)
.clickable() {
Timber.d("Prevent parent clicks")
}
) {
// children
}
// ...
}
Is there a better way to let the red Box
consume the event without putting an explicit clickable
modifier on it?
Upvotes: 6
Views: 5335
Reputation: 66814
This is possible but requires much more work than using
val context = LocalContext.current
Box {
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Blue)
.clickable {
// parent click listener
Toast
.makeText(context, "Parent", Toast.LENGTH_SHORT)
.show()
}
) {
// ...
}
Box(
modifier = Modifier
.size(50.dp)
.background(Color.Red)
.clickable(enabled = false) {
Toast
.makeText(context, "Child", Toast.LENGTH_SHORT)
.show()
}
) {
// children
}
}
If you set what you set as parent and child as sibling when you click Blue Box you won't see ripple effect on Red Box.
The default event design in compose works with PointerInputchange.consume()
, and events propagate from descendant to ancestor by default. I explained how touch system works in Jetpack Compose in this answer.
The thing that i didn't put in to that answer is you can change propagatation direction using PointerEventPass but as i recall unless consumed it will still reach to deepest descendant
/**
* The enumeration of passes where [PointerInputChange] traverses up and down the UI tree.
*
* PointerInputChanges traverse throw the hierarchy in the following passes:
*
* 1. [Initial]: Down the tree from ancestor to descendant.
* 2. [Main]: Up the tree from descendant to ancestor.
* 3. [Final]: Down the tree from ancestor to descendant.
*
* These passes serve the following purposes:
*
* 1. Initial: Allows ancestors to consume aspects of [PointerInputChange] before descendants.
* This is where, for example, a scroller may block buttons from getting tapped by other fingers
* once scrolling has started.
* 2. Main: The primary pass where gesture filters should react to and consume aspects of
* [PointerInputChange]s. This is the primary path where descendants will interact with
* [PointerInputChange]s before parents. This allows for buttons to respond to a tap before a
* container of the bottom to respond to a tap.
* 3. Final: This pass is where children can learn what aspects of [PointerInputChange]s were
* consumed by parents during the [Main] pass. For example, this is how a button determines that
* it should no longer respond to fingers lifting off of it because a parent scroller has
* consumed movement in a [PointerInputChange].
*/
enum class PointerEventPass {
Initial, Main, Final
}
Upvotes: 2