Jetpack Compose - Modifier.layout (Part 2)
In part 1, we learn the basic of Modifier.layout
using a sample from the official document. In part 2, we explore the AlignmentLine
concept to answer the following questions: what is it and how to use it.
The AlignmentLine
In the previous part, we had the following custom layout:
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)
}
}
Let's focus on this part:
// Check the composable has a first baseline and extract it
check(placeable[FirstBaseline] != AlignmentLine.Unspecified)
val firstBaseline = placeable[FirstBaseline]
There is barely an explanation for it. It turns out that, here, we extract the value exported by the placeable, and use it to calculate and lay our composable out. This is mentioned in the document:
The Compose layout model lets you use AlignmentLine to create custom alignment lines that can be used by parent layouts to align and position their children. For example, Row can use its children's custom alignment lines to align them.
When a layout provides a value for a particular AlignmentLine, the layout's parents can read this value after measuring, using the Placeable.get operator on the corresponding Placeable instance. Based on the position of the AlignmentLine, the parents can then decide the positioning of the children.
To better understand this, let consider this question: given a parent layout X with 3 children: A, B, C. How can we align B and C relatively to some value calculated by laying out A?
Based on the document, we can do this using AlignmentLine
. But how?
To demonstrate the solution, consider this example:
- A is a block of text, which has the first baseline value (distance from the top of the text to the first baseline in pixel) and the last baseline value (distance from the top of the text block to the baseline of that last line in pixel).
- B is a text block whose first baseline is aligned horizontally with the first baseline of A,
- and C is another text block whose first baseline is aligned horizontally with the last baseline of A.
- X is the custom layout we build to fulfill our requirement:
The full code of this example is as below (with some adjustment to draw lines for our verification):
@Composable
fun TextWithBaselineIndicator(
modifier: Modifier = Modifier,
topLine: @Composable () -> Unit,
bottomLine: @Composable () -> Unit,
content: @Composable () -> Unit,
) {
Layout(
content = {
content()
Text(text = "first baseline")
Text(text = "last baseline")
topLine()
bottomLine()
},
modifier = Modifier.then(modifier)
) { measurables: List<Measurable>, constraints: Constraints ->
check(measurables.size == 5)
val placeables = measurables.map {
it.measure(constraints = constraints)
}
val mainBlock = placeables[0]
val firstBaseline = mainBlock[FirstBaseline]
val firstBaselineText = placeables[1]
val firstBaselineTextY =
firstBaseline - firstBaselineText[FirstBaseline]
val lastBaseline = mainBlock[LastBaseline]
val lastBaselineText = placeables[2]
val lastBaselineTextY =
lastBaseline - lastBaselineText[FirstBaseline]
layout(
width = constraints.maxWidth,
height = constraints.maxHeight
) {
// The texts that are aligned with the baselines
firstBaselineText.placeRelative(0, firstBaselineTextY)
lastBaselineText.placeRelative(0, lastBaselineTextY)
// Red lines
placeables[3].placeRelative(0, firstBaseline)
placeables[4].placeRelative(0, lastBaseline)
// The main text block
mainBlock.placeRelative(
max(firstBaselineText.width, lastBaselineText.width) + 16,
0
)
}
}
}
// Usage
TextWithBaselineIndicator(
modifier = Modifier,
topLine = {
Canvas(modifier = Modifier.fillMaxWidth()) {
val canvasWidth = size.width
val canvasHeight = size.height
drawLine(
start = Offset(x = canvasWidth, y = 0f),
end = Offset(x = 0f, y = canvasHeight),
color = Color.Red
)
}
},
bottomLine = {
Canvas(modifier = Modifier.fillMaxWidth()) {
val canvasWidth = size.width
val canvasHeight = size.height
drawLine(
start = Offset(x = canvasWidth, y = 0f),
end = Offset(x = 0f, y = canvasHeight),
color = Color.Red
)
}
}
) {
Text(
text = "This is a really long text.\nThis is a really long text.\nThis is a really long text.\nThis is a really long text.",
fontSize = 24.sp,
)
}
As you can see, we extract the information about the first baseline and last baseline of the main text by calling:
val firstBaseline = mainBlock[FirstBaseline]
val lastBaseline = mainBlock[LastBaseline]
So what does that code do?
We can see that, a Placeable
is also a Measured
, so placeable[FirstBaseline]
is equal to placeable.get(FirstBaseline)
, which "returns the position of an alignment line, or AlignmentLine.Unspecified
if the line is not provided." (operator fun get(alignmentLine: AlignmentLine): Int). But where does the FirstBaseline
come from and why placeable[FirstBaseline]
returns the value we need? Reading the source code, we learn that:
FirstBaseline
is a built-inAlignmentLine
.And a Text composable exposes that value via the layout call.
Now, go back to our original question: what is AlignmentLine
and how we can use it. From the example above, we can guess that: an AlignmentLine
is a value exposed by a composable so that the parent composable can use it to align the children composable accordingly. A Text
composable exposes 2 alignment lines: FirstBaseline
and LastBaseline
by which we can build custom layouts like above. A custom AlignmentLine
can be exposed by passing them to the layout(width, height, alignmentLines)
callback in the custom layout implementation.
In the next part, we will learn about the AlignmentLine
further, by going through the official document.