Can Jetpack Compose input modifiers be prevented from consuming input events?
Asked Answered
V

1

17

In the old Android View paradigm, a view could listen for MotionEvents without consuming them. DispatchTouchEvent or OnTouchEvent could simply return "false", and the MotionEvent would pass through layer after layer of view until something returned "true", at which time the MotionEvent was consumed and never passed onto lower-level views.

How do you do that in Jetpack Compose (JC)?

The JC touch listener Modifier.pointerInteropFilter consumes every MotionEvent it detects, and never passes MotionEvents down regardless of whether it returns "true" or "false. (This feels like a bug to me, but it's what we have to work with.)

JC listeners such as Modifier.pointerInput.detectDragGestures and Modifier.pointerInput.detectTapGestures all consume their input events when they detect a series of events that matches what they are looking for.

This makes it difficult (but surely not impossible?) to, for instance, conditionally consume an input event if it's made by a pen, but pass that event onto the next Composable (or even the next listener in the same Composable) if it's made by a finger.

This same question was asked here, in a narrower sense, but it was never answered and the OP still seems to be struggling to work around the limitation.

The only way I can think of to work around this bug/limitation is to manually pass MotionEvents onto other Composables.

In the old View paradigm, you could manually target a View's onTouchEvent method, and pass a MotionEvent directly to that view for handling. That was nice because, for instance, you could conditionally pass finger touches onto View, which would then take care of its own scrolling, but intercept all pen touches and use them to draw rather than scroll.

But how on earth do you pass input events directly to a Composable that is sitting underneath another Composable? Or, assuming I have to do all this in one,giant Composable rather than in layers of Composables, how do you even pass touch events around inside a Composable?

I'd be happy with either solution: not consuming input events/MotionEvents in the first place, and letting them pass down till something wants them; or manually passing them around.

But I can't figure out how to do either!

I'd also be satisfied with an answer telling me I'm erroneously bringing View-style thinking to Composables, but could you kindly outline how I should be thinking, when it comes to conditionally using input events and pointerInput modifiers to (in the example I've been using) either draw on a Composable, or scroll it?

Is there the equivalent of Python's super() feature, where you detect (for instance) a drag gesture, and if it's a drag made by a finger, do the Composable's default (ie scroll)?

Varro answered 7/11, 2021 at 1:7 Comment(4)
Methods like detectDragGestures are marking touches as consumed. If you need to override this behaviour you can check out this answer. It's about Compose Desktop but in Android it work the same. – Vitrics
So you want to be able to draw and scroll on the same location on the screen? Is that even possible under the older view system? I personally cannot recall seeing an app where you can draw and scroll on the same location. Can you point me to an app that does that? – Bove
Most pen-based handwriting apps, such as Samsung's Notes apps, will write with a pen and scroll with finger gestures. It's straightforward with Views, but it appears to be tricky (to say the least) with Jetpack Compose. – Varro
Hmm have you raised an issue on issueTracker? This seems like an oversight for a lot of use cases. I'm trying to overlay a transparent composable on top of Views, and there seems to be no way to conditionally pass through touches πŸ˜₯ – Illuminism
M
3

As of androidx.compose.ui:ui:1.4.3, Modifier.pointerInteropFilter() passes the MotionEvent down if we return false from onTouchEvent().

Column {
  val msg = remember { mutableStateOf("") }

  Box(
      Modifier.fillMaxWidth().height(200.dp).padding(12.dp).background(Color.Cyan).clickable {
        msg.value += "Bottom Layout received a click\n"
      },
      contentAlignment = Alignment.Center) {
        Box(Modifier.fillMaxSize().padding(4.dp), contentAlignment = Alignment.TopStart) {
          Text("Bottom Layout")
        }

        Box(
            Modifier.fillMaxSize(0.6f).background(Color.Green).pointerInteropFilter {
              msg.value +=
                  "Top Layout received ${
                        when (it.action) {
                            0 -> "a down"
                            1 -> "an up"
                            else -> "a"
                        }
                    } touch event\n"

              // Must return false here to pass the event to bottom layout
              false
            },
            contentAlignment = Alignment.Center) {
              Text("Top Layout")
            }
      }

  Text(msg.value, Modifier.padding(12.dp))
}

Result:

When true is returned:

When false is returned:

I used the same to control the position of DropDownMenu.

Mig answered 15/8, 2023 at 11:51 Comment(1)
Unfortunately if your Column is in another Box and there are views behind it, they won't receive any events. The same can be said about all the views behind ComposeView – Horgan

© 2022 - 2024 β€” McMap. All rights reserved.