Search code examples
androidkotlinandroid-jetpack-composecompose-multiplatform

How to completely consume touch events in Jetpack Compose to prevent child clicks?


I am trying to intercept and consume all click events at the parent level in Jetpack Compose so that child composables (like a Button) do not receive clicks. However, even after calling consume() in awaitPointerEventScope, the child still receives the click event.

Here is my code:

@Composable
fun ParentConsumesClick() {
    Box(
        modifier = Modifier
            .fillMaxSize()
            .pointerInput(Unit) {
                awaitPointerEventScope {
                    while (true) {
                        val event = awaitPointerEvent()
                        Log.d("Click", "Consumed in Parent")
                        event.changes.forEach { it.consume() } // Consume touch event
                    }
                }
            }
            .background(Color.Gray),
        contentAlignment = Alignment.Center
    ) {
        Button(onClick = { Log.d("Click", "Child clicked") }) {
            Text("Click Me")
        }
    }
}

Expected Behavior:

  • Clicking anywhere inside the Box should log "Consumed in Parent".
  • The Button inside should not receive any clicks (i.e., "Child clicked" should not be logged).

Actual Behavior:

  • "Consumed in Parent" is logged (showing that the parent receives the event).
  • But the Button still receives the click event and logs "Child clicked".

Solution

  • You need to consume down event for clickable Modifier or Button to not receive click events.

    Also since gestures propagate from child to parent you need to change pass from Main to Initial for parent to receive touch first.

    You can check out this answer for more details about how gesture system works in Jetpack Compose.

    Create a Modifier such as

    private fun Modifier.customTouch(
        pass: PointerEventPass,
        onDown: () -> Unit = {},
        onUp: () -> Unit = {}
    ) = this.then(
        Modifier.pointerInput(pass) {
            awaitEachGesture {
                val down = awaitFirstDown(pass = pass)
                down.consume()
                onDown()
               val up = waitForUpOrCancellation(pass)
                if (up != null) {
                    onUp()
                }
            }
        }
    )
    

    And apply this Modifier to parent such as

    @Preview
    @Composable
    fun ParentConsumesClick() {
    
        val context = LocalContext.current
        Box(
            modifier = Modifier
                .fillMaxSize()
                .customTouch(
                    pass = PointerEventPass.Initial,
                    onDown = {
                        Toast
                            .makeText(context, "Parent Touched", Toast.LENGTH_SHORT)
                            .show()
                    },
                    onUp = {
                        Toast
                            .makeText(context, "Parent Up", Toast.LENGTH_SHORT)
                            .show()
                    }
                )
                .background(Color.Gray),
            contentAlignment = Alignment.Center
        ) {
            Button(onClick = {
                Toast.makeText(context, "Child clicked", Toast.LENGTH_SHORT).show()
            }) {
                Text("Click Me")
            }
        }
    }
    

    Also if you want to apply ripple on this click you can add InteractionSouce and Modifier.indication.