시배's Android

Compose | 컴포즈 커스텀 레이아웃 구현하기 본문

Android/Compose

Compose | 컴포즈 커스텀 레이아웃 구현하기

si8ae 2023. 7. 16. 23:23

Layout

Compose의 Layout은 레이아웃을 커스터마이징 하는 데 사용됩니다. Layout은 자체적인 측정 및 배치 로직을 가지며, 하위 요소들을 포함하여 화면에 표시할 위치와 크기를 결정합니다.

먼저, Layout 클래스를 상속하여 사용자 정의 레이아웃을 작성해보겠습니다. 다음은 커스텀 레이아웃을 구현하기 위한 기본적인 코드입니다.

import androidx.compose.foundation.layout.Layout
import androidx.compose.runtime.Composable
import androidx.compose.ui.LayoutModifier
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.IntOffset
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.Constraints

@Composable
fun CustomLayout(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        // 측정 로직 구현
        val placeables = measurables.map { measurable ->
            measurable.measure(constraints)
        }

        // 배치 로직 구현
        layout(constraints.maxWidth, constraints.maxHeight) {
            // placeables를 사용하여 하위 요소 배치
            // 예: placeables.forEach { placeable -> placeable.place(...) }
        }
    }
}

위의 코드에서 CustomLayout은 커스텀 레이아웃을 생성하는 함수입니다. 이 함수는 modifier와 content 매개변수를 받습니다. modifier는 레이아웃에 적용할 수 있는 Compose Modifier이고, content는 레이아웃에 포함될 Composable 요소들입니다.

Layout 측정 로직은 measurables constraints 사용하여 하위 요소의 크기를 측정합니다. 그리고 배치 로직에서 layout 함수를 사용하여 하위 요소들을 화면에 배치합니다.

SubcomposeLayout

Compose 1.1.0부터 도입된 SubcomposeLayout Layout 유사하지만, 더욱 강력한 기능을 제공합니다. SubcomposeLayout 레이아웃 내부에서 자체적인 Composable 트리를 작성할 있게 해 주며, 필요한 경우에만 다시 작성될 있습니다.

위와 같은 채팅 로그를 구현할 때, 각 채팅 로그의 너비를 가장 긴 채팅 로그의 너비와 동일하게 구현이 필요로 할 수 있습니다.

@Composable
fun SubComposeLayoutDemo() {
    ResizeWidthColumn(Modifier.fillMaxWidth(), true) {

        Box(
            modifier = Modifier
                .background(Color.Red)
        ) {
            Text("Hello")
        }

        Box(
            modifier = Modifier
                .padding(top = 8.dp)
                .background(Color.Red)
        ) {
            Text("This is a long messsage \n and its longer")
        }
    }
}

@Composable
fun ResizeWidthColumn(modifier: Modifier, resize: Boolean, mainContent: @Composable () -> Unit) {
    SubcomposeLayout(modifier) { constraints ->
        val mainPlaceables = subcompose(SlotsEnum.Main, mainContent).map {
            // Here we measure the width/height of the child Composables
            it.measure(Constraints())
        }

        //Here we find the max width/height of the child Composables
        val maxSize = mainPlaceables.fold(IntSize.Zero) { currentMax, placeable ->
            IntSize(
                width = maxOf(currentMax.width, placeable.width),
                height = maxOf(currentMax.height, placeable.height)
            )
        }

        val resizedPlaceables: List<Placeable> =
            subcompose(SlotsEnum.Dependent, mainContent).map {
                if (resize) {
                    /** Here we rewrite the child Composables to have the width of
                     * widest Composable
                     */
                    it.measure(
                        Constraints(
                            minWidth = maxSize.width
                        )
                    )
                } else {
                    // Ask the child for its preferred size.
                    it.measure(Constraints())
                }
            }

        /**
         * We can place the Composables on the screen
         * with layout() and the place() functions
         */

        layout(constraints.maxWidth, constraints.maxHeight) {
            resizedPlaceables.forEachIndexed { index, placeable ->
                val widthStart = resizedPlaceables.take(index).sumOf { it.measuredHeight }
                placeable.place(0, widthStart)
            }
        }
    }
}


enum class SlotsEnum {
    Main,
    Dependent

}
  • subcompose 함수를 사용하여 mainContent를 SlotsEnum.Main 슬롯에 대해 subcompose 합니다. 이렇게 하면 mainContent의 Placeable 리스트가 반환됩니다.
  • 반환된 Placeable 리스트를 이용하여 주요 콘텐츠의 최대 너비와 높이를 계산합니다.
  • resize 값에 따라 mainContent를 SlotsEnum.Dependent 슬롯에 대해 다시 subcompose 합니다. resize가 true인 경우, 모든 콘텐츠의 너비를 최대 너비로 조정합니다. false인 경우에는 각 콘텐츠의 기본적인 크기를 유지합니다.
  • layout 함수를 사용하여 컨텐츠를 배치합니다. 주어진 constraints 최대 너비와 높이를 사용하여 컨텐츠를 배치합니다.

응용

이를 활용하여 Category TabBar를 Custom 하게 구현할 수 있습니다. 

카테고리의 아이템이 화면 너비를 초과하는 경우, 화면 너비를 기준으로 아이템의 크기를 자동으로 조정하여 화면에 모든 카테고리 아이템을 표시할려고 합니다.

 

@Composable
private fun TabRow(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    SubcomposeLayout(modifier = modifier) { constraints ->
        val mainPlaceables = subcompose(SlotsEnum.MAIN, content).map { it.measure(constraints) }

        var xPosition = 0
        var overflow = false

        for (mainPlaceable in mainPlaceables) {
            if (xPosition + mainPlaceable.width > constraints.maxWidth) {
                overflow = true
                break
            }

            xPosition += mainPlaceable.width
        }

        val resizedPlaceables: List<Placeable> =
            subcompose(SlotsEnum.DEPENDANT, content).map {
                if (overflow) {
                    val resizedWidth = constraints.maxWidth / mainPlaceables.size
                    it.measure(
                        Constraints(
                            maxWidth = resizedWidth,
                            maxHeight = mainPlaceables.maxOf { it.height })
                    )
                } else {
                    it.measure(Constraints())
                }
            }

        layout(
            width = constraints.maxWidth,
            height = resizedPlaceables.maxOf { it.height }
        ) {
            resizedPlaceables.forEachIndexed { index, placeable ->
                val widthStart = resizedPlaceables.take(index).sumOf { it.measuredWidth }
                placeable.place(widthStart, 0)
            }
        }
    }
}