timr
timr

Reputation: 6962

Prevent parent Compose click handler in children

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
    }

    // ...
}

enter image description here

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

Answers (1)

Thracian
Thracian

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

Related Questions