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
Measure children: Composable이 Compose 런타임에 처음으로 등록된 시점에 수행되며 자식 composable이 어떻게 배치되어야 하는 지에 대한 정보를 포함시킨다.Decide own size: Composable의 크기를 결정한다.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를 통해 하위 요소와 제약조건을 이용하여 하위 요소의 크기와 위치를 결정하고 배치한다. 이 과정에서 자신의 크기 또한 결정한다.
MeasurePolicy는 interface로 구성되어 있으며 MeasurePolicy.measure를 구현해야 한다. 해당 메소드는 measurables와 constraints를 인자로 받고 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)
}
}
measurable의 matchsParentSize 여부에 따라 결정한다. 만약 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의 크기를 결정한다. 이후 matchesParentSize 인 measurable은 따로 제약조건을 구하고 이를 이용하여 크기를 측정한다.
모든 측정이 끝나면 placeables를 순회하여 배치한다.
Layout Modifier 작성 예정