I want to create layout like this. Cloud above eve...
# compose
m
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 🧵
Copy code
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
Copy code
@Composable
fun Home() {
    Box {
        Scaffold(
            modifier = Modifier.fillMaxSize(),
            topBar = { HomeAppBar() },
        ) {
            ...
        }
        CircleMenu()
    }
}
c
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
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
1
m
@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
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
@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
c
This is what I got working
Copy code
@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
Yeah, @Chris Sinco [G]’s example is very similar to what I had been doing as well.
Copy code
@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
1
m
I will try these suggestions, huge thanks
c
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.
👍 1
m
both solutions worked, problem was that
pointerInput
was on
Box
instead of cloud itself.
offset
and
pointerInput
should indeed be chained together
🎉 3