2

I am trying to have a checkbox with some trailing text. The idea is that whether you select the checkbox itself, or the text, the value is toggled. For brevity in code I've done everything in the composable.

It works as expected when selecting the text. But when I specifically press the checkbox, nothing happens. I see the ripple effect from pressing the checkbox but it is obviously ignoring the row's clickable function and hitting onCheckChange = {}

I'm not entirely fluent in composable scopes so I thought it might be due to Row() being an inline function so I tried applying the same modifier to the Surface but that doesn't work either.

It isn't a big deal having the same call from the 2 spots, I would just like to know why.

@Composable
fun CheckboxInRowWithText() {
    var checkedState by remember { mutableStateOf(false) }
    Surface {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .clickable {
                    checkedState = !checkedState
                },
            verticalAlignment = Alignment.CenterVertically) {
            Checkbox(checked = checkedState, onCheckedChange = {} )

            Text(text = "Clicking this Row will toggle checkbox")
        }
    }
}

2 Answers2

2

The reason you can't click an ancestor Composable is not because it's under another, touch propagation goes from descendant to ancestor by default because of PointerEventPass which is Main.

The reason Modifier.clickable doesn't allow parent to get event is, it calls PointerEventChange.consume but you can write your own click functions to propagate from child to parent or parent to child or consume conditionally.

You can check out this answer for more details

You are basically doing nothing on checkBoxChange that's why it doesn't change, set checkedState inside onCheckedChange.

@Composable
fun CheckboxInRowWithText() {

    val context = LocalContext.current

    var checkedState by remember { mutableStateOf(false) }
    Surface {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .clickable {
                    Toast
                        .makeText(context, "Row clicked", Toast.LENGTH_SHORT)
                        .show()
                    checkedState = !checkedState
                },
            verticalAlignment = Alignment.CenterVertically) {
            Checkbox(checked = checkedState, onCheckedChange = {
                Toast.makeText(context, "Checkbox check", Toast.LENGTH_SHORT).show()
                checkedState = it
            }
            )

            Text(text = "Clicking this Row will toggle checkbox")
        }
    }
}

Also you can build it like this which will show ripple even when you click CheckBox.

@Composable
 fun CheckBoxWithTextRippleFullRow(
    label: String,
    state: Boolean,
    onStateChange: (Boolean) -> Unit
) {

    // Checkbox with text on right side
    Row(modifier = Modifier
        .fillMaxWidth()
        .height(40.dp)
        .clickable(
            role = Role.Checkbox,
            onClick = {
                onStateChange(!state)
            }
        )
        .padding(8.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {
        Checkbox(
            checked = state,
            onCheckedChange = null
        )
        Spacer(modifier = Modifier.width(8.dp))
        Text(text = label)
    }
}

Result

enter image description here

Click propagation sample

You can click below a Composable even with a Button which is a Surface with Modifier.clickable under the hood.

Let's create our own custom touch only for the parent below Button

private fun Modifier.customTouch(
    pass: PointerEventPass,
    onDown: () -> Unit,
    onUp: () -> Unit
) = this.then(
    Modifier.pointerInput(pass) {
        awaitEachGesture {
            awaitFirstDown(pass = pass)
            onDown()
            waitForUpOrCancellation(pass)
            onUp()
        }
    }
)

And assign it to parent with PointerEventPass.Initial with no consume call and will result as

enter image description here

@Preview
@Composable
private fun LookAheadLayoutTest() {

    val context = LocalContext.current

    Box(
        modifier = Modifier
            .customTouch(
                pass = PointerEventPass.Initial,
                onDown = {
                    Toast
                        .makeText(context, "Parent down", Toast.LENGTH_SHORT)
                        .show()
                }, onUp = {
                    Toast
                        .makeText(context, "Parent up", Toast.LENGTH_SHORT)
                        .show()
                }
            )
            .padding(bottom = 50.dp)
            .fillMaxSize()
            .padding(20.dp),
        contentAlignment = Alignment.BottomCenter
    ) {
        Button(
            onClick = {
            Toast.makeText(context, "Button is clicked", Toast.LENGTH_SHORT).show()
        }) {
            Text("Click Button")
        }
    }
}
Thracian
  • 43,021
  • 16
  • 133
  • 222
  • Thank you for the detailed answer Thracian. Plenty for me to dive into from this, have a good day. – HoinzeyBear May 11 '23 at 15:40
  • 1
    You are welcome. Touch system in Compose is pretty straightforward, when you read some documentation and create some samples it's not that difficult to overcome any custom gesture needs or understand why you can't drag or detect transform gestures do not work with scroll or some callbacks are not invoked. If you need to change direction change pass, if you need some child or parent not to react some touch event consume, that's it. – Thracian May 11 '23 at 15:54
  • 1
    I just noticed I have your Compose course project starred and have actually used it recently so quite funny you are the one answering ! Small world – HoinzeyBear May 11 '23 at 16:00
  • 1
    Thanks for your support, you can check out gesture tutorials that shows every step about gestures in some degree of isolation to show effect of propagation, consume, indications and so on. After grasping the basics with consume and propgation and `awaitFirstDown` and `awaintPointerEvent` everything about gesture will be like piece of cake and when you inspect source code of any gesture you will see why it doesn't work in combination with another – Thracian May 11 '23 at 16:03
0

Children are simply drawn on top of their parent container, that's the only way it makes sense. If you set background to that Checkbox, you'd expect it to cover background of the Row, no? It's same with clicking. You can't click on something that's below other clickable element. It's the same for Compose and View system.

Jan Bína
  • 3,447
  • 14
  • 16
  • This answer contains incorrect information. The reason you can't propagate gesture or click from child to parent `Modifier.clickable` is it propagates from child to parent and it consumes PointerEventChange. You can click or pass any gesture from one to another in direction you prefer by changing PointeEventPass and not consuming via Modifier.pointerInput and implementing your own click gesture. `Modifier.clickable` is also Modifier.pointerInput under the hood. – Thracian May 11 '23 at 12:32