Android

Codelab | Jetpack Compose layout - Intrinsic Measure

8iggy 2022. 7. 24. 21:23

읽기 전

  • 불필요한 코드나 잘못 작성된 내용에 대한 지적은 언제나 환영합니다.
  • 개인적으로 사용해보면서 배운 점을 정리한 글입니다.

Jetpack Compose의 레이아웃 코드랩에서 실습한 내용들을 정리합니다. Compose가 레이아웃 렌더링 시 크기 측정을 단 한번만 수행하는데 필요에 의해 미리 1회 이상 측정해야 하는 경우를 위한 측정 도구를 실습합니다. 코드랩 실습에 요구되는 시간은 대략 1시간 정도로 학습 목적을 고려하면 대략 2 - 3시간 정도를 투자해야 합니다. 분량이 너무 많아 챕터 별로 분할하여 업로드합니다.

  1. 내장 기능(Intrinsic Measure)

Jetpack Compose를 사용하여 개발함에 있어 항상 유념해야 하는 점은 하위 요소(Child Element)를 한 번만 측정할 수 있다는 점이다. 하위 요소를 두 번 측정하도록 작성하면 Runtime Exception이 발생한다. 다만, 하위 요소를 측정하기 앞서 하위 요소에 대한 정보가 필요한 경우도 있다.

Intrinsic 기능을 사용하면 하위 요소가 실제로 measure되기 전에 하위 요소를 쿼리할 수 있다.

  • (min|max)InstrinsicWidth : width가 주어졌을 때 컴포저블을 그리기 위해 필요한 최소/최대 width 반환
  • (min|max)IntrinsicHeight : height가 주어졌을 때 컴포저블을 그리기 위해 필요한 최소/최대 height 반환

만약 width가 무한대인 Text 컴포저블에 대해 minIntrinsicHeight를 요청하면 single line으로 Text를 배치했을 때 필요한 height를 반환한다.

Intrinsic Measure 실제 사례

다음 그림처럼 화면에 구분선으로 구분된 두 개의 Text 컴포저블을 만들어보자.

Codelab_Jetpack_Compose_layout_034

Text 컴포저블을 클릭할 수 있게 하고자 함이 아니니 weight를 동일하게 부여하고 중앙에 구분선으로 작용할 Divider 컴포저블을 선언한다. 구분선의 높이는 텍스트의 높이만큼, 너비는 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
        )

        Divider(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() {
    Surface {
        TwoTexts(text1 = "Hi", text2 = "there")
    }
}

그러나 preview로 본 화면은 의도대로 동작하지 않는다. 구분선이 화면 전체 높이를 차지하여 비정상적으로 길어졌다.

Codelab_Jetpack_Compose_layout_035

Row 컴포저블이 각 하위 요소들을 측정하기 때문에 Text의 높이를 사용하여 Divider의 높이에 제약을 설정하지 못했다. 따라서, 텍스트의 높이에 맞추려고 부여했던 Divider의 Modifier에 입력한 fillMaxHeight로 인해 전체 화면 높이만큼 차지하였다. 의도대로 구현을 위해 Row 컴포저블의 Modifier에 height(Intrinsic.Min)을 부여하여 해결할 수 있다.

height(Intrinsic.Min)은 하위 요소의 크기를 결정할 때 minIntrinsicHeight를 요청한다. 추가로, 하위 요소들이 하위 요소를 갖는다면 재귀적으로 요청한다. 그리고 하위 요소들이 반환한 minIntrinsicHeight 값들 중 가장 큰 값이 Row 컴포저블의 minIntrinsicHeight 값이 된다.

Divider 컴포저블은 별도의 컨텐츠가 없어 Row 컴포저블의 하위 요소들인 Text 컴포저블들의 minIntrinsicHeight 중 큰 값이 Row의 minIntrinsicHeight가 된다. 따라서, Divider는 Row의 height에 대해 fillMaxHeight하기로 했으므로 Row 컴포저블로부터 전달받은 height 값만큼만 늘어나게 된다.

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

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

Codelab_Jetpack_Compose_layout_036

Intrinsic Measure Customizing

대부분의 경우 기본적으로 제공되는 기능만으로도 충분하다. 그러나, Intrinsic Measure 방식을 목적에 맞게 커스터마이징하고자 하는 경우가 있을 수 있다. 이에 대해 Custom Layout 정의 시 MeasurePolicy 인터페이스를 구현하거나 Custom Modifier 작성 시 Density 인터페이스를 구현하는 경우로 나눌 수 있겠다.

  1. Custom Layout 정의 시 MeasurePolicy 구현
@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.
        // Ex. (min|max)Intrinsic(Width|Height)
    }
}

주어진 레이아웃이 아니라 특정 목적에 맞게끔 레이아웃을 커스터마이징하고자 할 때, Intrinsic Measure 방식도 수정하고 싶다면 MeasurePolicy 인터페이스를 override하여 재정의할 수 있다. 예를 들자면, (min|max)Intrinsic(Width|Height) 함수들을 overriding할 수 있다.

  1. custom Modifier 정의 시 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.
})

Modifier 인터페이스 커스터마이징 시에도 Density.(min|max)Intrinsic(Width|Height)Of 함수를 override하여 Intrinsic Measure 방식을 커스터마이징 할 수 있다고 설명한다. 그러나 예제 코드에서는 Density 타입을 발견할 수가 없는데 IntrinsicMeasureScopeDensity 타입을 상속받기에 Density.(min|max)Intrinsic(Width|Height)Of 함수를 override한다고 설명을 첨부한 듯하다.

참고자료

  1. Google Codelab - Jetpack Compose Layout #10 내장 기능