Android Studio

[젯팩 컴포즈로 개발하는 안드로이드 UI] 4장 UI 요소 배치

vision333 2023. 7. 5. 16:30
728x90

미리 정의된 레이아웃 사용

요소가 어디에 나타나야하는지, 어느 크기가 돼야하는지 정의해야 한다.

배열하기 위한 기본적인 레이아웃 구성

  • 수평 - Row()
  • 수직 - Column()
  • 스택 - Box(), BoxWithConstraints()

기본 구성 요소 전환

@Composable
fun CheckboxWithLabel(label: String, state: MutableState<Boolean>) {
    Row(
        modifier = Modifier.clickable {
            state.value = !state.value
        },
        verticalAlignment = Alignment.CenterVertically,
    ) {
        Checkbox(checked = state.value, onCheckedChange = { state.value = it })
        Text(text = label, modifier = Modifier.padding(start = 8.dp))
    }
}
  • verticalAlignment = Alignment.CenterVertically : 열 내부에서 Checkbox와 Text를 수직선에서 가운데에 위치하도록 함
  • state: MutableState<Boolean> : onCheckedChange 내부에서 해당 값이 변경되면 다른 컴포저블 함수를 재구성
@Composable
fun PredefinedLayoutsDemo() {
    val red = remember { mutableStateOf(true) }
    val blue = remember { mutableStateOf(true) }
    val green = remember { mutableStateOf(true) }
    Column(
        modifier = Modifier.fillMaxSize().padding(16.dp),
    ) {
        //
    }
}
  • .fillMaxSize() : 사용할 수 있는 공간은 행으로 가득 채우기
  • .padding(16.dp) : 패딩 추가
  • red, blue, green 상태를 CheckboxWithLabel()로 전달
Column(
        modifier = Modifier.fillMaxSize().padding(16.dp),
    ) {
        CheckboxWithLabel(label = "red", state = red)
        CheckboxWithLabel(label = "blue", state = blue)
        CheckboxWithLabel(label = "green", state = green)

        Box(
            modifier = Modifier.fillMaxSize().padding(top = 16.dp),
        ) {
            if (red.value) {
                Box(modifier = Modifier.fillMaxSize().background(Color.Red))
            }
            if (blue.value) {
                Box(modifier = Modifier.fillMaxSize().padding(32.dp).background(Color.Blue))
            }
            if (green.value) {
                Box(modifier = Modifier.fillMaxSize().padding(64.dp).background(Color.Green))
            }
        }
    }

제약 조건을 기반으로 하는 레이아웃 생성

RelativeLayout / LinearLayout 은 성능에 영향을 일으켜서 ConstraintLayout로 View 계층 구조를 평탄화하여 문제를 방지하게 됨.

Box(), Row(), Column()을 중첩하는 것을 제한하고 싶을 때 ConstraintLayout()로 구현

@Composable
fun CheckboxWithLabel2(label: String, state: MutableState<Boolean>, modifier: Modifier = Modifier) {
    ConstraintLayout(
        modifier = modifier.clickable {
            state.value = !state.value
        },
    ) {
        val (checkbox, text) = createRefs()
        Checkbox(
            checked = state.value,
            onCheckedChange = {
                state.value = it
            },
            modifier = Modifier.constrainAs(checkbox) {},
        )
        Text(
            text = label,
            modifier = Modifier.constrainAs(text) {
                start.linkTo(checkbox.end, margin = 8.dp)
                top.linkTo(checkbox.top)
                bottom.linkTo(checkbox.bottom)
            }
        )
    }
}
  • ConstraintLayout()은 도메인 특화 언어(DSL)를 사용해 UI 요소의 위치와 크기를 정의함
  • ConstraintLayout() 내부에 있는 각각의 컴포저블 함수는 자신과 관련된 참조를 가져야 함
  • createRefs() : 참조 생성
  • constrainAs() : 제약 조건을 변경자에 의해 생성
  • ConstrainScope
    • constrainAs() 변경자의 람다 표현식은 ConstrainScope을 받음
    • start, top, bottom, end와 같은 프로퍼티를 생성
    • linkTo로 다른 컴포저블의 위치와 연결 → 앵커라고 부름
    • bottom.linkTo(checkbox.bottom) : 텍스트의 bottom과 체크박스의 bottom이 연결
val (cbRed, cbGreen, cbBlue, boxRed, boxBlue, boxGreen) = createRefs()
  • createRefs()로 제약조건 정의하는데 필요한 참조 생성
CheckboxWithLabel2(
    label = "red",
    state = red,
    modifier = Modifier.constrainAs(cbRed) { top.linkTo(parent.top) },
)
CheckboxWithLabel2(
    label = "blue",
    state = blue,
    modifier = Modifier.constrainAs(cbBlue) { top.linkTo(cbRed.bottom) },
)
  • 제약사항 추가
if (red.value) {
    Box(
        modifier = Modifier
            .background(Color.Red)
            .constrainAs(boxRed) {
                start.linkTo(parent.start)
                end.linkTo(parent.end)
                top.linkTo(cbGreen.bottom, margin = 16.dp)
                bottom.linkTo(parent.bottom)
                width = Dimension.fillToConstraints
                height = Dimension.fillToConstraints
            },
    )
}

if (blue.value) {
    Box(
        modifier = Modifier
            .background(Color.Blue)
            .constrainAs(boxBlue) {
                start.linkTo(parent.start, margin = 32.dp)
                end.linkTo(parent.end, margin = 32.dp)
                top.linkTo(cbGreen.bottom, margin = (16 + 32).dp)
                bottom.linkTo(parent.bottom, margin = 32.dp)
                width = Dimension.fillToConstraints
                height = Dimension.fillToConstraints
            },
    )
}
  • 제약을 boxRed에 건다면 red 체크박스가 해제되면 파란박스가 사라짐 (제약이 사라지니까..)

단일 측정 단계의 이해

측정 정책 정의

Column() 코드

@Composable
inline fun Column(
    modifier: Modifier = Modifier,
    verticalArrangement: Arrangement.Vertical = Arrangement.Top,
    horizontalAlignment: Alignment.Horizontal = Alignment.Start,
    content: @Composable ColumnScope.() -> Unit
) {
    val measurePolicy = columnMeasurePolicy(verticalArrangement, horizontalAlignment)
    Layout(
        content = { ColumnScopeInstance.content() },
        measurePolicy = measurePolicy,
        modifier = modifier
    )
}
  • measurePolicy 값 할당
  • Layout()에 content, measurePolicy, modifier을 전달하여 호출

columnMeasurePolicy() 호출

@PublishedApi
@Composable
internal fun columnMeasurePolicy(
    verticalArrangement: Arrangement.Vertical,
    horizontalAlignment: Alignment.Horizontal
) = remember(verticalArrangement, horizontalAlignment) {
    if (verticalArrangement == Arrangement.Top && horizontalAlignment == Alignment.Start) {
        DefaultColumnMeasurePolicy
    } else {
        rowColumnMeasurePolicy(
            orientation = LayoutOrientation.Vertical,
            arrangement = { totalSize, size, _, density, outPosition ->
                with(verticalArrangement) { density.arrange(totalSize, size, outPosition) }
            },
            arrangementSpacing = verticalArrangement.spacing,
            crossAxisAlignment = CrossAxisAlignment.horizontal(horizontalAlignment),
            crossAxisSize = SizeMode.Wrap
        )
    }
}
  • verticalArrangement, horizontalAlignment 값에 따라
    • DefaultColumnMeasurePolicy를 반환 → rowColumnMeasurePolicy()를 호출함
    • rowColumnMeasurePolicy()의 결과를 반환
  • rowColumnMeasurePolicy() → MeasurePolicy 를 반환

MeasuerPolicy

  • 레이아웃을 어떻게 측정하고 배치할지 정의하는 레이아웃

  • `override fun MeasureScope.measure()`

    • 매개변수 : measurables:List<Measurable>, constraints: Constraints

    • MeasurableResult 인스턴스 반환

    • 리스트에서 각 요소는 자식 레이아웃을 나타냄. child.measure()을 사용하여 측정

    • Measurable 코드

        interface Measurable : IntrinsicMeasurable {
            fun measure(constraints: Constraints): Placeable
        }
      • 레이아웃이 확장하고자하는 크기를 나타내는 Placeable 인스턴스 반환
  • 4가지 확장함수 제공

    • minIntrinsicWidth(), maxIntrinsicWidth() : 주어진 특정 높이에서 레이아웃의 최소, 최대 너비 값을 반환
    • minIntrinsicHeight(), maxIntrinsicHeight() : 주어진 특정 너비에서 레이아웃의 최서, 최대 높이 값을 반환

minIntrinsicWidth 함수

override fun IntrinsicMeasureScope.minIntrinsicWidth(
    measurables: List<IntrinsicMeasurable>,
    height: Int
) = MinIntrinsicWidthMeasureBlock(orientation)(
    measurables,
    height,
    arrangementSpacing.roundToPx()
)
  • 매개변수 : height, 자식 요소의 목록

커스텀 레이아웃 작성

가로세로 길이가 여러가지인 43개의 임의의 색을 입힌 박스를 생성

한줄이 꽉차면 알아서 아래로 내려가게 만들기

  • 박스 생성
@Composable
fun ColoredBox() {
    Box(
        modifier = Modifier
            .border(width = 2.dp, color = Color.Black)
            .background(randomColor())
            .width((40 * randomInt123()).dp)
            .height((10 * randomInt123()).dp),
    )
}

private fun randomInt123() = Random.nextInt(1, 4)
private fun randomColor() = when (randomInt123()) {
    1 -> Color.Red
    2 -> Color.Blue
    else -> Color.Green
}

@Composable
@Preview
fun CustomLayoutDemo() {
    SimpleFlexBox {
        for (i in 0..42) {
            ColoredBox()
        }
    }
}
  • 커스텀 레이아웃 구현
@Composable
fun SimpleFlexBox(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content,
        measurePolicy = simpleFlexboxMeasurePolicy()
    )
}

커스텀 측정 정책 구현

private fun simpleFlexboxMeasurePolicy(): MeasurePolicy =
    MeasurePolicy { measurables, constraints ->
        val placeables = measurables.map { measurable ->
            measurable.measure(constraints)
        }
        layout(
            constraints.maxWidth,
            constraints.maxHeight,
        ) {
            var yPos = 0
            var xPos = 0
            var maxY = 0
            placeables.forEach { placeable ->
                if (xPos + placeable.width > constraints.maxWidth) {
                    xPos = 0
                    yPos += maxY
                    maxY = 0
                }
                placeable.placeRelative(
                    x = xPos,
                    y = yPos,
                )
                xPos += placeable.width
                if (maxY < placeable.height) {
                    maxY = placeable.height
                }
            }
        }
    }

MeasurePolicy 구현체는 MeasureScope.measure() 구현체를 제공해야 함

MeasureResult 인터페이스의 객체 반환 : 이를 구현할 필요는 없지만, layout()를 호출해야 함

val placeables = measurables.map { measurable ->
    measurable.measure(constraints)
}

측정 정책은 content, 자식 리스트로 전달받음

자식 레이아웃은 배치 전에 정확히 한번만 측정됨

measurable.measure()를 호출하는 placeables의 맵을 생성해서 처리함

placeables를 반복하면서 xpos, ypos를 증가시켜 placeables의 위치를 계산함

728x90
반응형