[Jetpack Compose] Create your custom layout

Juan·2022년 2월 27일
0

이 글은 Layouts in Jetpack Compose codelab 중 7. Create your custom layout의 내용을 정리한 글입니다.

문장 하나하나를 번역하지는 않았고 제가 이해한 내용을 바탕으로 정리했습니다. 100% 이해하고 작성한 내용이 아니라 틀린 내용이 있으면 지적해주시면 감사하겠습니다.

Principles of layouts in Compose

Composable 함수들에 의해 구성되는 UI 요소들은 하나의 부모와 여러 자식을 가질 수 있습니다. 그리고 부모 안에서의 위치를 (x, y) 값으로 가지며 width와 height 값을 크기로 갖습니다.

UI 요소 들은 주어진 제약 안에서 자기 자신을 측정해야 합니다. 제약 안에서 width와 height의 최소값과 최대값이 결정 됩니다. 만약 여러 자식 요소를 갖고 있다면 자기 자신의 크기를 결정하기 위해서는 먼저 모든 자식 요소의 크기를 측정해야 할 것입니다. 일단 자기 자신의 크기를 결정하고 나면 자신의 내부에 자식 요소들을 배치해나갈 수 있습니다.

Compose에서는 UI 요소들이 자식 요소들의 크기를 한번만 측정할 수 있습니다. (Single-pass measurement, 아직 무슨 말인지 잘 모르겠습니다.) 이는 성능 상의 이점을 갖습니다.

Using the layout modifier

layout modifier를 이용한 custom modifier를 먼저 구현해 보겠습니다. 일반적으로 custom modifier는 아래와 같은 구조를 갖습니다.

// customLayoutModifier을 Modifier의 확장 함수로 정의
// layout은 Modifier(this)의 함수이며 then으로 감싼 것은 chaining을 위한 것으로 보임
fun Modifier.customLayoutModifier(...) = this.then(
    layout { measurable, constraints ->
        ...
    }
)

layout modifier는 아래와 같은 두 개의 lambda parameter를 갖습니다.

  • measurable 측정되고 배치될 자식 요소
  • constraints 자식 요소의 width, height 최소값과 최대값

이제 firstBaselineToTop이라는 custom modifier를 만들어 봅시다. 이 modifier는 padding의 일종이지만 자식 요소의 top을 기준으로 적용되지 않고 자식 요소의 First baseline을 기준으로 적용됩니다.

이해를 돕기 위해 24dp 값을 기준으로 그림과 함께 두가지 경우를 살펴보겠습니다.

일반적인 Padding


자식 요소의 top을 기준으로 padding이 24dp 값으로 적용되어 있습니다.

일반적인 padding modifier에서 수행해야 할 작업을 예상해보면 다음과 같을 것입니다.

  1. 자식 요소인 Text의 크기를 측정
  2. 자식의 height에 24dp를 더해 자신의 height를 확정(width는 자식의 constraints를 그대로 따른다고 가정)
  3. 자신의 내부의 (0, 24dp)의 위치에 자식을 배치

firstBaselineToTop


이 padding은 top이 아닌 First baseline 위치를 기준으로 적용됩니다. 그림에서는 First baseline의 위치가 나와 있고 이후 코드에서 나올 First baseline 값은 f의 픽셀 길이를 의미합니다. (의미하는 것 같습니다, 정확한 정의는 찾지 못함)
이제 이 modifer에서 수행해야 할 작업은

  1. (마찬가지로) 자식 요소인 Text의 크기를 측정 (f, First baseline 값도 함께 측정됨)
  2. 24dp - fresidual 값을 계산
  3. 자식의 height인 hresidual을 더해 자신의 height를 확정(width는 자식의 constraints를 그대로 따른다고 가정)
  4. 자신의 내부의 (0, residual) 위치에 자식을 배치

이제 이 내용을 코드로 구현해보면 아래와 같습니다.

fun Modifier.firstBaselineToTop(
    firstBaselineToTop: Dp
) = this.then(
    layout { measurable, constraints ->
		// measure를 통해 주어진 constraints 하에서 자식 크기를 측정
        val placeable = measurable.measure(constraints)

		// 자식이 FirstBaseline이 존재하는 UI 요소인지 확인
        check(placeable[FirstBaseline] != AlignmentLine.Unspecified)
		// 그림 상의 f값을 저장
        val firstBaseline = placeable[FirstBaseline]

		// 그림 상의 residual 값을 계산
        val placeableY = firstBaselineToTop.roundToPx() - firstBaseline
		// 자식의 height + residual 값으로 자신의 height를 확정
        val height = placeable.height + placeableY

		// 자신의 크기를 지정하고 내부에 자식을 배치하는 
		// layout 함수를 호출 (Modifier.layout과는 다른 함수)
		// placeable.width는 자식의 maxWidth, height는 위에서 계산
        layout(placeable.width, height) {
			// 자식을 자신의 (0, residual) 위치에 배치
            placeable.placeRelative(0, placeableY)
        }
    }
)

Using the Layout composable

위의 custom modifier에서는 한 개의 자식 요소만을 다룹니다. 이제 Column 과 같이 여러 개의 자식 요소를 다루기 위해서는 Layout이라는 composable을 활용해야 합니다. 여러 개의 자식 요소를 다루기 위한 CustomLayout은 아래와 같은 형태를 갖습니다.

@Composable
fun CustomLayout(
    modifier: Modifier = Modifier,
		...
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
			...
    }
}

List 형태의 measurables만 제외하면 custom modifier 경우와 유사합니다.

Implementing a basic Column

이해를 돕기 위해 Column을 CustomLayout으로 직접 구현해 보겠습니다. 구현해야 할 로직은 아래와 같습니다.

  1. 모든 자식 요소의 크기를 측정합니다.
  2. 자신은 주어진 제약 하에서 가능한 큰 크기를 갖는다고 가정합니다.
  3. 자신의 내부에 자식들을 하나하나 배치합니다. (자식들을 수직으로 배치하기 위해 이전 자식의 height와 y좌표를 활용합니다.)

이제 이 내용을 코드로 구현하면 아래와 같습니다.

@Composable
fun MyOwnColumn(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
		// measure를 통해 주어진 constraints 하에서 모든 자식 크기를 측정
        val placeables = measurables.map { measurable ->
            measurable.measure(constraints)
        }

        // y좌표를 저장하기 위한 변수
        var yPosition = 0

        // 자신의 크기를 지정하고 내부에 자식을 배치하는 
		// layout 함수를 호출 (Modifier.layout과는 다른 함수)
		// constraints.maxWidth와 constraints.maxHeight는
		// 가능한 모든 공간을 차지하겠다는 의미
        layout(constraints.maxWidth, constraints.maxHeight) {
            // 모든 자식을 하나씩 배치함
            placeables.forEach { placeable ->
                placeable.placeRelative(x = 0, y = yPosition)

                // 이전 자식의 y좌표에 height를 더해 다음 자식의 y좌표를 구함
                yPosition += placeable.height
            }
        }
    }
}

내용이 쉽지 않은데 특히 아직까지 constraints의 정확한 의미를 잘 모르겠습니다. measure 호출 시 주어진 constraints가 아닌 새로운 값을 적용하는 예시도 좀 더 찾아봐야 할 것 같습니다.

profile
I love Kotlin!

0개의 댓글