Jetpack Compose - Modifier.layout (Part 1)
Reference: https://developer.android.com/jetpack/compose/layouts/custom#layout-modifier
The simple Modifier.layout
from sample code
Let's replicate the Modifier.firstBaselineToTop
in the official document, step by step to understand how it works.
The final form:
fun Modifier.firstBaselineToTop(
firstBaselineToTop: Dp
) = layout { measurable, constraints ->
// Measure the composable
val placeable = measurable.measure(constraints)
// Check the composable has a first baseline
check(placeable[FirstBaseline] != AlignmentLine.Unspecified)
val firstBaseline = placeable[FirstBaseline]
// Height of the composable with padding - first baseline
val placeableY = firstBaselineToTop.roundToPx() - firstBaseline
val height = placeable.height + placeableY
layout(placeable.width, height) {
// Where the composable gets placed
placeable.placeRelative(0, placeableY)
}
}
And its result:
Source: official document |
Step 1: the do-nothing attempt
In the first step, let's just add the bare minimum code that does nothing:
fun Modifier.firstBaselineToTop(
firstBaselineToTop: Dp
) = layout { _: Measurable, _: Constraints ->
// Usually, we need to measure the composable and get the `placeable`, but in this step, we will ignore it, and pass 0 as width and height to the layout callback.
layout(width = 0, height = 0) { /* Do nothing */ }
}
And verify the UI by using the Android Studio layout inspector. In this step, it is easy to imagine that there is nothing shown in the UI. Let's skip this step.
Step 2: measure it, lay it out, but don't place it there.
What we do is to measure the composable, pass the measured width and height to the layout
callback in the lambda.:
fun Modifier.firstBaselineToTop(
firstBaselineToTop: Dp
) = layout { measurable: Measurable, constraints: Constraints ->
// Measure the composable and get the placeable, using the provided measurable and constraints
val placeable = measurable.measure(constraints)
layout(width = placeable.width, height = placeable.height) { /* Do nothing */ }
}
Now let's check the UI. It is still a blank space in the screen, but if we inspect it, there is something populates the space:
Adjusting the width or height above, we can conclude that: the layout
callback will tell the parent to reserve a rectangle whose width and height are provided value, and its coordinate starts from (0, 0). This rectangle will be the boundary of our composable after laying out. Indeed, if we update out code to the following and check the result:
fun Modifier.firstBaselineToTop(
firstBaselineToTop: Dp
) = layout { measurable: Measurable, constraints: Constraints ->
// Measure the composable and get the placeable, using the provided measurable and constraints
val placeable = measurable.measure(constraints)
layout(width = placeable.width, height = placeable.height) {
placeable.placeRelative(x = 20, y = 20)
}
}
The layout inspector shows that: our text is placed 20 pixel from the left, and 20 pixel from the top, and it is cut at the edge of the dimension we provided above.
To be able to correctly place the composable, we need to adjust the space required by it. It is time to take the firstBaselineToTop
into account.
Step 3: measure it, reserve space for it and place it
fun Modifier.firstBaselineToTop(
firstBaselineToTop: Dp
) = layout { measurable, constraints ->
// Measure the composable
val placeable = measurable.measure(constraints)
// Check the composable has a first baseline and extract it
check(placeable[FirstBaseline] != AlignmentLine.Unspecified)
val firstBaseline = placeable[FirstBaseline]
// Calculate: the total height of the composable after applying the `firstBaselineToTop` value is equal to its original height plus the diff between the
`firstBaselineToTop` value and the first baseline, which is:
val totalHeight = placeable.height + (firstBaselineToTop.roundToPx() - firstBaseline)
// Therefore, the starting point from top by which we place the composable is:
val placeableY = totalHeight - placeable.height
layout(placeable.width, totalHeight) {
// Now place it
placeable.placeRelative(0, placeableY)
}
}
And tada:
Exploring the layout
callback
It seems that the layout
callback is one of the core actor here. Let's learn more about it:
interface MeasureScope : IntrinsicMeasureScope {
/**
* @param alignmentLines the alignment lines defined by the layout
* @param placementBlock block defining the children positioning of the current layout
*/
fun layout(
width: Int,
height: Int,
alignmentLines: Map<AlignmentLine, Int> = emptyMap(),
placementBlock: Placeable.PlacementScope.() -> Unit
) = object : MeasureResult {
override val width = width
override val height = height
override val alignmentLines = alignmentLines
override fun placeChildren() {
Placeable.PlacementScope.executeWithRtlMirroringValues(
width,
layoutDirection,
placementBlock
)
}
}
}
So beside the width, the height and the placementBlock
lambda we used to place the placeable, there is alignmentLines
values which is default to an empty map. Let's learn about this value in part 2.