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

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

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

컴포저블에 intrinsicWidth 또는 intrinsicHeight를 요청할 수 있습니다.

  • (min|max)IntrinsicWidth: 이 너비에서 콘텐츠를 적절하게 그릴 수 있는 최소/최대 너비는 얼마인가요?
  • (min|max)IntrinsicHeight: 이 높이에서 콘텐츠를 적절하게 그릴 수 있는 최소/최대 높이는 무엇인가요?

예를 들어 height가 무한대인 TextminIntrinsicHeight를 요청하면 텍스트가 한 줄에 그려진 것처럼 Textheight가 반환됩니다.

내장 기능 실제 사례

다음과 같이 화면에 구분선으로 구분된 두 텍스트를 표시하는 컴포저블을 만든다고 가정해 보겠습니다.

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

이렇게 하려면 어떻게 해야 하나요? 안에 두 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
        )
        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가 전체 화면으로 확장되며 이는 원하는 결과가 아닙니다.

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

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

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

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

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

맞춤 레이아웃의 내장 기능

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

맞춤 Layout의 내장 기능 측정을 지정하려면 만들 때 MeasurePolicy 인터페이스의 minIntrinsicWidth, minIntrinsicHeight, maxIntrinsicWidth, maxIntrinsicHeight를 재정의하세요.

@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.
}