Compose 레이아웃의 내장 기능 측정

Compose 규칙 중 하나는 하위 요소를 한 번만 측정해야 한다는 것입니다. 하위 요소를 두 번 측정하면 런타임 예외가 발생합니다. 하지만 하위 요소를 측정하기 전에 하위 요소에 관한 정보가 필요한 경우도 있습니다.

내장 기능을 사용하면 하위 요소가 실제로 측정되기 전에 하위 요소를 쿼리할 수 있습니다.

컴포저블에 IntrinsicSize.Min 또는 IntrinsicSize.Max를 요청할 수 있습니다.

  • Modifier.width(IntrinsicSize.Min) - 콘텐츠를 제대로 표시하는 데 필요한 최소 너비는 얼마인가요?
  • Modifier.width(IntrinsicSize.Max) - 콘텐츠를 제대로 표시하는 데 필요한 최대 너비는 얼마인가요?
  • Modifier.height(IntrinsicSize.Min) - 콘텐츠를 제대로 표시하는 데 필요한 최소 높이는 얼마인가요?
  • Modifier.height(IntrinsicSize.Max) - 콘텐츠를 제대로 표시하는 데 필요한 최대 높이는 얼마인가요?

예를 들어 맞춤 레이아웃에서 width 제약 조건이 무한대인 Text의 minIntrinsicHeight를 요청하면 텍스트가 한 줄에 그려진 Text의 height가 반환됩니다.

내장 기능 실제 사례

다음과 같이 화면에 구분선으로 구분된 두 텍스트를 표시하는 컴포저블을 만들 수 있습니다.

나란히 표시된 두 텍스트 요소, 사이에 세로 구분선이 있음

이렇게 하려면 사용 가능한 공간을 채우는 두 개의 Text 컴포저블과 중간에 Divider가 있는 Row를 사용합니다. 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
        )
        VerticalDivider(
            color = Color.Black,
            modifier = Modifier.fillMaxHeight().width(1.dp)
        )
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(end = 4.dp)
                .wrapContentWidth(Alignment.End),

            text = text2
        )
    }
}

Divider가 전체 화면으로 확장되며 이는 원하는 동작이 아닙니다.

나란히 표시된 두 텍스트 요소, 사이에 구분선이 있지만 구분선이 텍스트 하단 아래로 확장됨

Row가 각 하위 요소를 개별적으로 측정하며 Text의 높이를 사용하여 Divider를 제약할 수 없기 때문에 이러한 결과가 발생합니다.

Divider가 가용 공간을 지정된 높이로 채우도록 하려면 height(IntrinsicSize.Min) 수정자를 사용합니다.

height(IntrinsicSize.Min)는 하위 요소의 크기를 고유한 최소 높이로 지정합니다. 이 수정자는 반복적이므로 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
        )
        VerticalDivider(
            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")
        }
    }
}

미리보기는 다음과 같습니다.

나란히 표시된 두 텍스트 요소, 사이에 세로 구분선이 있음

Row의 높이는 다음과 같이 결정됩니다.

  • Row 컴포저블의 minIntrinsicHeight는 하위 요소의 최대 minIntrinsicHeight입니다.
  • Divider 요소의 minIntrinsicHeight는 0입니다. 주어진 제약 조건이 없으면 공간을 차지하지 않기 때문입니다.
  • Text minIntrinsicHeight는 특정 width의 텍스트입니다.
  • 따라서 Row 요소의 height 제약 조건은 Text의 최대 minIntrinsicHeight가 됩니다.
  • 그런 다음 Divider가 height를 Row가 지정한 height 제약 조건으로 확장합니다.

맞춤 레이아웃의 내장 기능

맞춤 Layout 또는 layout 수정자를 만들 때 내장 기능 측정은 근사값에 따라 자동으로 계산됩니다. 따라서 일부 레이아웃의 계산은 정확하지 않을 수 있습니다. 이러한 API는 이와 같은 기본값을 재정의하는 옵션을 제공합니다.

맞춤 Layout의 내장 기능 측정을 지정하려면 만들 때 MeasurePolicy 인터페이스의 minIntrinsicWidth, minIntrinsicHeight, maxIntrinsicWidth, maxIntrinsicHeight를 재정의하세요. 재정의는 measure의 작업과 일치해야 합니다. 예를 들어 하위 요소를 세로로 쌓고 각 하위 요소에 전체 열 너비를 제공하는 맞춤 열에서 지정된 width의 고유한 최소 높이는 해당 width에 대한 하위 요소의 고유한 최소 높이의 합계입니다.

@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 {
                val childConstraints = constraints.copy(minHeight = 0)
                val placeables = measurables.map { measurable ->
                    measurable.measure(childConstraints)
                }
                val width = constraints.constrainWidth(
                    placeables.maxOfOrNull { it.width } ?: 0
                )
                val height = constraints.constrainHeight(
                    placeables.sumOf { it.height }
                )
                return layout(width, height) {
                    var yPosition = 0
                    placeables.forEach { placeable ->
                        placeable.placeRelative(x = 0, y = yPosition)
                        yPosition += placeable.height
                    }
                }
            }

            override fun IntrinsicMeasureScope.minIntrinsicHeight(
                measurables: List<IntrinsicMeasurable>,
                width: Int
            ): Int {
                return measurables.sumOf { measurable ->
                    measurable.minIntrinsicHeight(width)
                }
            }

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

맞춤 layout 수정자를 만들 때 LayoutModifier 인터페이스에서 관련 메서드를 재정의합니다. 고유 계산은 measure와 일치해야 합니다. 예를 들어 특정 height의 경우 모든 측면에 고정 패딩을 추가하는 수정자는 하위 요소의 minIntrinsicWidth를 쿼리하기 전에 세로 패딩을 빼고 결과에 가로 패딩을 추가합니다.

fun Modifier.myCustomModifier(padding: Dp = 8.dp) =
    this then object : LayoutModifier {
        override fun MeasureScope.measure(
            measurable: Measurable,
            constraints: Constraints
        ): MeasureResult {
            val paddingPx = padding.roundToPx()
            val horizontal = paddingPx * 2
            val vertical = paddingPx * 2
            val placeable = measurable.measure(
                constraints.offset(-horizontal, -vertical)
            )
            val width = constraints.constrainWidth(placeable.width + horizontal)
            val height = constraints.constrainHeight(placeable.height + vertical)
            return layout(width, height) {
                placeable.place(paddingPx, paddingPx)
            }
        }

        override fun IntrinsicMeasureScope.minIntrinsicWidth(
            measurable: IntrinsicMeasurable,
            height: Int
        ): Int {
            val paddingPx = padding.roundToPx()
            val horizontal = paddingPx * 2
            val vertical = paddingPx * 2
            val childHeight = if (height == Constraints.Infinity) {
                Constraints.Infinity
            } else {
                (height - vertical).coerceAtLeast(0)
            }
            return measurable.minIntrinsicWidth(childHeight) + horizontal
        }

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