Compose レイアウトの固有の測定値

Compose のルールのひとつとして、子を 1 回しか測定できないことが挙げられます。子を 2 回測定した場合、ランタイム例外がスローされます。ただし、測定する前に子の情報が必要になる場合もあります。

Intrinsic を使用すると、実際に測定する前に子をクエリできます。

コンポーザブルに対して、次のように intrinsicWidth または intrinsicHeight を要求できます。

  • (min|max)IntrinsicWidth: ある幅の場合に、コンテンツを正しく描画できる最小/最大の幅はいくつでしょうか。
  • (min|max)IntrinsicHeight: ある高さの場合に、コンテンツを正しく描画できる最小/最大の高さはいくつでしょうか。

たとえば、TextminIntrinsicHeight を無限大の height で要求した場合、テキストが 1 行で描画されているかのように、Textheight が返されます。

Intrinsic の使い方

次のように、2 つのテキストを分割線で区切って画面上に表示するコンポーザブルを作成するとします。

2 つのテキスト要素を横に並べ、その間に縦方向の分割線を配置します

これを実現するには、Row 内に 2 つの Text を含めてテキストが可能な限り拡大するようにして、中央に Divider を含めます。Divider の高さは最も高い Text と同じにして、幅は狭くします(width = 1.dp)。

@Composable
fun TwoTexts(modifier: Modifier = Modifier, text1: String, text2: String) {
    Row(modifier = modifier) {
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(start = 4.dp)
                .wrapContentWidth(Alignment.Start),
            text = text1
        )
        HorizontalDivider(
            color = Color.Black,
            modifier = Modifier.fillMaxHeight().width(1.dp)
        )
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(end = 4.dp)
                .wrapContentWidth(Alignment.End),

            text = text2
        )
    }
}

これをプレビューすると、想定とは異なり、Divider が画面全体に拡大されます。

2 つのテキスト要素が横に並び、分割線で区切られていますが、分割線がテキストの下まで伸びています

これは、Row がそれぞれの子を個別に測定するため、Text の高さを使用して Divider を制約できないためです。Divider が指定の高さで空きスペースを埋めるように設定する必要があります。そのためには、height(IntrinsicSize.Min) 修飾子を使用します。

height(IntrinsicSize.Min) は、子の高さが Intrinsic の最小の高さと同じになるように強制します。この修飾子は再帰的であるため、Row とその子の minIntrinsicHeight をクエリします。

これを次のようにコードに適用すると、想定どおりに動作します。

@Composable
fun TwoTexts(modifier: Modifier = Modifier, text1: String, text2: String) {
    Row(modifier = modifier.height(IntrinsicSize.Min)) {
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(start = 4.dp)
                .wrapContentWidth(Alignment.Start),
            text = text1
        )
        HorizontalDivider(
            color = Color.Black,
            modifier = Modifier.fillMaxHeight().width(1.dp)
        )
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(end = 4.dp)
                .wrapContentWidth(Alignment.End),

            text = text2
        )
    }
}

// @Preview
@Composable
fun TwoTextsPreview() {
    MaterialTheme {
        Surface {
            TwoTexts(text1 = "Hi", text2 = "there")
        }
    }
}

プレビュー:

2 つのテキスト要素を横に並べ、その間に縦方向の分割線を配置します

Row コンポーザブルの minIntrinsicHeight が、子の最大 minIntrinsicHeight になります。Divider 要素の minIntrinsicHeight は、制約が設定されていない場合はスペースを占有しないため、0 になります。TextminIntrinsicHeight は、特定の width が指定されたテキストの minIntrinsicHeight になります。したがって、Row 要素の height 制約が、Text の最大 minIntrinsicHeight になります。Divider は自身の height を、Row で指定された height 制約まで拡大します。

カスタム レイアウトでの Intrinsic

カスタムの Layout 修飾子または layout 修飾子を作成すると、近似値に基づいて固有の測定値が自動的に計算されます。このため、すべてのレイアウトで計算が不正確になる場合があります。これらの API には、こうしたデフォルト値をオーバーライドするオプションが用意されています。

カスタム Layout の組み込み測定値を指定するには、作成時に MeasurePolicy インターフェースの minIntrinsicWidthminIntrinsicHeightmaxIntrinsicWidthmaxIntrinsicHeight をオーバーライドします。

@Composable
fun MyCustomComposable(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        content = content,
        modifier = modifier,
        measurePolicy = object : MeasurePolicy {
            override fun MeasureScope.measure(
                measurables: List<Measurable>,
                constraints: Constraints
            ): MeasureResult {
                // Measure and layout here
                // ...
            }

            override fun IntrinsicMeasureScope.minIntrinsicWidth(
                measurables: List<IntrinsicMeasurable>,
                height: Int
            ): Int {
                // Logic here
                // ...
            }

            // Other intrinsics related methods have a default value,
            // you can override only the methods that you need.
        }
    )
}

カスタムの layout 修飾子を作成する場合は、LayoutModifier インターフェースの関連するメソッドをオーバーライドします。

fun Modifier.myCustomModifier(/* ... */) = this then object : LayoutModifier {

    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        // Measure and layout here
        // ...
    }

    override fun IntrinsicMeasureScope.minIntrinsicWidth(
        measurable: IntrinsicMeasurable,
        height: Int
    ): Int {
        // Logic here
        // ...
    }

    // Other intrinsics related methods have a default value,
    // you can override only the methods that you need.
}