Customizing Layout in Jetpack Compose

How to layout in compose

하나의 Composable은 하나의 부모 Composable을 가지며 여러 자식 Composable을 가질 수 있다. 각각의 Composable은 부모 Composable 안에서 x, y좌표에 의해 배치되고 너비와 높이를 가진다.

각각의 Composable은 부모가 넘겨주는 제약조건으로부터 자신의 size를 정하게 된다.

제약조건(Constraints): Composable이 가질 수 있는 width, height의 min/max 값

Compose는 레이아웃을 크게 3가지 단계에 걸쳐 진행된다.

Measure children -> Decide own size -> Place children
  1. Measure children: Composable이 Compose 런타임에 처음으로 등록된 시점에 수행되며 자식 composable이 어떻게 배치되어야 하는 지에 대한 정보를 포함시킨다.
  2. Decide own size: Composable의 크기를 결정한다.
  3. Place children: 앞 두 단계를 거쳐 얻은 위치와 크기 정보를 통해 실제로 화면에 Composable을 배치한다.

Compose는 성능 저하를 막기 위해 자식 Compossable의 size를 단 한번만 측정할 수 있으며 여러번 측정을 시도할 경우 런타임 에러가 발생한다. 자식 Composable의 크기를 측정하기 전에 추가적인 정보가 필요할 경우 intrinsic measurement라는 기능을 이용한다.

Box Implementation

Layout Composable

@Composable  
inline fun Box(  
    modifier: Modifier = Modifier,  
    contentAlignment: Alignment = Alignment.TopStart,  
    propagateMinConstraints: Boolean = false,  
    content: @Composable BoxScope.() -> Unit  
) {  
    val measurePolicy = rememberBoxMeasurePolicy(contentAlignment, propagateMinConstraints)  
    Layout(  
        content = { BoxScopeInstance.content() },  
        measurePolicy = measurePolicy,  
        modifier = modifier  
    )  
}

Layout Composable은 자식 Composable을 배치할 content, 자식 Composable을 주어진 제약조건을 사용하여 측정하고 배치하는 measurePolicy, modifier로 구성되어 있다.

Measure Policy

MeasurePolicy를 통해 하위 요소와 제약조건을 이용하여 하위 요소의 크기와 위치를 결정하고 배치한다. 이 과정에서 자신의 크기 또한 결정한다.

MeasurePolicyinterface로 구성되어 있으며 MeasurePolicy.measure를 구현해야 한다. 해당 메소드는 measurablesconstraints를 인자로 받고 MeasureResult를 반환한다.

  • Measurables: 각각의 하위 Composable를 나타내며 하위 Composable의 크기를 Measurable.measure메소드를 이용하여 측정할 수 있다. 해당 메소드의 반환 값은 Placeable인데, Placeable.place메소드를 이용하여 자식 Composable을 배치할 위치를 결정한다.
  • Constraints: 크기를 측정하기 위한 제약조건을 가지고 있다.
  • MeasureResult: MeasureScope.layout 메소드를 이용하여 만들며 이 객체 안에서 Placeable.place 메소드를 사용하여 Composable의 크기와 자식 Composable의 위치를 지정한다. 이 객체는 Composer가 실제로 배치하는 데 사용된다.

Example. Box Measure Policy

internal fun boxMeasurePolicy(alignment: Alignment, propagateMinConstraints: Boolean) =  
    MeasurePolicy { measurables, constraints ->  
        if (measurables.isEmpty()) {  
            return@MeasurePolicy layout(  
                constraints.minWidth,  
                constraints.minHeight  
            ) {}  
        }  
  
        val contentConstraints = if (propagateMinConstraints) {  
            constraints  
        } else {  
            constraints.copy(minWidth = 0, minHeight = 0)  
        }  
  
        if (measurables.size == 1) {  
            val measurable = measurables[0]  
            val boxWidth: Int  
            val boxHeight: Int  
            val placeable: Placeable  
            if (!measurable.matchesParentSize) {  
                placeable = measurable.measure(contentConstraints)  
                boxWidth = max(constraints.minWidth, placeable.width)  
                boxHeight = max(constraints.minHeight, placeable.height)  
            } else {  
                boxWidth = constraints.minWidth  
                boxHeight = constraints.minHeight  
                placeable = measurable.measure(  
                    Constraints.fixed(constraints.minWidth, constraints.minHeight)  
                )  
            }  
            return@MeasurePolicy layout(boxWidth, boxHeight) {  
                placeInBox(placeable, measurable, layoutDirection, boxWidth, boxHeight, alignment)  
            }  
        }  
  
        val placeables = arrayOfNulls<Placeable>(measurables.size)  
        // First measure non match parent size children to get the size of the Box.  
        var hasMatchParentSizeChildren = false  
        var boxWidth = constraints.minWidth  
        var boxHeight = constraints.minHeight  
        measurables.fastForEachIndexed { index, measurable ->  
            if (!measurable.matchesParentSize) {  
                val placeable = measurable.measure(contentConstraints)  
                placeables[index] = placeable  
                boxWidth = max(boxWidth, placeable.width)  
                boxHeight = max(boxHeight, placeable.height)  
            } else {  
                hasMatchParentSizeChildren = true  
            }  
        }  
  
        // Now measure match parent size children, if any.  
        if (hasMatchParentSizeChildren) {  
            // The infinity check is needed for default intrinsic measurements.  
            val matchParentSizeConstraints = Constraints(  
                minWidth = if (boxWidth != Constraints.Infinity) boxWidth else 0,  
                minHeight = if (boxHeight != Constraints.Infinity) boxHeight else 0,  
                maxWidth = boxWidth,  
                maxHeight = boxHeight  
            )  
            measurables.fastForEachIndexed { index, measurable ->  
                if (measurable.matchesParentSize) {  
                    placeables[index] = measurable.measure(matchParentSizeConstraints)  
                }  
            }  
        }  
  
        // Specify the size of the Box and position its children.  
        layout(boxWidth, boxHeight) {  
            placeables.forEachIndexed { index, placeable ->  
                placeable as Placeable  
                val measurable = measurables[index]  
                placeInBox(placeable, measurable, layoutDirection, boxWidth, boxHeight, alignment)  
            }  
        }    
    }

Box의 경우 3가지 경우로 나누어 측정과 배치를 시도한다.

  • content로 제공된 Composable이 없을 경우
if (measurables.isEmpty()) {  
    return@MeasurePolicy layout(  
        constraints.minWidth,  
        constraints.minHeight  
    ) {}  
}

상위 제약조건의 최소 크기만큼을 Box의 크기로 삼는다.

  • content로 제공된 Composable이 하나일 경우
if (measurables.size == 1) {  
    val measurable = measurables[0]  
    val boxWidth: Int  
    val boxHeight: Int  
    val placeable: Placeable  
    if (!measurable.matchesParentSize) {  
        placeable = measurable.measure(contentConstraints)  
        boxWidth = max(constraints.minWidth, placeable.width)  
        boxHeight = max(constraints.minHeight, placeable.height)  
    } else {  
        boxWidth = constraints.minWidth  
        boxHeight = constraints.minHeight  
        placeable = measurable.measure(  
            Constraints.fixed(constraints.minWidth, constraints.minHeight)  
        )  
    }  
    return@MeasurePolicy layout(boxWidth, boxHeight) {  
        placeInBox(placeable, measurable, layoutDirection, boxWidth, boxHeight, alignment)  
    }  
}

measurablematchsParentSize 여부에 따라 결정한다. 만약 matchesParentSize일 경우 Box의 크기를 Box의 최소 크기 제약조건 또는 하위 Composable의 측정된 크기 중 큰 값을 선택하며 그렇지 않을 경우 Box의 최소 크기 제약조건을 이용하여 하위 Composable의 크기를 측정한다.

  • content로 제공된 Composable이 둘 이상일 경우
val placeables = arrayOfNulls<Placeable>(measurables.size)  
// First measure non match parent size children to get the size of the Box.  
var hasMatchParentSizeChildren = false  
var boxWidth = constraints.minWidth  
var boxHeight = constraints.minHeight  
measurables.fastForEachIndexed { index, measurable ->  
    if (!measurable.matchesParentSize) {  
        val placeable = measurable.measure(contentConstraints)  
        placeables[index] = placeable  
        boxWidth = max(boxWidth, placeable.width)  
        boxHeight = max(boxHeight, placeable.height)  
    } else {  
        hasMatchParentSizeChildren = true  
    }  
}  
  
// Now measure match parent size children, if any.  
if (hasMatchParentSizeChildren) {  
    // The infinity check is needed for default intrinsic measurements.  
    val matchParentSizeConstraints = Constraints(  
        minWidth = if (boxWidth != Constraints.Infinity) boxWidth else 0,  
        minHeight = if (boxHeight != Constraints.Infinity) boxHeight else 0,  
        maxWidth = boxWidth,  
        maxHeight = boxHeight  
    )  
    measurables.fastForEachIndexed { index, measurable ->  
        if (measurable.matchesParentSize) {  
            placeables[index] = measurable.measure(matchParentSizeConstraints)  
        }  
    }  
}  
  
// Specify the size of the Box and position its children.  
layout(boxWidth, boxHeight) {  
    placeables.forEachIndexed { index, placeable ->  
        placeable as Placeable  
        val measurable = measurables[index]  
        placeInBox(placeable, measurable, layoutDirection, boxWidth, boxHeight, alignment)  
    }  
}

모든 matchesParentSize 가 아닌 measureble객체를 이용하여 Box의 크기를 결정한다. 이후 matchesParentSizemeasurable은 따로 제약조건을 구하고 이를 이용하여 크기를 측정한다.

모든 측정이 끝나면 placeables를 순회하여 배치한다.

Layout Modifier 작성 예정


© 2021. All rights reserved.

Powered by Hydejack v9.1.6