Jetpack Compose でのアライメント ライン

Compose レイアウト モデルでは、AlignmentLine を使用してカスタム アライメント ラインを作成できます。これにより、親レイアウトで子の整列と配置を行うことができます。たとえば、Row では、その子のカスタム アライメント ラインを使用して子を整列できます。

レイアウトで特定の AlignmentLine に値を指定する場合、レイアウトの親は対応する Placeable インスタンスに Placeable.get 演算子を使用し、この値を測定した後に読み取ることができます。親は、AlignmentLine の位置に基づいて子の配置を決定できます。

Compose の一部のコンポーザブルには、すでにアライメント ラインが搭載されています。たとえば、BasicText コンポーザブルは、FirstBaselineLastBaseline のアライメント ラインを公開します。

下の例では、firstBaselineToTop というカスタム LayoutModifierFirstBaseline を読み取り、その最初のベースラインから Text にパディングを追加しています

図 1. 特定の要素に通常のパディングを追加することと、テキスト要素のベースラインにパディングを適用することの違い。

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)
    }
}

@Preview
@Composable
fun TextWithPaddingToBaselinePreview() {
    MaterialTheme {
        Text("Hi there!", Modifier.firstBaselineToTop(32.dp))
    }
}

この例では、FirstBaseline を読み取るため、測定フェーズで placeable [FirstBaseline] を使用します。

カスタムのアライメント ラインを作成する

カスタム Layout コンポーザブルまたはカスタム LayoutModifier を作成するときに、カスタムのアライメント ラインを指定できます。これにより、他の親のコンポーザブルがそれらを使用して子を適切に整列させ、配置することが可能になります。

次の例は、他のコンポーザブルがグラフデータの最大値と最小値に整列できるように 2 つのアライメント ライン、MaxChartValueMinChartValue を公開するカスタム BarChart コンポーザブルを示しています。2 つのテキスト要素、Max と Min がカスタム アライメント ラインの中心に整列しています。

図 2. Text がデータの最大値と最小値に整列している BarChart コンポーザブル。

カスタム アライメント ラインは、プロジェクトの最上位の変数として定義されます。

import kotlin.math.max
import kotlin.math.min
import androidx.compose.ui.layout.HorizontalAlignmentLine

/**
 * AlignmentLine defined by the maximum data value in a [BarChart]
 */
val MaxChartValue = HorizontalAlignmentLine(merger = { old, new -> min(old, new) })

/**
 * AlignmentLine defined by the minimum data value in a [BarChart]
 */
val MinChartValue = HorizontalAlignmentLine(merger = { old, new -> max(old, new) })

サンプルを作成するためのカスタムのアライメント ラインは HorizontalAlignmentLine 型です。これは、子を垂直方向に配置するために使用されます。複数のレイアウトでこれらのアライメント ラインの値が提供される場合、マージポリシーがパラメータとして渡されます。Compose レイアウト システムの座標と Canvas 座標は [0, 0] を表しているため、左上隅と、x 軸、y 軸の正の方向はすべて下向きであり、MaxChartValue 値は常に MinChartValue より小さくなります。したがって、マージポリシーは、グラフデータの最大値のベースラインでは min、グラフデータの最小値のベースラインでは max になります。

カスタムの Layout または LayoutModifier を作成する場合は、MeasureScope.layout メソッド(alignmentLines: Map<AlignmentLine, Int> パラメータを受け取ります)でカスタムのアライメント ラインを指定します。

@Composable
fun BarChart(
    dataPoints: List<Int>,
    modifier: Modifier = Modifier
) {
    var maxValueBaseline by remember { mutableStateOf(Float.MAX_VALUE) }
    var minValueBaseline by remember { mutableStateOf(Float.MIN_VALUE) }

    Layout(
        modifier = modifier,
        content = {
            // ... Logic to draw the chart in a Canvas ...
            // maxValueBaseline and minValueBaseline are updated here
        }
    ) { measurables, constraints ->
        val placeable = measurables[0].measure(constraints)
        layout(
            width = constraints.maxWidth,
            height = constraints.maxHeight,
            // Custom AlignmentLines are set here. These are propagated
            // to direct and indirect parent composables.
            alignmentLines = mapOf(
                MinChartValue to minValueBaseline.roundToInt(),
                MaxChartValue to maxValueBaseline.roundToInt()
            )
        ) {
            placeable.placeRelative(0, 0)
        }
    }
}

このコンポーザブルの直接的および間接的な親は、アライメント ラインを使用できます。次のコンポーザブルは、カスタム レイアウトを作成し、2 つの Text スロットとデータポイントをパラメータとして受け取り、2 つのテキストをグラフデータの最大値と最小値に整列させます。このコンポーザブルのプレビューを図 2 に示します。

@Composable
fun BarChartMinMax(
    dataPoints: List<Int>,
    maxText: @Composable () -> Unit,
    minText: @Composable () -> Unit,
    modifier: Modifier = Modifier
) {
    Layout(
        content = {
            maxText()
            minText()
            // Set a fixed size to make the example easier to follow
            BarChart(dataPoints, Modifier.size(200.dp))
        },
        modifier = modifier
    ) { measurables, constraints ->
        check(measurables.size == 3)
        val placeables = measurables.map {
            it.measure(constraints.copy(minWidth = 0, minHeight = 0))
        }

        val maxTextPlaceable = placeables[0]
        val minTextPlaceable = placeables[1]
        val barChartPlaceable = placeables[2]

        // Obtain the alignment lines from BarChart to position the Text
        val minValueBaseline = barChartPlaceable[MinChartValue]
        val maxValueBaseline = barChartPlaceable[MaxChartValue]
        layout(constraints.maxWidth, constraints.maxHeight) {
            maxTextPlaceable.placeRelative(
                x = 0,
                y = maxValueBaseline - (maxTextPlaceable.height / 2)
            )
            minTextPlaceable.placeRelative(
                x = 0,
                y = minValueBaseline - (minTextPlaceable.height / 2)
            )
            barChartPlaceable.placeRelative(
                x = max(maxTextPlaceable.width, minTextPlaceable.width) + 20,
                y = 0
            )
        }
    }
}

@Preview
@Composable
fun ChartDataPreview() {
    MaterialTheme {
        BarChartMinMax(
            dataPoints = listOf(4, 24, 15),
            maxText = { Text("Max") },
            minText = { Text("Min") },
            modifier = Modifier.padding(24.dp)
        )
    }
}