https://kotlinlang.org logo
#compose
Title
# compose
m

mattinger

02/01/2023, 7:50 PM
Does anyone know if there’s a better way to anchor some buttons at the bottom of a layout than using a column and then applying a weight? The problem i’m having is that if i do this:
Copy code
.weight(1.0f, true)
Then the outer column is occupying the entire space that its parent is allowing it to fill. This means i have to pass a variable to the “weight” function based on whether i want the element to occupy additional space or not. I’m using this inside of a modal bottom sheet and it’s making my bottom sheet occupy the entire height of the screen, when it doesn’t really need it. Code in 🧵
Copy code
@Composable
fun StandardTemplateLayout(
    modifier: Modifier = Modifier,
    fullscreen: Boolean = true,
    scrollState: ScrollState = rememberScrollState(),
    media: @Composable ColumnScope.() -> Unit = { },
    buttons: @Composable (ColumnScope.() -> Unit)? = null,
    content: @Composable ColumnScope.() -> Unit,
    ) {
    Column(
        modifier = modifier
    ) {
        Column(
            modifier = Modifier
                .testTag(TestTags.ScrollContainer)
                .fillMaxWidth()
                .weight(1.0f, fullscreen)
                .verticalScroll(scrollState),
            horizontalAlignment = Alignment.Start,
            verticalArrangement = spacedBy(24.dp)
        ) {
              ...
        }

        buttons?.let {
            Column(
                modifier = Modifier.padding(
                    start = HorizontalPadding,
                    end = HorizontalPadding,
                    top = ButtonTopPadding,
                    bottom = ButtonBottomPadding,
                )
            ) {
                it()
            }
        }
    }
}
I’m not thrilled about the fullscreen parameter having to be there. I’d much prefer it pay attention to the modifier. If it’s intrinsically sized, then it wouldn’t fill and if it is, then make the scrollable area take up whatever area the buttons don’t.
Am i better off with A constraint layout in this case?
d

Dave Scheck

02/01/2023, 8:12 PM
What if you made your outer column have
verticalArrangement = Arrangement.Bottom
and reverse the order of your inner elements. Have the buttons draw first and they should be at the bottom. Then your column should draw after it and take up the remaining space right?
Actually, you don't need to reverse the order. I tried this:
Copy code
val fullScreen = remember { mutableStateOf(false)}

    Column(
        modifier = Modifier
            .fillMaxWidth()
            .fillMaxHeight()
            .background(Color.Red),
        verticalArrangement = Arrangement.Bottom
    ) {
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .weight(1.0f, fullScreen.value)
                .background(Color.Blue)
            ,
        ) {
            Text("Test")
        }
        Row(modifier = Modifier.fillMaxWidth().background(Color.Yellow)) {
            Button(onClick = {
                fullScreen.value = !fullScreen.value
            }) {Text("Button")}
        }
    }
I think this is the toggled behavior that you're looking for based on your flag
a

Alex Vanyo

02/01/2023, 8:32 PM
The reversed arrangement idea is so clever, I love it
m

mattinger

02/01/2023, 8:35 PM
I guess what i’m looking for is kind of two fold: 1. I want it to be able to intrinsically size 2. If the parent gives it more room, i want the scroll area to expand to fill the space
a

Alex Vanyo

02/01/2023, 8:39 PM
Do you also want 3. If the intrinsic height (of the scrollable content) is less than the amount of space available given by the parent, the buttons should be anchored to the bottom, and the scroll area should be anchored to the top?
m

mattinger

02/01/2023, 10:11 PM
yes.
I’m trying with constraint layout but seeing odd behavior there .
a

Alex Vanyo

02/01/2023, 10:15 PM
Here’s an approach based on the cool reversed arrangement idea:
Copy code
Column(Modifier.height(70.dp), verticalArrangement = BottomReversed) {
    // This will take up space first and placed at the bottom, so it won't be cut off
    Box(
        Modifier
            .background(Color.Red)
            .size(32.dp))

    // This will take up space third, if there is still space left over after
    // the column reaches its intrinsic size, so that the scrollable column is
    // pinned to the top
    Spacer(Modifier.weight(1f))

    // This will take up the remaining space second. If there isn't enough space,
    // the column will become scrollable. The Column won't expand any further
    // than it's intrinsic size
    Column(Modifier.verticalScroll(rememberScrollState())) {
        Box(
            Modifier
                .background(Color.Blue)
                .size(32.dp))
        Box(
            Modifier
                .background(Color.Green)
                .size(32.dp))
    }
}
And then
BottomReversed
is based on the built-in `Arrangement.Reversed`:
Copy code
@Stable
val BottomReversed = object : Arrangement.Vertical {
    override fun Density.arrange(
        totalSize: Int,
        sizes: IntArray,
        outPositions: IntArray
    ) = placeRightOrBottom(totalSize, sizes, outPositions, reverseInput = true)

    override fun toString() = "Arrangement#BottomReversed"
}

internal fun placeRightOrBottom(
    totalSize: Int,
    size: IntArray,
    outPosition: IntArray,
    reverseInput: Boolean
) {
    val consumedSize = size.fold(0) { a, b -> a + b }
    var current = totalSize - consumedSize
    size.forEachIndexed(reverseInput) { index, it ->
        outPosition[index] = current
        current += it
    }
}

private inline fun IntArray.forEachIndexed(reversed: Boolean, action: (Int, Int) -> Unit) {
    if (!reversed) {
        forEachIndexed(action)
    } else {
        for (i in (size - 1) downTo 0) {
            action(i, get(i))
        }
    }
}
11 Views