본문 바로가기

안드로이드 Android

안드로이드 개발 (14) Layout in Compose 2편

1편 정리

https://gift123.tistory.com/41

 

안드로이드 개발 (13) Layout in Compose 1편

지금까지 Compose에 대해 composable 라이프사이클, Compose 내부흐름 , Composition, recompostion, Sdie-effects 활용방법에 대한 원론 방법에 알았다면 이제 실질적으로 Compose로 Layout을 어떻게 구성하는지..

gift123.tistory.com

 

1편에 이어서 2편도 정리해가보도록 하겠습니다.

 

1편 요약

- 기본 레이아웃은 Colum , Row, Box 3가지를 주로 사용

- Modifiers 로 Composable의 설정이나 여러가지를 추가적으로 작업할 수 있음

- Modifiers는 순서가 매우 중요

- 가중치, 제약조건 사용으로 반응형 레이아웃을 만들 수 있음

- 슬롯기반 API로 쉽게 머티리얼 디자인을 구성하거나 기본적으로 지원해주는 UI를 빠르게 만들어 낼 수 있음

(대표적으로 Scaffold)

 

1. ConstraintLayout

- ConstraintLayout은 더 복잡한 정렬 요구사항이 있는 더 큰 레이아웃을 구현할 때 유용함

- 간단한 레이아웃을 만드는 경우 Column & Row & Box 사용하기를 권장함

- Xml에서 플랫 View 계층 구조를 갖는 콘스트레이아웃의 특징은 Compose는 이미 깊은 레이아웃 계층 구조를 효율적으로 처리할 수 있기 때문에 Compose에서 콘스트레이아웃의 장점이 아님

 

Compose의 ConstraintLayout 사용방법 

 

(1) 참조는 createRefs() ,createRefFor()를 사용하여 생성 ConstraintLayout의 각 Composable에는 연결된 참조가 필수

 

(2) 제약 조건은 constrainAs() Modifiers를 사용하여 제공함 이 수정자는 참조를 매개변수로 사용하고 람다에 제약 조건을 지정할 수 있게함.

 

(3) 제약 조건은 linkTo() 또는 다른 메서드를 사용하여 지정함

 

(4) parent는 ConstraintLayout 컴포저블 자체에 대한 제약 조건을 지정하는 데 사용할 수 있는 기존 참조임

 

예제 

 @Composable
    fun ConstraintLayoutContent() {
        ConstraintLayout {

            // 1. 참조 생성
            val (button, text) = createRefs()

            Button(onClick = {/*아무말*/ },
                //2. constrainAs Modifier를 사용해서 제약조건을 걸 환경을 제공 시킴
                modifier = Modifier.constrainAs(button) {
                    //3.linkTo 함수로 실제 제약조건을 구체적으로 걸어줌
                    top.linkTo(parent.top, margin = 16.dp)
                }) {
                Text("Button")
            }
                //위의 2~3과 같은 내용
            Text("Text",modifier = Modifier.constrainAs(text){
                top.linkTo(button.bottom, margin = 8.dp)
            })
        }
    }

- 위 코드 주석을 살펴보시면 상세한 순서를 확인할 수 있음. 

- 위 예제는 제약 조건은 적용되는 Composable 의 수정자와 함께 인라인으로 지정

 

결과

 

분리된 API

기본적으로 제약 조건은 적용되는 Composable 의 수정자와 함께 인라인으로 지정함 

하지만 제약 조건이 적용되는 레이아웃에서 제약 조건을 분리하는 것이 더 좋은 상황이 있음

 

예를 들어 화면 구성을 기반으로 제약 조건을 변경하거나

두 제약 조건 세트 사이에 애니메이션을 적용하는 경우 등등 존재함

 

이 같은 경우에는 ConstraintLayout을 서로 다른 방식으로 사용할 수 있습니다.

 

(1) ConstraintSet을 파라미터로 ConstraintLayout에 전달함

(2) layoutId Modifier를 사용하여 ConstraintSet에 생성된 참조를 Composable에 할당함

//ConstraintSet을 리턴하는 함수  
    private fun decoupledConstraints(margin: Dp): ConstraintSet {
        return ConstraintSet {
            val button = createRefFor("button")
            val text = createRefFor("text")

            constrain(button) {
                top.linkTo(parent.top, margin)
            }
            constrain(text) {
                top.linkTo(button.bottom, margin)
            }
        }
    }
    @Preview
    @Composable
    fun DecoupledConstraintLayout() {
        //BoxWithConstraints 를 사용해서 화면의 minWidth를 참고 
        BoxWithConstraints {
            val constrains = if (minWidth < 600.dp)
                decoupledConstraints(16.dp)
            else
                decoupledConstraints(32.dp)

            ConstraintLayout(constrains) {
                Button(
                    onClick = { /*아무말*/ },
                    modifier = Modifier.layoutId("button")
                ) {
                    Text("button")
                }

                Text("Text",Modifier.layoutId("text") )
            }


        }
    }

 

-위 예제대로 라면 제약 조건을 변경해야 할 때 다른 ConstraintSet을 전달하거나 수정하면 됨 

 

2. Custom Layout

Compose 에서 UI를 그리는 순서

 

(1) Compose에서 UI 요소는 호출될 때 UI 요소를 내보내는 Composable 함수로 표시

(2) 화면에 렌더링되는 UI 트리에 추가됨

    (각 UI 요소에는 하나의 상위 요소와 여러 개의 하위 요소가 있을 수 있음)

(3) 각 요소는 (x, y) 위치로 지정된 상위 요소 내에 배치되며 width  height로 크기가 지정됨

 

설명

상위 요소는 하위 요소의 제약조건을 정의합니다.

제약 조건은 요소의 최소/최대 width 및 height를 제한 합니다.

요소에 하위 요소가 있을때, 각 하위요소를 측정해서 요소의 크기를 결정할 수 있습니다. 

 

Compose UI는 다중 패스 측정을 허용하지 않습니다.

(단일 패스 측정은 성능 측면에서 효율적이므로 Compose가 깊은 UI 트리를 효율적으로 처리할 수 있음)

 

즉, 레이아웃 요소가 다른 측정 구성을 시도하기 위해 하위 요소를 두 번 이상 측정할 수 없다는걸 뜻합니다.

(단일 패스 측정 하기전에 정보가 필요하다면 맨 아래에 설명할 내장 기능 측정을 참고하면 됩니다.)

 

(1) Layout Modifier 사용

fun Modifier.customLayoutModifier(...) =
    this.layout { measurable, constraints ->
        ...
    })

- layout modifier 를 사용하여 요소가 측정되고 배치되는 방식을 수정할 수 있음. 

- Layout은 람다

- layout modifier 는 호출하는 Composable만 변경함

- 파라미터에 측정할 수 있는 요소(measurable로 전달됨) 및 이 Composable의 수신된 제약 조건(constraints로 전달됨)이 포함

 

layout Modifier를 커스텀해서 

화면에 Text를 표시하고 텍스트 첫 줄의 상단에서 기준선까지의 거리를 제어해 보겠습니다. 

 

예제

fun Modifier.firstBaselineToTop(
    firstBaselineToTop: Dp
) = layout { measurable, constraints ->
    val placeable = measurable.measure(constraints)


    check(placeable[FirstBaseline] != AlignmentLine.Unspecified)
    val firstBaseline = placeable[FirstBaseline]

    val placeableY = firstBaselineToTop.roundToPx() - firstBaseline
    val height = placeable.height + placeableY
    layout(placeable.width, height) {
   
        placeable.placeRelative(0, placeableY)
    }
}

예제설명

(1) Modifier의 확장함수 

 

(2) measurable.measure(constraints)를 호출하여 측정 가능한 매개변수로 표시되는 Text 를 측정

 

(3) layout(width, height) Composable의 크기를 지정합니다. 

 

(4) placeable.place(x, y)로 화면에 래핑된 요소를 배치함 

(y 위치는 텍스트의 첫 번째 기준선 위치인 상단 패딩에 상응함)

 

실제 사용

    @Preview
    @Composable
    fun TextWithPaddingToBaselinePreview() {
            Text("Hi there!", Modifier.firstBaselineToTop(32.dp))
    }
    @Preview
    @Composable
    fun TextWithNormalPaddingPreview() {
            Text("Hi there!", Modifier.padding(top = 32.dp))
    }

 

결과

 

(2) Layout Composable

@Composable
fun MyBasicColumn(
    modifier: Modifier = Modifier,
    content: @Composable() () -> Unit
) {
    Layout(
        modifier = modifier,
        children = content
    ) { measurables, constraints ->
        // measure and position children given constraints logic here
    }
}

- 여러 Composable을 측정하고 배치하려면 Layout Composable를 사용해야함

- 하위 요소를 수동으로 측정하고 배치할 수 있음.

( Column  Row와 같은 모든 상위 수준 레이아웃은 Layout 컴포저블을 사용하여 빌드됩니다.)

 

참고: 기존 View System에서 맞춤 레이아웃을 생성하려면 ViewGroup을 확장하고 측정 및 레이아웃 함수를 구현해야하는것과 달리 Compose에서는 Layout Composable을 사용하여 함수를 작성하기만 하면 됩니다.

 

 

예제

  @Composable
    fun MyBasicColumn(
        modifier: Modifier = Modifier,
        content: @Composable() () -> Unit
    ) {
        Layout(content = content, modifier = modifier) { measurables, constraints ->
            val placeable = measurables.map { measurable ->
                measurable.measure(constraints)
            }
            layout(constraints.maxWidth, constraints.maxHeight) {
                var y = 0
                placeable.forEach {
                    it.placeRelative(0, y)
                    y += it.height
                }
            }
        }
    }

예제 설명

compose layout의 기본 중 하나인 Colum을 직접 만든 코드 입니다. 

하위 Composable은 (minHeight 제약 조건 없이) Layout 제약 조건에 의해 이전 Composable의 y을 기반으로 배치됩니다.

 

만든 커스텀 레이아웃 사용예시

 

@Composable
fun CallingComposable(modifier: Modifier = Modifier) {
    MyBasicColumn(modifier.padding(8.dp)) {
        Text("MyBasicColumn")
        Text("places items")
        Text("vertically.")
        Text("We've done it by hand!")
    }
}

3. 레이아웃 방향

- LocalLayoutDirection Composition 로컬을 변경하여 Composable의 레이아웃 방향을 변경함

- Composable 을 수동으로 배치하는 경우 LayoutDirection은 layout 수정자 또는 Layout 컴포저블의 LayoutScope에 포함되어 있음

- layoutDirection을 사용할 때는 place 를 사용하여 컴포저블을 배치함

- placeRelative 메서드와 달리 place는 레이아웃 방향에 따라 변경되지 않음

 

4. 내장 측정

- Compose는 한번만 측정하기 때문에 측정하기 전에 하위요소에 관한 정보가 필요한 경우에 내장측정을 사용함

  (하위 요소를 두 번 측정하면 런타임 예외가 발생합니다.)

- 하위 요소가 실제로 측정되기 전에 하위 요소를 쿼리할 수 있음

- Composable에 intrinsicWidth 또는 intrinsicHeight를 요청할 수 있음 

  -> (min|max)IntrinsicWidth, (min|max)IntrinsicHeight

     -> 이 높이/너비에서 콘텐츠를 적절하게 그릴 수 있는 최소/최대 너비는 무엇인가?

 

사용예시

구분선으로 구분된 두 텍스트를 표시하는 Composable 을 만든다고 가정

 

예제

  @Composable
    fun TwoTexts(modifier: Modifier = Modifier, text1: String, text2: String) {
        Row(modifier = modifier
            .height(intrinsicSize = IntrinsicSize.Min)
            .padding(2.dp)) {
            Text(
                modifier = Modifier
                    .weight(1f)
                    .padding(start = 4.dp)
                    .wrapContentWidth(Alignment.Start),
                text = text1
            )
            Divider(
                color = Color.Black, modifier = Modifier
                    .fillMaxHeight()
                    .width(1.dp)
            )
            Text(
                modifier = Modifier
                    .weight(1f)
                    .padding(end = 4.dp)
                    .wrapContentWidth(Alignment.End),
                text = text2
            )

        }
    }

 

예제 설명

- height(IntrinsicSize.Min)는 하위 요소의 크기를 고유한 최소 높이로 강제 지정

  (이 기능은 반복적이므로 Row 및 하위 minIntrinsicHeight를 쿼리합니다.)

 

Row 컴포저블의 minIntrinsicHeight가 하위 요소의 최대 minIntrinsicHeight입니다.

제약 조건이 지정되지 않은 경우 공간을 차지하지 않으므로 Divider element'sminIntrinsicHeight는 0이고 Text minIntrinsicHeight는 특정 width가 지정된 경우 텍스트의 높이입니다.

 

따라서 Row 요소의 height 제약 조건은 Text의 최대 minIntrinsicHeight입니다.

그런 다음 Divider가 height를 Row가 지정한 height 제약 조건으로 확장합니다.

 

결과

 

5.최종정리

-컴포즈에서 컨스트레이아웃 사용가능

-컴포즈에서 컨스트레이아웃을 사용하려면 연결된 참조를 만들고 제약 연결해야함

-컴포즈에서 컨스르레이아웃의 제약조건을 constaintSet으로 변경하거나 쉽게 갈아끼울 수 있음 

-커스텀 레이아웃은 Layout Modifier 를 사용하거나 (한개의 Composable만 적용) 

Layout Composable(여러가지 Composable을 적용할 수 있음) 를 사용하면 됨

-컴포즈는 UI의 요소를 측정할때 단 한번만 측정을 함 

-내장 측정은 UI 요소를 측정하기 전에 미리 쿼리해서 사용할 수 있음

 

이상 Compose Layout in compose 문서 정리 마무리 입니다.

알면 알아갈수록 선언형 프로그래밍의 간단한 사용방식과 직관적인 문법에 감탄이 나옵니다. 얼른 스테이블이 되길 기다립니다.

 

 

 

이전 Compose 정리

https://gift123.tistory.com/33?category=967702 

 

안드로이드 개발 (8) Compose 이해 정리

이번 포스팅부터 Compose에 대해 차근히 파헤쳐 가보겠습니다. Android Compose 공식 문서를 보면서 정리한 내용들 입니다. https://developer.android.com/jetpack/compose/mental-model?hl=en Compose 이해  |..

gift123.tistory.com

https://gift123.tistory.com/34?category=967702 

 

안드로이드 개발 (9) Compose 상태 관리

jetpack compose 에 한창 포스팅 중입니다. https://gift123.tistory.com/33 안드로이드 개발 (8) Compose 이해 정리 이번 포스팅부터 Compose에 대해 차근히 파헤쳐 가보겠습니다. Android Compose 공식 문서를..

gift123.tistory.com

 

https://gift123.tistory.com/38?category=967702 

 

안드로이드 개발 (10) Compose Composable Lifecycle

안녕하세요 Loner 입니다. 오늘은 Compose의 컴포저블 라이프사이클 공부를 정리해 봤습니다. 아래 문서를 정리한 내용입니다. https://developer.android.com/jetpack/compose/lifecycle?hl=en 컴포저블 수명 주..

gift123.tistory.com

https://gift123.tistory.com/39?category=967702 

 

안드로이드 개발 (11) Compose Side-effects

https://developer.android.com/jetpack/compose/side-effects#state-effect-use-cases Compose의 부수 효과  | Jetpack Compose  | Android Developers 컴포저블에는 부수 효과가 없어야 합니다. 하지만 앱의..

gift123.tistory.com

https://gift123.tistory.com/41?category=967702 

 

안드로이드 개발 (13) Layout in Compose 1편

지금까지 Compose에 대해 composable 라이프사이클, Compose 내부흐름 , Composition, recompostion, Sdie-effects 활용방법에 대한 원론 방법에 알았다면 이제 실질적으로 Compose로 Layout을 어떻게 구성하는지..

gift123.tistory.com