읽기 전
- 불필요한 코드나 잘못 작성된 내용에 대한 지적은 언제나 환영합니다.
- 개인적으로 사용해보면서 배운 점을 정리한 글입니다.
Jetpack Compose의 레이아웃 코드랩에서 실습한 내용들을 정리합니다. Compose가 레이아웃 렌더링 시 크기 측정을 단 한번만 수행하는데 필요에 의해 미리 1회 이상 측정해야 하는 경우를 위한 측정 도구를 실습합니다. 코드랩 실습에 요구되는 시간은 대략 1시간 정도로 학습 목적을 고려하면 대략 2 - 3시간 정도를 투자해야 합니다. 분량이 너무 많아 챕터 별로 분할하여 업로드합니다.
- 내장 기능(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 컴포저블을 만들어보자.
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로 본 화면은 의도대로 동작하지 않는다. 구분선이 화면 전체 높이를 차지하여 비정상적으로 길어졌다.
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") } } }
Intrinsic Measure Customizing
대부분의 경우 기본적으로 제공되는 기능만으로도 충분하다. 그러나, Intrinsic Measure 방식을 목적에 맞게 커스터마이징하고자 하는 경우가 있을 수 있다. 이에 대해 Custom Layout 정의 시 MeasurePolicy 인터페이스를 구현하거나 Custom Modifier 작성 시 Density 인터페이스를 구현하는 경우로 나눌 수 있겠다.
- 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할 수 있다.
- 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
타입을 발견할 수가 없는데 IntrinsicMeasureScope
이 Density
타입을 상속받기에 Density.(min|max)Intrinsic(Width|Height)Of
함수를 override한다고 설명을 첨부한 듯하다.
참고자료
'Android' 카테고리의 다른 글
Android | Jetpack Compose State & Event (0) | 2022.12.05 |
---|---|
Android | Activity의 개념과 생명주기(Life Cycle) (0) | 2022.09.22 |
Codelab | Jetpack Compose layout - ConstraintLayout (0) | 2022.07.24 |
Codelab | Jetpack Compose layout - Complicated Custom Layout (0) | 2022.07.24 |
Codelab | Jetpack Compose layout - Custom Layout (0) | 2022.07.24 |