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:

Screen Shot 2021-08-08 at 22.36.20.png

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:

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.