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

Stefan Oltmann

01/31/2023, 9:36 AM
I use Accompanist
FlowRow
and I want the contained Composables to grow after it has been determined how much fit into one row. Since
Placeable.placeAt()
seems only to take a position a
MeasurePolicy
might not be able to change the size of the Composable at all. Is that correct? How can this be solved if not using a
FlowRow
? Is there a better Composable for that?
1
a

Albert Chang

01/31/2023, 9:42 AM
You can write a custom layout that is similar to FlowRow but instead queries the intrinsic sizes first and measures the children with the actual sizes after children count in a row is decided.
s

Stefan Oltmann

01/31/2023, 11:34 AM
Thank you, Albert. 👍 That was the right idea. 🙂 Needs a bit of fine-tuning, but that's the way. Do you have an idea why the scrolling goes too far in my implementation?
Copy code
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasurePolicy
import androidx.compose.ui.unit.Constraints

@Composable
fun FlowRow(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit,
) {
    val measurePolicy = flowRowMeasurePolicy()
    Layout(
        measurePolicy = measurePolicy,
        content = content,
        modifier = modifier
    )
}

private fun flowRowMeasurePolicy(): MeasurePolicy = MeasurePolicy { measurables, constraints ->
    layout(constraints.maxWidth, constraints.maxHeight) {

        val measurablesPerRow = calcMeasurablesPerRow(measurables, constraints)

        var yPos = 0

        for (measurablesThisRow in measurablesPerRow) {

            var xPos = 0
            var maxY = 0

            val cumWidth = measurablesThisRow.sumOf { it.maxIntrinsicWidth(constraints.maxHeight) }

            val extraSpacePerItem = (constraints.maxWidth - cumWidth) / measurablesThisRow.size

            for (measurable in measurablesThisRow) {

                val thisConstrains = constraints.copy(
                    maxWidth = measurable.maxIntrinsicWidth(constraints.maxHeight) + extraSpacePerItem
                )

                val placeable = measurable.measure(thisConstrains)

                placeable.placeRelative(
                    x = xPos,
                    y = yPos
                )

                xPos += placeable.width

                if (maxY < placeable.height)
                    maxY = placeable.height
            }

            yPos += maxY
        }
    }
}

private fun calcMeasurablesPerRow(
    measurables: List<Measurable>,
    constraints: Constraints
): MutableList<List<Measurable>> {

    var cumWidth = 0

    val measurablesPerRow = mutableListOf<List<Measurable>>()

    var measurablesThisRow = mutableListOf<Measurable>()

    for (measurable in measurables) {

        val width = measurable.maxIntrinsicWidth(constraints.maxHeight)

        val willFitInRow = cumWidth + width <= constraints.maxWidth

        if (!willFitInRow) {

            /* Reset */
            measurablesPerRow.add(measurablesThisRow)

            measurablesThisRow = mutableListOf()
            cumWidth = 0
        }

        /* Add */
        measurablesThisRow.add(measurable)

        cumWidth += width
    }

    if (measurablesThisRow.isNotEmpty())
        measurablesPerRow.add(measurablesThisRow)

    return measurablesPerRow
}
Copy code
Box(
                contentAlignment = Alignment.Center,
                modifier = Modifier
                    .size(300.dp)
                    .verticalScroll(rememberScrollState())
            ) {

                FlowRow(
                    modifier = Modifier
                        .border(1.dp, Color.Red)
                ) {

                    for (keyword in listOf(
                        "animal",
                        "canine",
                        "dog",
                        "grass",
                        "pet",
                        "plant",
                        "wildlife",
                        "red fox",
                        "insect",
                        "something longer"
                    )) {
                        Text(
                            keyword,
                            color = Color.White,
                            modifier = Modifier
                                .minimalPadding()
                                .clip(defaultRoundBorderShape)
                                .background(Color.Blue)
                                .defaultPadding()
                                .fillMaxWidth()
                        )
                    }
                }
            }
And thank you again for making Zoomable work with Compose Multiplatform. 🙏 A well deserved place in my credits section of my app that recently had it's Early Access release on the Ashampoo Connect platform. ❤️
s

Stylianos Gakis

01/31/2023, 11:59 AM
Your
calcMeasurablesPerRow
could probably return
List
instead of
MutableList
right? 😄 Also curious about one thing. You alter the constraints to have a
maxWidth
of how much space you want them to take, but that doesn’t mean that they will fill this space right? Shouldn’t their minWidth also be adjusted so that they’re forced to fill the entire width? Maybe it works now for you due to the content itself having .fillMaxSize or something like that? Or is
maxIntrinsicSize
by itself calculating exactly how much width they’ll take if you place them as they are?
m

MR3Y

01/31/2023, 12:45 PM
@Stefan Oltmann the scrolling is probably misbehaving in your implementation due to this suspicious line:
Copy code
layout(constraints.maxWidth, constraints.maxHeight) {}
You should instead layout children based on the size they are actually occupying(from your calculations) and that wouldn't necessarily be the passed in constraints size from the parent layout, I've faced a similar issue and posted the solution that fixed this scrolling bug in the thread https://kotlinlang.slack.com/archives/CJLTWPH7S/p1672528123554529?thread_ts=1672510349.523609&amp;cid=CJLTWPH7S
s

Stefan Oltmann

01/31/2023, 12:47 PM
@Stylianos Gakis Thanks for the hint, yes. The code needs some refactoring, cleaning and comments. I go to this as soon as I resolved my bug. Yes, altering the
maxWidth
in the
Constraint
I pass to
measure()
alone has no effect without
fillMaxWidth()
. This was a thing I needed a moment to understand: You can't tell the Composable/Measurable how big it should be, you just can tell it the lower and upper limits. The element itself must take this space. I don't need to adjust the
minWidth
because
fillMaxWidth()
does everything I need. That's right. And even without that the
maxIntrinsicSize
will calculate how much it needs without filling. First I used
minIntrinsicSize
, but that gives a smaller number for "red fox", because it calculates that "fox" can go in the second line and wraps the text. That's not what I want, so
maxIntrinsicSize
is the
Text
size without wrapping.
@MR3Y Thank you a lot. That's the problem, yes. Below my changed code, dirty hack state, but working.
Copy code
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.unit.Constraints

@Composable
fun FlowRow(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit,
) {

    Layout(content, modifier) {measurables, constraints ->

        val measurablesPerRow = calcMeasurablesPerRow(measurables, constraints)

        val height = measurablesPerRow.get(0).get(0).maxIntrinsicHeight(constraints.maxWidth) * measurablesPerRow.size

        layout(constraints.maxWidth, height) {

            var yPos = 0

            for (measurablesThisRow in measurablesPerRow) {

                var xPos = 0
                var maxY = 0

                val cumWidth = measurablesThisRow.sumOf { it.maxIntrinsicWidth(constraints.maxHeight) }

                val extraSpacePerItem = (constraints.maxWidth - cumWidth) / measurablesThisRow.size

                for (measurable in measurablesThisRow) {

                    val thisConstrains = constraints.copy(
                        maxWidth = measurable.maxIntrinsicWidth(constraints.maxHeight) + extraSpacePerItem
                    )

                    val placeable = measurable.measure(thisConstrains)

                    placeable.placeRelative(
                        x = xPos,
                        y = yPos
                    )

                    xPos += placeable.width

                    if (maxY < placeable.height)
                        maxY = placeable.height
                }

                yPos += maxY
            }
        }
    }
}

private fun calcMeasurablesPerRow(
    measurables: List<Measurable>,
    constraints: Constraints
): MutableList<List<Measurable>> {

    var cumWidth = 0

    val measurablesPerRow = mutableListOf<List<Measurable>>()

    var measurablesThisRow = mutableListOf<Measurable>()

    for (measurable in measurables) {

        val width = measurable.maxIntrinsicWidth(constraints.maxHeight)

        val willFitInRow = cumWidth + width <= constraints.maxWidth

        if (!willFitInRow) {

            /* Reset */
            measurablesPerRow.add(measurablesThisRow)

            measurablesThisRow = mutableListOf()
            cumWidth = 0
        }

        /* Add */
        measurablesThisRow.add(measurable)

        cumWidth += width
    }

    if (measurablesThisRow.isNotEmpty())
        measurablesPerRow.add(measurablesThisRow)

    return measurablesPerRow
}
m

MR3Y

01/31/2023, 1:14 PM
@Stefan Oltmann yeah, perfect. glad it did the trick for you. 👏
s

Stefan Oltmann

01/31/2023, 1:15 PM
Thank you a lot. 🙂
My first custom Layout, I'm a bit proud of myself 😄
m

MR3Y

01/31/2023, 1:17 PM
I hope it won't be the last 😄
s

Stylianos Gakis

01/31/2023, 1:19 PM
Feels good doesn’t it? It’s for some reason more fun than it should be 😅
s

Stefan Oltmann

01/31/2023, 1:21 PM
I think as soon as I gasp the basics it's less work than building a LayoutManager for Swing ;)
I fine-tuned a bit and now I'm pretty happy with the result. 🙂 Here is my final code. Feel free to use it.
Copy code
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntSize
import kotlin.math.roundToInt

@Composable
fun FlowRow(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit,
) {

    Layout(content, modifier) { measurables, constraints ->

        val measurablesPerRow = calcMeasurablesPerRow(measurables, constraints)

        val height = measurablesPerRow.sumOf { it.maxOf { entry -> entry.size.height } }

        layout(constraints.maxWidth, height) {

            var yPos = 0

            for (measurablesThisRow in measurablesPerRow) {

                var xPos = 0
                var maxY = 0

                val rowWidth = measurablesThisRow.sumOf { it.size.width }

                val extraWidthPerItem = (constraints.maxWidth - rowWidth) / measurablesThisRow.size.toDouble()

                val extraWidthPerItemRounded = extraWidthPerItem.roundToInt()

                val lostInRounding = (extraWidthPerItemRounded - extraWidthPerItem) * measurablesThisRow.size

                for ((index, measurableAndSize) in measurablesThisRow.withIndex()) {

                    val lastItem = index == measurablesThisRow.lastIndex

                    val roundingCompensation = if (lastItem) lostInRounding.roundToInt() else 0

                    val itemConstrains = constraints.copy(
                        maxWidth = measurableAndSize.size.width +
                            extraWidthPerItemRounded - roundingCompensation
                    )

                    val placeable = measurableAndSize.measurable.measure(itemConstrains)

                    placeable.placeRelative(xPos, yPos)

                    xPos += placeable.width

                    if (maxY < placeable.height)
                        maxY = placeable.height
                }

                yPos += maxY
            }
        }
    }
}

private data class MeasurableAndSize(
    val measurable: Measurable,
    val size: IntSize
)

private fun calcMeasurablesPerRow(
    measurables: List<Measurable>,
    constraints: Constraints
): MutableList<List<MeasurableAndSize>> {

    var rowWidth = 0

    val measurablesPerRow = mutableListOf<List<MeasurableAndSize>>()

    var measurablesThisRow = mutableListOf<MeasurableAndSize>()

    for (measurable in measurables) {

        val size = measurable.calcMaxIntrinsicSize(constraints)

        val startNewRow = rowWidth + size.width > constraints.maxWidth

        if (startNewRow) {

            /* Add the completed row. */
            measurablesPerRow.add(measurablesThisRow)

            /* Start a fresh row of Measurables  */
            measurablesThisRow = mutableListOf()

            /* Reset the width. */
            rowWidth = 0
        }

        /* Add */
        measurablesThisRow.add(MeasurableAndSize(measurable, size))

        rowWidth += size.width
    }

    if (measurablesThisRow.isNotEmpty())
        measurablesPerRow.add(measurablesThisRow)

    return measurablesPerRow
}

private fun Measurable.calcMaxIntrinsicSize(constraints: Constraints) =
    IntSize(
        maxIntrinsicWidth(constraints.maxHeight),
        maxIntrinsicHeight(constraints.maxWidth)
    )
u

Uchenna Okoye

03/27/2023, 8:37 PM
Thanks Stefan for the insights. Just for context, we are now deprecating Accompanist's Compose. The same behavior can be achieved with weights: https://github.com/google/accompanist/pull/1494/files
123 Views