Thread
#compose
    m

    Marko Novakovic

    1 year ago
    I want to create layout like this. Cloud above everything else and user should be able to move the cloud around. Cloud is menu that opens on
    +
    . Problem I have with my current implementation is that top layer with cloud is consuming all inputs so I can’t interact with rest of the app. Do you have any ideas how to do this. Code in 🧵
    import androidx.compose.foundation.Image
    import androidx.compose.foundation.gestures.detectDragGestures
    import androidx.compose.foundation.layout.Box
    import androidx.compose.foundation.layout.fillMaxSize
    import androidx.compose.foundation.layout.offset
    import androidx.compose.material.Icon
    import androidx.compose.material.icons.Icons
    import androidx.compose.material.icons.filled.Add
    import androidx.compose.runtime.*
    import androidx.compose.ui.Alignment
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.geometry.Offset
    import androidx.compose.ui.graphics.Color
    import androidx.compose.ui.input.pointer.consumeAllChanges
    import androidx.compose.ui.input.pointer.pointerInput
    import androidx.compose.ui.res.painterResource
    import androidx.compose.ui.unit.IntOffset
    import kotlin.math.roundToInt
    
    @Composable
    fun CircleMenu() {
        var offset by remember { mutableStateOf(Offset(0f, 0f)) }
        Box(
            modifier = Modifier
                .fillMaxSize()
                .pointerInput(Unit) {
                    detectDragGestures { change, dragAmount ->
                        change.consumeAllChanges()
                        offset += dragAmount
                    }
                },
        ) {
            Cloud(offset = offset)
        }
    }
    
    @Composable
    private fun Cloud(offset: Offset) {
        Box(
            modifier = Modifier.offset { IntOffset(offset.x.roundToInt(), offset.y.roundToInt()) },
            contentAlignment = Alignment.Center
        ) {
            Image(
                painter = painterResource(id = R.drawable.cloud),
                contentDescription = null,
            )
            Icon(
                imageVector = Icons.Filled.Add,
                tint = Color.White,
                contentDescription = null,
            )
        }
    }
    using it like this
    @Composable
    fun Home() {
        Box {
            Scaffold(
                modifier = Modifier.fillMaxSize(),
                topBar = { HomeAppBar() },
            ) {
                ...
            }
            CircleMenu()
        }
    }
    Chris Sinco [G]

    Chris Sinco [G]

    1 year ago
    I think the
    Modifier.fillMaxSize()
    in your
    CircleMenu
    might be the issue? I think if you change that to
    Modifier.wrapContentSize()
    the gestures should only be captured on it
    c

    Casey Brooks

    1 year ago
    Input events fall-through to their parent composable if they don’t get handled, but do not get sent to siblings. From your snippet, it looks like the CircleMenu is in a different hierarchy than the rest of your app. I don’t think there’s any way around this other than restructuring your UI such that the menu is a child component of the rest of your app, rather than sitting as a sibling of it. So somehow moving
    CircleMenu
    inside your
    Scaffold
    such that the other pieces of UI are somewhere in its parents’ layouts
    m

    Marko Novakovic

    1 year ago
    @Chris Sinco [G] thank you, I tried so many things so I don’t know how that will work 😄 will try this now
    @Casey Brooks I will try that too, thanks
    c

    Casey Brooks

    1 year ago
    I’ve done something very similar to this by setting up a bunch of “layers” inside a
    Box
    , each handling a different portion of the UI. Each of these layers needs to be nested within the others in order to have the input event propagate correctly. Give me a min to gather the relevant bits from my app into a something like a minimal example
    m

    Marko Novakovic

    1 year ago
    @Chris Sinco [G]
    Modifier.wrapContentSize()
    is not wanted behaviour. It detects drag gesture just inside a box at a original cloud position
    @Casey Brooks setting
    Box
    inside
    Scaffold
    with other content has the original issue
    I also want to move cloud across entire screen
    Chris Sinco [G]

    Chris Sinco [G]

    1 year ago
    This is what I got working
    @Preview
    @Composable
    fun PlaygroundPreview2() {
        var cloudXOffset by remember { mutableStateOf(0f) }
        var cloudYOffset by remember { mutableStateOf(0f) }
    
        Box(Modifier.fillMaxSize()) {
            Scaffold(
                modifier = Modifier.fillMaxSize(),
                topBar = { TopAppBar(title = { Text("title") }) }
            ) {
                Column(Modifier.fillMaxSize()) {
                    Spacer(Modifier.fillMaxHeight(0.25f))
                    CustomButton()
                }
            }
            Cloud(
                modifier = Modifier
                    .offset { IntOffset(cloudXOffset.roundToInt(), cloudYOffset.roundToInt()) }
                    .wrapContentSize()
                    .background(Color.Magenta)
                    .pointerInput(Unit) {
                        detectDragGestures { change, dragAmount ->
                            change.consumeAllChanges()
                            cloudXOffset += dragAmount.x
                            cloudYOffset += dragAmount.y
                        }
                    }
            )
        }
    }
    
    @Composable
    private fun Cloud(
        modifier: Modifier = Modifier,
    ) {
        Box(
            modifier = modifier
    
        ) {
            Icon(
                imageVector = Icons.Default.Face,
                contentDescription = null,
                tint = contentColorFor(Color.Magenta)
            )
        }
    }
    It may be the order of your Modifiers that is causing the issue. One tip is to draw a Magenta background with Modifiers to see how big your components actually are relative to tap/drag gestures
    c

    Casey Brooks

    1 year ago
    Yeah, @Chris Sinco [G]’s example is very similar to what I had been doing as well.
    @Composable
    override fun render() {
        Box(
            modifier = Modifier.fillMaxSize()
        ) {
            MainContent()
    
            // Cloud Menu layer
            CloudContainer()
        }
    }
    
    @Composable
    fun MainContent() {
        var mainContentCounter by remember { mutableStateOf(0) }
        Button(onClick = { mainContentCounter++ }) {
            Text("main counter: $mainContentCounter")
        }
    }
    
    @Composable
    fun CloudContainer() {
        var offset by remember { mutableStateOf(Offset(0f, 0f)) }
        Box(
            modifier = Modifier.fillMaxSize()
        ) {
            Cloud(offset = offset) { offset = it }
        }
    }
    
    @Composable
    fun Cloud(offset: Offset, updateOffset: (Offset)->Unit) {
        var cloudCounter by remember { mutableStateOf(0) }
        var accumulatedOffset by remember { mutableStateOf(Offset.Zero) }
    
        Box(
            modifier = Modifier
                .offset { IntOffset(offset.x.roundToInt(), offset.y.roundToInt()) }
                .pointerInput(Unit) {
                    detectDragGestures(
                        onDrag = { change, dragAmount ->
                            change.consumeAllChanges()
    
                            accumulatedOffset += dragAmount
    
                            updateOffset(offset + accumulatedOffset)
                        }
                    )
                }
                .background(MaterialTheme.colors.primary)
                .clickable { cloudCounter++ }
                .size(
                    width = 50.dp,
                    height = 50.dp
                ),
            contentAlignment = Alignment.Center
        ) {
            Text("Cloud counter: $cloudCounter")
        }
    }
    Looking at @Marko Novakovic’s code again, I think it might be an issue that the
    pointerInput
    modifier is on the cloud’s Box layout, rather than on the Cloud itself
    m

    Marko Novakovic

    1 year ago
    I will try these suggestions, huge thanks
    Chris Sinco [G]

    Chris Sinco [G]

    1 year ago
    Yes, to @Casey Brooks point, the thing to keep in mind is you want the
    pointerInput
    and
    offset
    modifiers to be chained together because are moving the dragTarget and the element together. So as you drag, you update the offset, which keeps the dragTarget visually under your finger.
    m

    Marko Novakovic

    1 year ago
    both solutions worked, problem was that
    pointerInput
    was on
    Box
    instead of cloud itself.
    offset
    and
    pointerInput
    should indeed be chained together