ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Jetpack compose 레이아웃 - ①
    Android 2023. 5. 9. 17:05

     

    들어가기 전에

    Jetpack Compose 를 사용하면 앱의 UI를 쉽게 디자인하고 빌드할 수 있지만 Compose 에서 제공하는 구성 요소와 레이아웃 모델의 이해없이 작업하게되면 내가 만든 UI 코드가 어떻게 동작하는지 알 수 없는 상황에 놓이게된다.

    필자도 Jetpack Compose 샘플 앱을 만들어보면서 화면으로 확인되는 UI 코드가 이해가 되질 않아 원하는 화면을 만드는데 한계를 느끼고 내용을 정리하려고 한다.

     

     

    Compose 레이아웃 기본사항

    • 요소 구성
    • 요소 레이아웃
    • 요소 그림

     

    Compose는 위 기본사항을 통해 상태를 UI 요소로 변환한다.

     

     

    Compose 레이아웃 목표

    Jetpack Compose 의 레이아웃 시스템 구현에는 두 가지 목표가 있다.

    • 고성능
    • 손쉬운 맞춤 레이아웃 작성

     

    구성 가능한 함수 기본사항

    구성 가능한 함수는 UI의 일부를 설명하는 Unit을 내보내는 함수이다. 이 함수는 몇 가지 입력을 받아서 화면에 표시되는 내용을 생성한다.

     

    구성 가능한 함수는 여러 UI 요소를 내보낼 수 있다. 하지만 개발자가 UI 요소를 어떻게 정렬해야 하는지에 관한 가이드를 제공하지 않으면 Compose는 개발자가 원하지 않는 방식으로 요소를 정렬한다.

     

    // 여기서 구성 가능한 함수는 Text() 를 뜻한다.
    @Composable
    fun ArtistCard() {
        Text("Alfred Sisley")
        Text("3 minutes ago")
    }

     

    정렬 방식에 관한 가이드가 없다면 서로 겹치게 표시된다.

     

     

    표준 레이아웃 구성요소

    Column 을 사용하여 항목을 화면에 세로로 배치한다.

    @Composable
    fun ArtistCard() {
        Column {
            Text("Alfred Sisley")
            Text("3 minutes ago")
        }
    }

     

     

     

    Row 를 사용하여 항목을 화면에 가로로 배치할 수 있다. Column과 Row 모두 포함된 요소의 정렬 구성을 지원한다.

    @Composable
    fun ArtistCard(artist: Artist) {
        Row(verticalAlignment = Alignment.CenterVertically) {
            Image(/*...*/)
            Column {
                Text(artist.name)
                Text(artist.lastSeenOnline)
            }
        }
    }

     

     

     

    Box를 사용하여 요소를 다른 요소 위에 놓을 수도 있다.

    @Composable
    fun ArtistAvatar(artist: Artist) {
        Box {
            Image(/*...*/)
            Icon(/*...*/)
        }
    }

     

     

    흔히 아래와 같은 구성요소만 있으면 된다.

     

     

    ✅ 참고
    : "Compose는 중첩된 레이아웃을 효율적으로 처리하므로 중첩된 레이아웃을 사용하면 복잡한 UI를 디자인하는 데 도움이 된다"라고 안내하고 있어 시간이되면 어떻게 효율적으로 처리하는지 조사해봐야겠다.

     

    Row 내에서 하위 요소의 위치를 설정하려면 horizontalArrangementverticalAlignment 인수를 설정하면 된다.

    Column 의 경우 verticalArrangementhorizontalAlignment 인수를 설정하면 된다.

    @Composable
    fun ArtistCard(artist: Artist) {
        Row(
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.End
        ) {
            Image(/*...*/)
            Column { /*...*/ }
        }
    }

     

    레이아웃 모델

    레이아웃 모델에서 UI 트리는 단일 패스로 배치된다.

    각 노드는 먼저 자체 측정을 요청받고 하위 요소를 반복적으로 측정하여 크기 제약 조건을 트리 아래 하위 요소로 전달한다. 그러면 리프 노드(자식이 없는 노드, 말단 노드)가 크기 지정 및 배치되고 확인된 크기 및 배치 안내는 다시 트리 위로 전달된다.

     

    간단히 말해 상위 요소는 하위 요소보다 먼저 측정되지만 크기와 위치는 하위 요소 다음에 지정된다.

     

    예를 들어 SearchResult 함수를 살펴본다면,

    @Composable
    fun SearchResult(...) {
      Row(...) {
        Image(...)
        Column(...) {
          Text(...)
          Text(..)
        }
      }
    }

     

    위 함수는 다음과 같은 UI 트리를 생성한다.

    SearchResult
      Row
        Image
        Column
          Text
          Text

     

    SearchResult 다음 순서에 따라 UI 트리가 레이아웃된다.

     

    1. 루트 노드 Row 에 측정을 요청한다.
    2. 루트 노드 Row 는 첫 번째 하위 요소 Image 에 측정을 요청한다.
    3. Image 는 리프 노드(하위 요소가 없음)이므로 크기를 보고하고 배치 안내를 반환한다.
    4. 루트 노드 Row 는 두 번째 하위 요소 Column 에 측정을 요청한다.
    5. Column 노드는 첫 번째 Text 하위 요소에 측정을 요청한다.
    6. 첫 번째 Text 노드는 리프 노드이므로 크기를 보고하고 배치 안내를 반환한다.
    7. Column 노드는 두 번째 Text 하위 요소에 측정을 요청한다.
    8. 두 번째 Text 노드는 리프 노드이므로 크기를 보고하고 배치 안내를 반환한다.
    9. 이제 Column 노드가 하위 요소를 측정하여 크기를 지정하고 배치했으므로 자체 크기와 배치를 결정할 수 있다.
    10. 이제 루트 노드 Row 가 하위 요소를 측정하여 크기를 지정하고 배치했으므로 자체 크기와 배치를 결정할 수 있다.

     

     

    위 이미지에서 레이아웃 요소들이 어떤순서로 측정되고 배치되는지 직관적으로 파악할 수 있다.

     

    성능

    Compose는 하위 요소를 한 번만 측정하여 높은 성능을 발휘한다.

    단일 PATH 측정은 성능 측면에서 효율적이므로 Compose 가 깊은 UI 트리를 효율적으로 처리할 수 있다.

    요소가 하위 요소를 두 번 측정한 후 이 하위 요소가 각각의 자체 하위 요소를 두 번 측정하는 방식은 전체 UI를 배치하려는 한 번의 시도에서 많은 작업을 실행해야 하므로 앱의 성능을 유지하기가 어렵다.

     

    어떤 이유로든 레이아웃에 여러 측정이 필요하면 Compose 는 내장 측정 기능이라는 특수한 시스템을 제공한다.

    이 기능에 관한 자세한 내용은 여기를 확인하자.

     

    레이아웃에서 수정자 사용

    수정자(Modifier)를 사용하여 컴포저블을 장식하거나 강화할 수 있다. 수정자는 레이아웃을 맞춤설정 하는 데 필수적이다.

    @Composable
    fun ArtistCard(
        artist: Artist,
        onClick: () -> Unit
    ) {
        val padding = 16.dp
        Column(
            Modifier
                .clickable(onClick = onClick)
                .padding(padding)
                .fillMaxWidth()
        ) {
            Row(verticalAlignment = Alignment.CenterVertically) { /*...*/ }
            Spacer(Modifier.size(padding))
            Card(elevation = 4.dp) { /*...*/ }
        }
    }

     

     

    위의 코드에서 다양한 수정자 함수가 함께 사용된 것을 확인할 수 있다.

    • clickable: 컴포저블이 사용자 입력에 반응하도록 설정하고 물결 효과를 표시한다.
    • padding: 요소 주위에 공간을 배치한다.
    • fillMaxWidth: 컴포저블이 상위 요소로부터 부여받은 최대 너비를 채우도록 한다.
    • size(): 요소의 기본 너비 및 높이를 지정한다.

     

    스크롤 가능한 레이아웃

    Android 공식 문서에서는 많은 양의 목록을 표시하려면 스크롤 API 대신 LazyColumnLazyRow 를 사용하는걸 권장한다. LazyColumn 및 LazyRow 는 스크롤 기능을 제공하며 필요할 때만 항목을 구성해주기 때문에 스크롤 수정자보다 훨씬 더 효율적이다.

     

    스크롤 수정자

    verticalScrollhorizontalScroll 수정자는 콘텐츠의 경계가 최대 크기 제약 조건보다 클 때 사용자가 요소를 스크롤할 수 있는 가장 간단한 방법을 제공한다.

    @Composable
    fun ScrollBoxes() {
        Column(
            modifier = Modifier
                .background(Color.LightGray)
                .size(100.dp)
                .verticalScroll(rememberScrollState())
        ) {
            repeat(10) {
                Text("Item $it", modifier = Modifier.padding(2.dp))
            }
        }
    }

     

    ScrollState 를 사용하면 스크롤 위치를 변경하거나 현재 상태를 가져올 수 있다. 기본 매개변수를 사용하여 만들려면 rememberScrollState() 를 사용한다.

    @Composable
    private fun ScrollBoxesSmooth() {
    
        // Smoothly scroll 100px on first composition
        val state = rememberScrollState()
        LaunchedEffect(Unit) { state.animateScrollTo(100) }
    
        Column(
            modifier = Modifier
                .background(Color.LightGray)
                .size(100.dp)
                .padding(horizontal = 8.dp)
                .verticalScroll(state)
        ) {
            repeat(10) {
                Text("Item $it", modifier = Modifier.padding(2.dp))
            }
        }
    }

     

    스크롤 가능한 수정자

    scrollable 수정자는 스크롤 수정자와는 다르다. 즉, scrollable은 스크롤 동작을 감지하지만 콘텐츠를 *오프셋하지 않는다.

    이 수정자가 올바르게 작동하려면 ScrollableState 가 필요하다. ScrollableState 를 구성할 때는 각 스크롤 단계에서 픽셀 단위 delta 를 사용해(동작 입력, 부드러운 스크롤 또는 플링)호출할 consumeScrollDelta 함수를 제공해야 한다.

    이 함수는 scrollable 수정자가 있는 중첩 요소가 있는 경우 이벤트가 올바르게 전파되도록 하기 위해 사용된 스크롤 거리를 반환해야 한다.

     

    다음 코드는 동작을 감지하고 오프셋의 숫자 값을 표시하지만 아무 요소도 오프셋하지 않는다.

    @Composable
    fun ScrollableSample() {
        // actual composable state
        var offset by remember { mutableStateOf(0f) }
        Box(
            Modifier
                .size(150.dp)
                .scrollable(
                    orientation = Orientation.Vertical,
                    // Scrollable state: describes how to consume
                    // scrolling delta and update offset
                    state = rememberScrollableState { delta ->
                        offset += delta
                        delta
                    }
                )
                .background(Color.LightGray),
            contentAlignment = Alignment.Center
        ) {
            Text(offset.toString())
        }
    }

     

     

    *오프셋: 지정된 요소 사이의 거리를 나타내는 수

     

     

     

    중첩 스크롤

    Compose 는 여러 요소가 단일 스크롤 동작에 반응하는 중첩 스크롤을 지원한다.

     

    자동 중첩 스크롤

    단순한 중첩 스크롤은 개발자가 아무 조치를 안해도 된다.

    스크롤 작업을 시작하는 동작은 하위 요소에서 상위 요소로 자동 전파된다. 따라서 하위 요소가 더 이상 스크롤할 수 없는 경우 상위 요소에 의해 동작이 처리된다.

     

    자동 중첩 스크롤은 verticalScroll, horizontalScrool, scrollable, Lazy APITextFieldCompose 의 일부 구성요소 및 수정자에 의해 즉시 지원 및 제공된다. 즉, 사용자가 중첩된 구성요소의 내부 하위 요소를 스크롤하면 이전 수정자가 중첩된 스크롤을 지원하는 상위 요소에 스크롤 delta를 전파한다.

     

    아래 예시는 verticalScroll 수정자가 적용된 컨테이너 내부에 있는 또 다른 verticalScroll 수정자가 적용된 요소를 볼 수 있다.

    val gradient = Brush.verticalGradient(0f to Color.Gray, 1000f to Color.White)
    Box(
        modifier = Modifier
            .background(Color.LightGray)
            .verticalScroll(rememberScrollState())
            .padding(32.dp)
    ) {
        Column {
            repeat(6) {
                Box(
                    modifier = Modifier
                        .height(128.dp)
                        .verticalScroll(rememberScrollState())
                ) {
                    Text(
                        "Scroll here",
                        modifier = Modifier
                            .border(12.dp, Color.DarkGray)
                            .background(brush = gradient)
                            .padding(24.dp)
                            .height(150.dp)
                    )
                }
            }
        }
    }

     

     

    nestedScroll 수정자 사용

    여러 요소 간에 조정된 고급 스크롤을 만들어야 하는 경우 nestedScroll 수정자를 사용하면 중첩된 스크롤 계층 구조를 정의하여 더 유연하게 만들 수 있다. 일부 구성요소에는 중첩 스크롤 지원이 내장되어있다. 그러나 Box 또는 Column 과 같이 자동으로 스크롤되지 않는 컴포저블의 경우 스크롤 델타가 중첩된 스크롤 시스템에서 전파되지 않고 델타가 NestedScrollConnection 또는 상위 구성요소에 도달하지 않는다. 이 문제를 해결하려면 nestedScroll 을 사용하여 맞춤 구성요소 등 다른 구성요소에 이러한 지원을 부여할 수 있다.

     

    반응형 레이아웃

    레이아웃은 여러 화면 방향과 폼 팩터 크기를 고려하여 디자인해야 한다. Compose에서 제공하는 즉시 사용 가능한 몇 가지 메커니즘으로 컴포저블 레이아웃을 다양한 화면 구성에 따라 쉽게 조정할 수 있다.

     

    제약 조건

    상위 요소의 제약 조건을 파악하고 그에 따라 레이아웃을 디자인하려면 BoxWithConstraints 를 사용하면 된다. 측정 제약 조건은 콘텐츠 람다의 범위에서 확인할 수 있다. 이 측정 제약 조건을 사용하여 다양한 화면 구성에 따라 다양한 레이아웃을 구성할 수 있다. 여기를 확인하자.

    @Composable
    fun WithConstraintsComposable() {
        BoxWithConstraints {
            Text("My minHeight is $minHeight while my maxWidth is $maxWidth")
        }
    }

     

    슬롯 기반 레이아웃

    Compose 는 UI를 쉽게 빌드할 수 있도록 Material 디자인 및 Android Studio에서 Compose 프로젝트를 만들 때 포함되는 androidx.compose.material:material 종속 항목을 기반으로 한 다양한 컴포저블을 제공한다. Drawer, FloatingActionButton 및 TopAppBar 와 같은 요소가 모두 제공된다.

     

    Material 구성요소는 Compose 가 컴포저블 위에 맞춤설정 레이어를 배치하기 위해 도입한 패턴인 슬록 API 를 많이 사용한다. 이 접근 방식을 사용하면 하위 요소의 모든 구성 매개변수를 노출하지 않고 자체적으로 하위 요소를 구성할 수 있으므로 구성요소의 유연성이 향상된다.

    슬롯은 개발자가 원하는 대로 채울 수 있도록 UI에 빈 공간을 남겨둔다.

    예를 들어, TopAppBar 에서 맞춤설정할 수 있는 슬롯을 보자면,

     

     

    컴포저블은 일반적으로 content 컴포저블 람다(content: @Composable () → Unit) 를 사용한다.

    슬롯 API는 특정 용도를 위해 여러 content 매개변수를 노출한다. 예를 들어 TopAppBar를 사용하면 title, navigationIcon 및 actions 의 콘텐츠를 제공할 수 있다.

     

    또 다른 예를 들어, Scaffold 를 사용하면 기본 Material 디자인 레이아웃 구조로 UI를 구현할 수 있다. Scaffold 는 TopAppBar, BottomAppBar, FloatingActionButton, Drawer 등 가장 일반적인 상위 Material 구성요소용 슬롯을 제공한다.

    Scaffold 를 사용하면 이러한 구성요소가 적절하게 배치되어 함께 올바르게 작동하는지 쉽게 확인할 수 있다.

     

     

    @Composable
    fun HomeScreen(/*...*/) {
        Scaffold(
            drawerContent = { /*...*/ },
            topBar = { /*...*/ },
            content = { /*...*/ }
        )
    }

     

    'Android' 카테고리의 다른 글

    Android Studio 멀티 모듈과 Gradle Wrapper  (0) 2023.05.15
    Jetpack compose 레이아웃 - ②  (0) 2023.05.12
    Jetpack compose State 심화  (0) 2023.05.08
    Jetpack compose Navigation  (0) 2023.05.04
    Jetpack compose Scaffold 와 Snackbar  (0) 2023.05.03
Designed by Tistory.