1

Note: this is very similar to this unanswered question, but that question is mixing events (onTap and onLongPress) and I don't know if that makes a difference or not.

I can react on tap events on an object with .pointerInput and detectTapGestures. With nested composables, the tap is consumed by the innermost child.

For example, if I nest a Box inside another Box, like so:

Box(
    modifier = Modifier
        .pointerInput(Unit) {
            detectTapGestures(onTap = {
                println("TAP in parent Box")
            })
        }
) {
    Box(
        modifier = Modifier
            .pointerInput(Unit) {
                detectTapGestures(onTap = {
                    println("TAP in child Box")
                })
            }
    ) {}
}

Then tapping the box will result in the innermost callback being triggered, showing "TAP in child Box".

How could I intercept the tap in the parent Box? And is there a way to not consume it, such that it goes through to the child?

JonasVautherin
  • 7,297
  • 6
  • 49
  • 95
  • You can't do this with default detectTapGestures. Because gestures are propagated from child to parent by default as you can see in [this answer](https://stackoverflow.com/a/70847531/5457853) You need to change PointerEventPass from Main to Initial to change propagation direction from parent to child and write a function that consumes conditionally. I posted similar one for transformGestures, it's not that difficult to write one for tap as well. https://stackoverflow.com/a/76021552/5457853 – Thracian May 02 '23 at 06:37
  • Thanks for the links! However, it seems like your solutions are intercepting the "complex" gestures (scroll, zoom) and letting the "easy" ones go through (e.g. a tap or a long press), right? In my case I want to consume only the tap. Does that mean that I need to implement my own timer there? I'd like my custom tap to use the same timing as the default ones, though... – JonasVautherin May 02 '23 at 08:08
  • It's not based on if they are easy or complex. In the link i posted i'm consuming in parent based on users preference. It applies to every pointerEventChange. If you inspect detectTapGestures source code you will see that with `waitForUpOrCancellation` it checks whether `awaitPointerEvent` is consumed before canceling gesture. This is how gestures mostly work. They check if it's consumed event that propagated to them is consumed before and if it's consumed they either return null or do nothing. This is how you intercept propagation. By consuming instead of letting another gesture gets event. – Thracian May 02 '23 at 08:42
  • I understand that, but it seems like your code is lower level than a "tap". I can detect a "down" and an "up", but not a "tap". So I assume that to detect a tap, I need to somehow detect a "down", start a timer, and if it goes up fast enough I consume both events and make that a tap? It's not completely clear to me yet. – JonasVautherin May 02 '23 at 09:29
  • Sure. detectTransformGestures checks 2 things for tap. First, if it has been down long enough to be a long press with timeout function, if it has been moved out of Composable before timeout. If it's up before timeout and up inside Composable it's a tap. There are more details for second tap and pressscope but basically what tap is as you described. And between you can set consume param or PointerEventPass to intercept or change propagation direction – Thracian May 02 '23 at 09:40
  • I posted a custom long press to detect if gesture has ended. They are all similar. https://stackoverflow.com/a/75781146/5457853 – Thracian May 02 '23 at 09:41
  • And code is not lower than tap. That's how you do transform functions. Basics are same for almost every gesture. Get awaitFirstDown, then awaitPointeEvent or if you need second down get another awaitFirstDown and implement your logic between. Get zoom, centroid, rotation, etc., get time for down, consume/intercept or change propagation direction are implementation details based on your logic. – Thracian May 02 '23 at 09:51
  • I got something to do roughly what I want (still working into detecting the double tap). Feel free to comment there, and thanks a lot for the help @Thracian! – JonasVautherin May 03 '23 at 21:15

1 Answers1

1

Following @Thracian's guidance, I got to the following, which detects a tap (making the difference with a "slop" and a long press, but interpreting a double tap as two taps):

suspend fun PointerInputScope.interceptTap(
    pass: PointerEventPass = PointerEventPass.Initial,
    onTap: ((Offset) -> Unit)? = null,
) = coroutineScope {
    if (onTap == null) return@coroutineScope

    awaitEachGesture {
        val down = awaitFirstDown(pass = pass)
        val downTime = System.currentTimeMillis()
        val tapTimeout = viewConfiguration.longPressTimeoutMillis
        val tapPosition = down.position

        do {
            val event = awaitPointerEvent(pass)
            val currentTime = System.currentTimeMillis()

            if (event.changes.size != 1) break // More than one event: not a tap
            if (currentTime - downTime >= tapTimeout) break // Too slow: not a tap

            val change = event.changes[0]

            // Too much movement: not a tap
            if ((change.position - tapPosition).getDistance() > viewConfiguration.touchSlop) break

            if (change.id == down.id && !change.pressed) {
                change.consume()
                onTap(change.position)
            }
        } while (event.changes.any { it.id == down.id && it.pressed })
    }
}

Note that it only consumes the event when the main pointer is released. So the "down" event is passed through to the descendants, as are all the events that are not an "up" event of this same pointer, within the tap timeout and slop limit.

JonasVautherin
  • 7,297
  • 6
  • 49
  • 95