본문으로 바로가기

[Compose] 10. Intrinsic Measure

category 개발이야기/Android 2021. 10. 4. 23:57
반응형

Phoyo by unsplash

Compose는 child elements를 한번만 measure할수 있습니다. 그렇지 않으면 runtime에서 exception이 발생합니다. 하지만 childe elements를 측정하기 전에 미리 특정 정보가 필요한 경우 Intrisic measure를 사용하며 이 사용법에 대해서 알아봅니다.

이 글은 Android developer 공식 사이트에서 제공하는 문서를 기반으로 의역, 번역하였습니다. [1]

 

Intrinsic 명령어를 사용하면 child를 measure하기 전에 query해 볼수 있습니다. 

  • (min|max)IntrinsicWidth: height가 주어진 상태에서 content를 그리기 위해 필요한 최소/최대의 width 반환
  • (min|max)IntrinsicHeight: width가 주어진 상태에서 content를 그리기 위해 필요한 최소/최대의 height 반환

하지만 그렇다고 두번 measure를 하는건 아닙니다. child elements는 실제로 measure 되기 전에 intrinsic measurement를 요청 받습니다. 그리고 이 정보를 가지고 부모는 child를 measure할때 제약사항(constraints)를 계산합니다.

예를들어 width가 무한이 제공되는 상태라고 할때 TextminIntrinsicHeight를 구하면 single line으로 text를 배치했을때 필요한 높이가 반환됩니다.

Intrinsics in action

위와 같이 두개의 text를 배치하고 중간에 divider를 배치시키고자 합니다. Text는 양쪽으로 최대한으로 확장 가능하며 줄바꿈이 되어 높이가 늘어가면 늘어난 놓이만큼 divider의 길이도 늘어나도록 합니다. 즉 양쪽 Text의 높이중 더 큰값에 맞춰 divider의 높이를 변경합니다.

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

        Divider(color = Color.Black, modifier = Modifier.fillMaxHeight().width(1.dp))
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(end = 4.dp)
                .wrapContentWidth(Alignment.End),
            text = text2
        )
    }
}

코드는 어렵지 않습니다. 두개의 Text를 배치하고 weight를 이용하여 동일한 공간을 자치하도록 했고, alignment를 이용하여 시작과 끝에 배치 시켰습니다. 그리고 divider의 크기는 최대 높이로 표시하도록 지정했습니다. 위 코드의 preview는 아래와 같습니다.

Divider는 Text의 최대 height로 맞추기 위해 fillMaxHeight()로 설정하였으나, 이 때문에 오히려 화면 전체를 분할해 버리는 상황이 발생합니다. 이는 Row가 child를 각각 따로 measure하기 때문에 Text의 height를 Divider에 제약 조건으로 줄수 없기 때문입니다. 따라서 Row의 Modifier에 height(IntrinsicSize.Min)을 주도록 하여 아래와 같이 코드를 변경하면 원래 원하던 형태로 바꿀 수 있습니다.

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

        Divider(color = Color.Black, modifier = Modifier.fillMaxHeight().width(1.dp))
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(end = 4.dp)
                .wrapContentWidth(Alignment.End),
            text = text2
        )
    }
}

위와같이 원하는대로 그려지는 이유는 아래와 같습니다.

  1. Row의 IntrinsicSize.Min에 의해서 모든 Child elements에 대한 minIntrinsicHeight가 동작합니다. (child의 child가 있을경우 recursive하게 모두 동작)
  2. Child의 minIntrinsicHeight값중에 제일 큰 값이 Row의 minIntrinsicHeight가 됩니다.
  3. 이때 Divider는 아무런 constraints가 없으므로 minIntrinsicHeight가 0이 됩니다.
  4. 따라서 Text두개의 minIntrinsicHeight중 큰값이 Row의 minIntrinsicHeight이 됩니다.
  5. Divicer는 Row로 부터 전달받은 height에 대한 constraint를 기준으로 해당 height까지만 늘어나게 됩니다.

Intrinsic in your custom layouts

Custom Layout이나 Layout modifier를 만들때 intrinsic Measurements는 근사치로 자동계산되기 때문에 실제 layout과 조금 다를수 있습니다. 따라서 정확한 계산을 위해 직접 intrinsic measure방법을 제공하려면 아래의 예제처럼 override함수를 사용해야 합니다.[2]

Custom composable이 생성시 MeausrePolicy interface의 minIntrinsicWidth, maxIntrinsicWidth, minIntrinsicHeight, maxIntrinsicHeight를 직접 override할 수 있습니다.

1. Custom Layout 사용시

@Composable
fun MyCustomComposable(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    return 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
        ) = {
            // Logic here
        }

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

2. Custom layout modifier 사용시

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

 

References

[1] https://developer.android.com/codelabs/jetpack-compose-layouts#10

[2] https://developer.android.com/jetpack/compose/layouts/intrinsic-measurements

반응형