코틀린 & Java/컴포즈 Compose

컴포즈(Compose) UI 요소 배치

코딩하는후운 2023. 8. 23. 16:17
반응형

미리 정의된 레이아웃 사용

고려해야 할 세가지 축

  • 수평(Horizontal)
  • 수직(Vertical)
  • 스택(Stacked)

Row() - 가로

Column() - 세로

Box() & BoxWithConstraint() - 콘텐츠를 맨위에 쌓음.

CheckBox() - 현재 상태(checked)와 선택하면 호출되는 람다(onCheckedChange)를 받는다

  • 함수 작성시점에 레이블 전달할 수 없다.
  • 대신 CheckBox(), Text()를 Row()안에 포함해 유사한 결과물 만들 수 있다.

CheckBoxWithLabel()은 MutableState<Boolean>을 받는다.

  • onCheckedChange 내부에서 값이 변경되면 다른 컴포저블 함수를 재구성해야 하기 때문이다

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

코드를 단순하고 깔끔하게 만들고자 Box(), Row(), Cloumn()을 중첩하는 행위를 제한하고 싶을 수 있다.

ConstraintLayout()

  • 도메인 특화 언어를 사용해 다른 요소와 연관있는 UI요소의 위치와 크기를 정의
  • 자신과 관련된 참조를 갖고 있어야 하며 createRefs()를 사용해 생성
  • 제약 조건은 constrainAs() 변경자를 사용해 제공
    • 변경자의 람다 표현식은 ConstrainScope를 받음
    • start, top, bottom과 같은 프로퍼티를 포함
      • 이 프로퍼티는 다른 컴포저블 위치와 연결되는 위치를 정의하기 때문에 앵커라 부름
    • parent는 ConstraintLayout()을 의미

ConstraintLayout()의 동작 방식

  • 컴포저블의 앵커를 다른 앵커와 연결함으로써 컴포저블 함수 간 제약 관계를 형성
  • 이러한 연결은 참조 기반으로 한다. 참조를 설정하려면 createRefs()를 호출해야만 한다.

Box()방식

  • 또 다른 Box를 쌓아 올려야 했다.

Constraint()방식

  • 세개의 박스가 동일한 부모(ConstraintLayout())를 공유하게 된다.

→ 이는 컴포저블 함수 개수를 줄이고 코드를 더욱 깔끔하게 만드는 효과

단일 측정 단계의 이해

레이아웃은 콘텐츠의 크기를 얻거나 측정하고 나면 자식(콘텐츠)을 배치할 것이다.

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에 값을 할당하는 부분을 제외하면
    • content, measurePolicy, modifier를 Layout에 전달해 호출할 뿐이다.
  • measurePolicy 변수는 MeasurePolicy 인터페이스의 구현체를 참조
    • columnMeasurePolicy()를 호출한 결과값이 이에 해당

측정 정책 정의

columnMeasurePolicy()

  • verticalArrangement와 horizontalAlignment값에 따라
    • DefaultColumnMeasurePolicy나 rowColumnMeasurePolicy()의 결과를 반환
  • DefaultColumnMeasurePolicy는 rowColumnMeasurePolicy를 호출
    • rowColumMeasurePolicy는 MeasurePolicy를 반환

MeasurePolicy

  • androidx.compose.ui.layout에 포함
  • 레이아웃을 어떻게 측정하고 배치할지 정의
    • 떄문에 미리 정의된 (Box(), Row(), Column()) 레이아웃과 커스텀 레이아웃의 기본 구성요소가 된다.
  • 가장 중요한 함수는 MeasureScope의 확장 함수인 measure()
    • measure()
    • List<Measurable>, Constraints 두 개의 매개변수를 받는다.
      • 리스트 : 자식 레이아웃을 나타낸다
    • 이러한 함수는 Measurable, measure()을 사용해 측정할 수 있다.
      • 자식 레이아웃이 확장하고자 하는 크기를 나타내는 Placeable 인스턴스 반환

MeasureScope.measure()

  • MeasureResult 인스턴스를 반환
  • 이 인터페이스는 다음의 컴포넌트를 정의
    • 레이아웃의 크기(width, height)
    • 정렬선(aligmentLines)
    • 자식 요소를 배치하기 위한 로직(placeChildren())

UI 복잡도에 따라 자식 레이아웃이 경계안에 잘 맞지 않는다는 것을 발견할 수도 있다.

  • 이 때 다른 측정 환경설정을 전달해 자식 레이아웃을 다시 측정하고 싶을 수도 있다.
  • Android View시스템에서는 자식 레이아웃을 다시 측정하는것이 가능하지만 성능 저하를 일으킬 수 있다.
  • 때문에 컴포즈에서 레이아웃은 콘텐츠를 한번만 측정한다.
    • 다시 측정하려고 시도할 경우 예외 발생

반면

레이아웃은 자식 레이아웃의 고유크기를 질의할 수 있다.(부모가 자식크기를 물어봐서 알수 있다는 것인듯)

이를 사용해 크기나 위치를 지정.

MeasurePolicy에서는 IntrinsicMeasureScope에 대한 네가지 확장 함수를 정의하고 있다.

  • minIntrinsicWidth()
  • maxIntrinsicWidth()
  • minIntrinsicHeight()
  • maxIntrinsicHeight()

: 주어진 특정 (높이 or 너비)에서 최소 최대 (너비 or 높이)값 반환

minIntrinsicWidth을 예로 파악해보기

fun IntrinsicMeasureScope.minIntrinsicWidth(
    measurables: List<IntrinsicMeasurable>,
    height: Int
): Int {
    val mapped = measurables.fastMap {
        DefaultIntrinsicMeasurable(it, IntrinsicMinMax.Min, IntrinsicWidthHeight.Width)
    }
    val constraints = Constraints(maxHeight = height)
    val layoutReceiver = IntrinsicsMeasureScope(this, layoutDirection)
    val layoutResult = layoutReceiver.measure(mapped, constraints)
    return layoutResult.width
}
  • 해당 함수는 자식요소의 목록과 height값을 받는다.
  • 각 자식요소는 DefaultIntrinsicMeasurable 인스턴스로 변환되고, 이 클래스는 Measurable 인터페이스를 구현하므로 measure() 구현체를 제공하게 된다.
  • 그리고 height값으로 최대 높이값이 정해지게 되고, 이 height에 대해 가능한 가장 작은 너비를 제공하는 FixedSizeIntrinsicPlaceable을 반환환다. 변환된 자식 요소는 IntrinsicMeasureScope의 인스턴스를 통해 측정된다고 한다.

커스텀 레이아웃 작성

@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: List<Placeable> = measurables.map { measurable ->
            measurable.measure(constraints)
        }
 
        layout(
            constraints.maxWidth,
            constraints.maxHeight
        ) {
            var yPos = 0
            var xPos = 0
            var maxY = 0
            placeables.forEach { placeable ->
                Log.d("Test", "placeable width : ${placeable.width}")
                Log.d("Test", "constraints maxWidth : ${constraints.maxWidth}")
 
                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.reasure() 구현체를 제공해야만 한다.
    • 이 함수는 MeasureResult 인터페이스의 객체를 반환
    • 이를 구현할 필요는 없다 대신 layout()을 호출해야만 한다.
      • 이 함수는 MeasureScope에 포함돼 있다.
  • 측정한 레이아웃 크기와 Placeable.PlacementScope의 확장 함수인 PlacementBlock을 전달한다.
    • 이는 부모 좌표계에서 자식을 배치하고자 placeRelative()와 같은 함수를 호출할 수 있다는 의미
  • 측정 정책은 콘텐츠나 자식 에이아웃을 List<Measureable>로 전달 받는다.

 

참조 : 젯팩 컴포즈로 개발하는 안드로이드 UI

반응형