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 작성 예정