Codelab | Jetpack Compose layout - Intrinsic Measure
읽기 전
- 불필요한 코드나 잘못 작성된 내용에 대한 지적은 언제나 환영합니다.
- 개인적으로 사용해보면서 배운 점을 정리한 글입니다.
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한다고 설명을 첨부한 듯하다.