Blog Infos
Author
Published
Topics
, , ,
Published

In the context of a recent technical presentation, I was experimenting different layouts in Jetpack Compose, from which SubComposeLayout was one of the most challenging.

Theory

SubComposeLayout is a low level API for building layout in Jetpack Compose, by overriding the standard steps of composition-layout-drawing with a subcomposition step for child nodes of a layout (that is obviously a SubComposeLayout).
This way, we engineers, can decide at runtime whether we want to display (place) a subsequent child node on the UI based on the measurement (size and position) of a preceeding child node.

More about the theory can be found in Advanced layout concepts — MAD Skills and SubcomposeLayout — breaking the Compose phases rule.

Use case

Some of the good uses cases are LazyLayout together with it’s derivatives and BoxWithConstraints, however in the Compose Layouts and Modifiers: Live Q&A — MAD Skills discussion the experts say that there are not that many cases for the custom SubComposeLayout, however I wanted to enforce one for my presentation so, here MultiItemPager comes 🙂

(do not miss the nice and actress-like look and behaviour of Simona from 41:25 in the video 🙂

Back to the use case, let’s consider that the business says the following:
– display a list of items on the UI that have different sizes
– place as many item on the UI as many can be fit vertically
– moving to the next page of items should be done by clicking on a button
– neither vertical nor horizontal scrolling cannot be used

After implementing, it looks this way:

Show me the code

The main point in the code is that it is only placing subsequent child items of SubComposeLayout, until there is space left by preceeding children (line 124).

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

No results found.

Jobs

package david.composesimulation.ui.subcompose
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
private data class Element(
val text: String,
val widthInPixel: Int,
val heightInPixel: Int,
val color: Color,
)
private val elements =
listOf(
Element(text = "1", widthInPixel = 800, heightInPixel = 1200, color = Color.Green),
Element(text = "2", widthInPixel = 1000, heightInPixel = 800, color = Color.Red),
Element(text = "3", widthInPixel = 900, heightInPixel = 1200, color = Color.Gray),
Element(text = "4", widthInPixel = 800, heightInPixel = 1300, color = Color.Blue),
Element(text = "5", widthInPixel = 1100, heightInPixel = 900, color = Color.Cyan),
Element(text = "6", widthInPixel = 1100, heightInPixel = 1200, color = Color.Magenta),
Element(text = "7", widthInPixel = 300, heightInPixel = 300, color = Color.Yellow),
Element(text = "8", widthInPixel = 6000, heightInPixel = 200, color = Color.DarkGray),
Element(text = "9", widthInPixel = 500, heightInPixel = 300, color = Color.LightGray),
)
@Composable
fun MultiItemPagerUseCase() {
Column {
var lastDisplayedElementIndex by rememberSaveable { mutableStateOf<Int?>(null) }
var elementIndex by rememberSaveable { mutableIntStateOf(0) }
Button(
enabled = (lastDisplayedElementIndex ?: 0) < elements.lastIndex,
onClick = {
lastDisplayedElementIndex?.let {
elementIndex = it + 1
}
},
modifier = Modifier
.padding(all = 16.dp)
.fillMaxWidth()
) {
Text(text = "Next page")
}
MultiItemPager(
firsElementIndexToDisplay = elementIndex,
elements = elements,
elementContent = { element ->
val density = LocalDensity.current
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.padding(top = 16.dp)
.padding(horizontal = 16.dp)
.size(
width = density.run { element.widthInPixel.toDp() },
height = density.run { element.heightInPixel.toDp() },
)
.background(color = element.color)
) {
Text(
text = element.text,
fontSize = 29.sp,
)
}
},
onPageIsFull = { page ->
lastDisplayedElementIndex = page
}
)
}
}
@Composable
private fun MultiItemPager(
firsElementIndexToDisplay: Int,
elements: List<Element>,
elementContent: @Composable (element: Element) -> Unit,
onPageIsFull: (lastDisplayedElementIndex: Int) -> Unit,
modifier: Modifier = Modifier,
) {
SubcomposeLayout(
modifier = modifier
) { constraints ->
val maxHeight = constraints.maxHeight
val placeables = mutableListOf<Placeable>()
var isAvailableSpaceLeft = true
var elementIndex = firsElementIndexToDisplay
println("SegmentedLayout maxHeight $maxHeight elementIndex $elementIndex")
while (isAvailableSpaceLeft) {
val measurables = subcompose(slotId = elementIndex, content = { elementContent(elements[elementIndex]) })
val currentPlaceables = measurables.map { measurable -> measurable.measure(constraints) }
val currentPlaceablesAggregatedHeight =
currentPlaceables.fold(0) { accumulator, element -> accumulator + element.height }
val previousPlaceablesAggregatedHeight =
placeables.fold(0) { accumulator, element -> accumulator + element.height }
if (maxHeight - previousPlaceablesAggregatedHeight < currentPlaceablesAggregatedHeight) {
isAvailableSpaceLeft = false
onPageIsFull(elementIndex - 1)
} else {
placeables.addAll(currentPlaceables)
elementIndex++
if (elementIndex == elements.size) {
isAvailableSpaceLeft = false
onPageIsFull(elementIndex - 1)
}
}
}
val layoutWidth = constraints.maxWidth
val layoutHeight = placeables.sumOf { placeable -> placeable.height }
layout(
width = layoutWidth,
height = layoutHeight,
) {
var y = 0
placeables.forEach { placeable ->
val horizontalSpace = (layoutWidth - placeable.width) / 2
placeable.placeRelative(x = horizontalSpace, y = y)
y += placeable.height
}
}
}
}
@Preview
@Composable
private fun MultiItemPagerUseCasePreview() {
MultiItemPagerUseCase()
}

The code is part of the ComposeSimulation draft project.

This article is previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
Compose is a relatively young technology for writing declarative UI. Many developers don’t even…
READ MORE
blog
When it comes to the contentDescription-attribute, I’ve noticed a couple of things Android devs…
READ MORE
blog
In this article we’ll go through how to own a legacy code that is…
READ MORE
blog
Compose is part of the Jetpack Library released by Android last spring. Create Android…
READ MORE
Menu