Trying to wrap my head around pointer input. Can a...
# compose
n
Trying to wrap my head around pointer input. Can anyone explain why the first child is not clickable? Ideally with a link to source code. I'm not consuming touch events in the second child, I'm not even collecting them.
Copy code
Box(Modifier.fillMaxSize()) {
    Box(Modifier.fillMaxSize().clickable { error("Yay!") })
    Box(Modifier.fillMaxSize().pointerInput(Unit) {})
}
I mean, even if parent Box decides that the second child should get events first (which is questionable, but okay), why does
pointerInput
block the flow?
k
The two child views are stacked on top of each other. So the topmost (second) child is probably ending up consuming all pointer events.
n
But it's not, you know? To consume the events you'd have to call
awaitPointerEventScope
inside pointerInput, collect them in a loop and then consume all PointerInputChanges. See for example:
Copy code
Box(Modifier.fillMaxSize().clickable { error("Yay!") }) {
    Box(Modifier.fillMaxSize().pointerInput(Unit) {})
}
In this case, the child is stacked on top of the parent, and it has priority for events (great!), but since my
pointerInput
implementation does not consume them, the Yay! part is reached, correctly. Issue shows up between siblings only.
a
cc @matvei
n
I'm thinking that these lines in InnerPlaceable are the culprit. By using any, only the first child is allowed to add their
PointerInputFilter
for a given offset, which means that the winner takes it all. This puts a hard boundary on the event flow between sibling nodes, ignoring the fact that the winning
PointerInputFilter
might not be consuming the events at all. It's a biiig difference with the View system, I hope you agree it's a bug 😄 assuming my analysis is correct
m
We have a node capture/locking logic in place. Initially, we hit test the tree down from the parent until we find the most inner child capable of the pointer input handling and that is hittested properly (pointer event in its bounds). We then lock on this child and run the evens up and down the tree from/to this child. Notice that we run up and down, not to the siblings. If you have a parent with two children (they are siblings to each other) which overlap and both hit test, the last one wins. For the Box the last one means the one on top of each other. So if you have pointer input handling on the top most child of the box - we will capture it and the children below will never receive anything regardless of the consumption. The equivalent of not being interested in the events in compose would be not to have modifier at all. The consumption of down or move events is a different mechanism for parent-child communication. And this is why even in compose we have, for example, a
ModalDrawer
that combines together both sliding "drawer" panel and the rest of the UI, because we have to handle the pointer input logic at the parent of both (
ModalDrawer
), and not on the sliding "drawer" panel itself, otherwise the rest of the content will never receive any events.
n
Thanks Matvei, I understand how it works now! But it's not very clear to me why it has to be so. If all input handlers check for consumption (which is what they should do), you'd be free to lift this barrier and pass the event to the next sibling. This enables interactions between siblings which are already possible in the old View system, to some extent. ViewGroup tests all children until one of them consumes the event.
m
View system is different in this regard. all Views by default has onTouchEvent and all potentially intercept pointer events. we HAVE to allow such propagation in views, otherwise system just won't work. It's not the case in compose. In compose, the presence/absence of the modifier is what dictates whether the child is interested or not about the pointer input events. It allows us to hit test faster (by essentially just finding next pointerEvent, skipping all the children), allows developers to express intent better (Modifier.pointerInput present - receive events; no modifier - no events) and it is not complicating the consumption logic (more about it below). The consumption logic is compose is what it should be ideally in views: it's the signal of consumption, that the event was processed and consumed, signalling to the pointer event propagation chain that this portion of the even is not available anymore. Regardless of the consumption, event will be propagated through all the cycles, and parents/children will be able to react to the fact that down/up or move portion of the event was consumed. It makes the consumption contract clear and easy to reason about, whereas in views, in contrast, consumption can sometimes signal for the "interest", and sometimes for the "actual consumption". Keep in mind that we're building a new ui framework, so we don't have to do the same at we have in view. In fact, we have to do quite the opposite 🙂 We have to learn from the previous mistakes and make a better APIs with clear contracts.
👍 1
n
When I compare with View I'm not talking about API design, just about being able to do the same things. The compose system is great, as you said, you can distinguish interest vs. consumption, you can granularly consume position vs. down, you can even react to consumption, expressive API for developers... But everything breaks once you do:
Copy code
Box(Modifier.fillMaxSize()) {
    Box(Modifier.fillMaxSize().pointerInput(Unit) {})
    Box(Modifier.fillMaxSize().pointerInput(Unit) {})
}
In this case I have very clearly expressed my developer intent to receive (not consume) inputs in the two boxes, but I will only receive in one of them. While in this case:
Copy code
Box(Modifier.fillMaxSize().pointerInput(Unit) {}) {
    Box(Modifier.fillMaxSize().pointerInput(Unit) {})
}
it suddenly works. This is confusing to me. I understand the performance gain, but I'm still not convinced 😅 not all sibling nodes have a
pointerInput
(this could make the loop faster) and if they do, that's a clear signal that they want to get the events. Thanks for answering!
z
(This thread would be awesome to capture on the doc site!)
👍 1
m
Agree Zach, cc @ppvi Jose, it seems like we have a candidate for a lower-level pointer API and behaviour overview 🙂 Anyway, filed bug to capture it either in the kotlin docs somewhere or in our overview docs written by DevRel: https://issuetracker.google.com/issues/193761473
p
ack!
n
I keep getting back to this thread whenever I have troubles with pointerInputs (not unfrequent). It would be very nice to have in depth documentation. I also wonder if something has changed recently - I did not go through all the internals again, but I see the function
shouldSharePointerInputWithSiblings
which I think did not exist at the time of the first post.
m
Right, this is a new API to allow exactly what is discussed in this thread, it should make a propogation between the siblings possible. Please do try if you have usecases for this API and let us know what you think 🙂
n
Great! I don’t see any public API to use it, is there? I saw
shouldSharePointerInputWithSiblings
in some hidden inner class, then I’d expect something like Modifier.pointerInput(share = true, …) maybe
p
@Florina cc
m
It is a public var on the
PointerInputFilter
itself: https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/[…]pose/ui/input/pointer/PointerEvent.kt;l=103?q=PointerEvent.kt This means that it is not easy to change in your regular
Modifier.pointerInput
, but tit is possible if you implement your own PointerInputFilter for your custom PointerInputModifier
🙏 1
There're very niche usecases that this thing unlocks, and we've yet to receive a usecase that would make it to be a good candidate for a more accessible api
k
@matvei a place where I would see this very helpful is with this library: https://github.com/canopas/Intro-showcase-view if the user clicks anywhere except the highlighted place then close intro otherwise do the action that is highlighted. I was not able to do this as the overlay consumes all the events.
m
interceptOutOfBoundsChildEvents
is not public and overridable in the
PointerInputModifier
. If you really want the overlay to propogate the event to the sublings below it - you can create your own modifiers and do this.
is now* public, sorry took me long enough to notice the typo 🙂