Compose IntrinsicMeasure에 대하여

SSY·2024년 7월 26일
0

Compose

목록 보기
8/10
post-thumbnail

문제 상황

Row하위 컴포저블 함수에는 height()를 직접 적용해주지 않고 있는 상황이었다. 이는 글자들의 크기가 시스템 폰트 크기게 맞게 자동 조절될 수 있도록 하기 위함이었다.

그리고 이러한 Row컴포저블 함수의 높이에 맞게 세로선의 라인의 높이 또한 반응형 방식으로 지정할 필요가 있었다. (즉, 이러한 세로선 라인은 글자 크기에 따라 함께 길어지고 줄어들어야 함.) 하지만 Row 하위 컴포저블 함수엔 직접적으로 height()을 지정하지 않았기에, 왼쪽 세로선 라인을 fillMaxHeight()로 지정해도 되지 않는 문제가 있었다.

[요구사항 UI]

[작성한 코드]

해결책

위 문제의 원인은 Row컴포저블의 가용할 수 있는 높이가 지정돼있지 않아서였다. 하지만 Row컴포저블 함수에 직접적으로 height()를 통해 높이를 지정하자니, UI가 반응형이 되지 않는다는 문제가 존재했다. 즉, Specer의 높이는 Column의 높이에 맞게 조정되어야만 한다.

이를 위해, 결국 상위 컴포저블 함수인 Rowheight()를 지정해야만 한다는 결론이 나왔고, 이를 가능하게 하는 IntrinsicSize.Min를 사용하면 된다는 결론이 나왔다.

IntrinsicSize란?

Intrinsic이라는 말은 '본질적인' 또는 '고유한'이라는 뜻이 있다. 하지만 이를 1차원적으론 이해가 어려워, 좀 더 뜻을 생각해봐야 한다. 좀 더 생각해보자면 IntrinsicSize는 상위 컴포저블에서 사용하는 속성값이고, 여기에 '본질적인'이라는 뜻을 고민해보면 '하위 컴포저블 함수'라고 유추해볼 수 있다. 즉, 하위 컴포저블 함수의 사이즈라는 뜻이다.

[IntrinsicSize란?] : 하위 컴포저블 함수의 사이즈

IntrinsicSize.Min or Max?

구글에선 아래의 코드를 통해 IntrinsicSize.Min을 설명해주고 있다. 하지만 IntrinsicSize.Max와의 차이 뿐만 아니라 쓰임새까지 이해하는덴 어려움이 있었다. 아래 코드를 간단히 설명하자면, 상위 컴포저블 함수(=Row) 높이가 IntrinsicSize.Min로 지정되고, 이를 Text와 동일하게 설정한다는 뜻이다. 그리고 이에 맞춰, SpacerfillMaxHeight()의 높이 또한 Text에 맞게 가져간다고 이해할 수 있다.

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

하지만 아래 StackOverflow를 통해 이를 좀 더 잘 이해할 수 있게되었다.

https://stackoverflow.com/questions/76147960/how-to-use-intrinsicsize-min-with-canvas-in-jetpack-compose

위와 같은 UI가 나오기 위해선 custom한 layout을 첫 번째로 정의한다.

@Composable
fun CustomColumnWithIntrinsicDimensions(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    val measurePolicy = object : MeasurePolicy {

        override fun MeasureScope.measure(
            measurables: List<Measurable>,
            constraints: Constraints
        ): MeasureResult {

            val looseConstraints = constraints.copy(minHeight = 0)
            val placeables = measurables.map { measurable ->
                measurable.measure(looseConstraints)
            }

            var yPosition = 0

            val totalHeight: Int = placeables.sumOf {
                it.height
            }

            // 🔥 This can be sum or longest of Composable widths, or maxWidth of Constraints
            val maxWidth: Int = placeables.maxOf {
                it.width
            }

            return layout(maxWidth, totalHeight) {
                placeables.forEach { placeable ->
                    placeable.placeRelative(x = 0, y = yPosition)
                    yPosition += placeable.height
                }
            }
        }

        override fun IntrinsicMeasureScope.minIntrinsicHeight(
            measurables: List<IntrinsicMeasurable>,
            width: Int
        ): Int {

            println("🍏 minIntrinsicHeight() width: $width, measurables: ${measurables.size}")
            // 🔥 This is just sample to show usage of minIntrinsicHeight, don't set
            // static values
            return 200
        }

        override fun IntrinsicMeasureScope.maxIntrinsicHeight(
            measurables: List<IntrinsicMeasurable>,
            width: Int
        ): Int {

            println("🍎 maxIntrinsicHeight() width: $width, measurables: ${measurables.size}")

            // 🔥 This is just sample to show usage of maxIntrinsicHeight, don't set
            // static values
            return 400
        }
    }

    Layout(modifier = modifier, content = content, measurePolicy = measurePolicy)
}

그리고 위 코드를 아래와 같이 사용한다.

Text(text = "No height Modifier")
CustomColumnWithIntrinsicDimensions(
    modifier = Modifier
        .width(100.dp)
        .background(Green400)
        .padding(4.dp)
) {
    Text(
        "First Text",
        modifier = Modifier
            .background(Color(0xffF44336)),
        color = Color.White
    )
    Text(
        "Second Text",
        modifier = Modifier
            .background(Color(0xff9C27B0)),
        color = Color.White
    )
}

Text(text = "height(IntrinsicSize.Min)")
CustomColumnWithIntrinsicDimensions(
    modifier = Modifier
        .width(100.dp)
        .height(IntrinsicSize.Min)
        .background(Yellow400)
        .padding(4.dp)
) {
    Text(
        "First Text",
        modifier = Modifier
            .background(Color(0xffF44336)),
        color = Color.White
    )
    Text(
        "Second Text",
        modifier = Modifier
            .background(Color(0xff9C27B0)),
        color = Color.White
    )
}

Text(text = "height(IntrinsicSize.Max)")
CustomColumnWithIntrinsicDimensions(
    modifier = Modifier
        .width(100.dp)
        .height(IntrinsicSize.Max)
        .background(Blue400)
        .padding(4.dp)
) {
    Text(
        "First Text",
        modifier = Modifier
            .background(Color(0xffF44336)),
        color = Color.White
    )
    Text(
        "Second Text",
        modifier = Modifier
            .background(Color(0xff9C27B0)),
        color = Color.White
    )
}

CustomColumnWithIntrinsicDimensions()내, minIntrinsicHeight(), maxIntrinsicHeight()의 반환값이 각각 200, 400으로 정의된 것을 볼 수 있다. 즉, Modifier를 사용해서 IntrinsicSize.Min을 지정할 경우, minIntrinsicHeight()의 200px가 반환된다는 것이고, IntrinsicSize.Max를 지정할 경우, maxIntrinsicHeight()의 400px가 반환된다는 것이다.

이를 통해, 아래와 같이 매핑된다는 것을 알 수 있다.

  • .width(IntrinsicSize.Min) -> IntrinsicMeasureScope.minIntrinsicWidth()의 호출
  • .width(IntrinsicSize.Max) : IntrinsicMeasureScope.maxIntrinsicWidth()의 호출
  • .height(IntrinsicSize.Min) : IntrinsicMeasureScope.minIntrinsicHeight()의 호출
  • .height(IntrinsicSize.Max) : IntrinsicMeasureScope.maxIntrinsicHeight()의 호출

How To Use IntrinsicSize.Min or Max?

개인적인 견해입니다.

compose API에서는 수많은 컴포저블 함수를 제공해준다. Button, Text, OutlinedTextField, TextField, CheckBox, HorizontalPager, Column, LazyColomn, Row, LazyRow등등.. 무수히 많다. 그리고 이들은 커스텀한 Layout컴포저블 함수를 구현하고 있음과 동시에 위 4가지 메서드들 또한 오버라이딩 하고 있을 것이다. 하지만 현실적으로 이러한 모든 메서드들의 구현체를 분석하는 것은 쉽지 않을것이며, 이에 맞는 IntrinsicSize 옵션에 따른 동작 사양을 안드로이드 공식 홈페이지에서도 가이드하고 있지도 않다. 뿐만 아니라 Row함수의 경우, IntrinsicSize.MaxIntrinsicSize.Min의 쓰임새 또한 동일한 것을 확인할 수 있어, 정확한 쓰임새를 알기가 힘들다 생각한다.

따라서 이를 잘 사용하기 위해선 위에서 강조한 아래 사항,

  • .width(IntrinsicSize.Min) -> IntrinsicMeasureScope.minIntrinsicWidth()의 호출
  • .width(IntrinsicSize.Max) : IntrinsicMeasureScope.maxIntrinsicWidth()의 호출
  • .height(IntrinsicSize.Min) : IntrinsicMeasureScope.minIntrinsicHeight()의 호출
  • .height(IntrinsicSize.Max) : IntrinsicMeasureScope.maxIntrinsicHeight()의 호출

의 매핑 관계를 기억하며, 요구사항에 의미적으로 일치하는 메서드를 사용해야할 것으로 보인다. 예를 들어, 내가 위에서 마주한 문제 상황에선 좌측 세로 라인이 오른쪽 뷰의 최소 높이에만 부합하면 되므로, IntrinsicSize.Min을 사용하면 된다고 생각한다.

요약

  1. IntrinsicSize 옵션은 아래와 같이 매핑된다.
  • .width(IntrinsicSize.Min) -> IntrinsicMeasureScope.minIntrinsicWidth()의 호출
  • .width(IntrinsicSize.Max) : IntrinsicMeasureScope.maxIntrinsicWidth()의 호출
  • .height(IntrinsicSize.Min) : IntrinsicMeasureScope.minIntrinsicHeight()의 호출
  • .height(IntrinsicSize.Max) : IntrinsicMeasureScope.maxIntrinsicHeight()의 호출
  1. 모든 컴포저블 함수의 IntrinsicSize동작 사양을 알기 힘드므로, 요구사항에 맞게 사용한다.
profile
불가능보다 가능함에 몰입할 수 있는 개발자가 되기 위해 노력합니다.

0개의 댓글