compose로 레이아웃을 짜보자!

Hanseul Lee·2023년 3월 20일
0

Compose를 공부하자!

목록 보기
1/2
post-thumbnail

구현 할 디자인

modifier를 사용해 컴포저블을 수정할 수 있다.

  • 컴포저블의 크기, 레이아웃, 동작, 모양
  • 접근성 라벨과 같은 정보 추가
  • 사용자 입력 처리
  • 요소 클릭, 스크롤, 드래그, 확대 및 축소

검색창 구현 (TextField)

@Composable
fun SearchBar(
    modifier: Modifier = Modifier
) {
    TextField(
        value = "",
        onValueChange = {},
        leadingIcon = {
            Icon(
                imageVector = Icons.Default.Search,
                contentDescription = null
            )
        },
        colors = TextFieldDefaults.textFieldColors(
            backgroundColor = MaterialTheme.colors.surface
        ),
        placeholder = {
            Text(stringResource(R.string.placeholder_search))
        },
        modifier = modifier
            .fillMaxWidth()
            .heightIn(min = 56.dp)
    )
}

  • heightIn→ 최소 높이 지정
  • fillMaxWidth→ 상위 요소의 전체 공간을 차지하도록 함

원 이미지 구현 (Image)

@Composable
fun AlignYourBodyElement(
   modifier: Modifier = Modifier
) {
   Column(
       modifier = modifier
   ) {
       Image(
           painter = painterResource(R.drawable.ab1_inversions),
           contentDescription = null,
           contentScale = ContentScale.Crop,
           modifier = Modifier
               .size(88.dp)
               .clip(CircleShape)
       )
       Text(
           text = stringResource(R.string.ab1_inversions)
       )
   }
}
  • size→특정 크기에 맞게 컴포저블 조정
  • **clip**→컴포저블 모양을 조정
  • **contentScale**로 다음과 같이 컴포저블 조정이 가능하다.

이미지와 텍스트를 정렬하려면 align 수정자를 사용한다.

@Composable
fun AlignYourBodyElement(
   @DrawableRes drawable: Int,
   @StringRes text: Int,
   modifier: Modifier = Modifier
) {
   Column(
       modifier = modifier,
       horizontalAlignment = Alignment.CenterHorizontally
   ) {
       Image(
           painter = painterResource(R.drawable.ab1_inversions),
           contentDescription = null,
           contentScale = ContentScale.Crop,
           modifier = Modifier
               .size(88.dp)
               .clip(CircleShape)
       )
       Text(
           text = stringResource(R.string.ab1_inversions)
       )
   }
}
  • Column → 가로정렬
    • Start
    • CenterHorizontally
    • End
  • Row → 세로정렬
    • Top
    • CenterVertically
    • Bottom
  • Box → 가로세로정렬 결합
    • TopStart
    • TopCenter
    • TopEnd
    • CenterStart
    • Center
    • CenterEnd
    • BottomStart
    • BottomCenter
    • BottomEnd

추가로 이미지와 텍스트를 동적으로 구현하게 하면 다음과 같다.

@Composable
fun AlignYourBodyElement(
   @DrawableRes drawable: Int,
   @StringRes text: Int,
   modifier: Modifier = Modifier
) {
   Column(
       modifier = modifier,
       horizontalAlignment = Alignment.CenterHorizontally
   ) {
       Image(
           painter = painterResource(drawable),
           contentDescription = null,
           contentScale = ContentScale.Crop,
           modifier = Modifier
               .size(88.dp)
               .clip(CircleShape)
       )
       Text(
           text = stringResource(text),
           style = MaterialTheme.typography.h3,
           modifier = Modifier.paddingFromBaseline(
               top = 24.dp, bottom = 8.dp
           )
       )
   }
}

@Preview(showBackground = true, backgroundColor = 0xFFF0EAE2)
@Composable
fun AlignYourBodyElementPreview() {
   MySootheTheme {
       AlignYourBodyElement(
           text = R.string.ab1_inversions,
           drawable = R.drawable.ab1_inversions,
           modifier = Modifier.padding(8.dp)
       )
   }
}
  • 인수로 drawble과 text를 전달해 동적으로 구현되게 했다.
  • text의 style로 서체를 지정할 수 있게 했다.
  • 텍스트 요소의 기준 간격을 업데이트했다.

카드 구현 (Material Surface)

화면의 배경과 다른 색을 적용하고, 모서리는 둥글게 처리된 컨테이너를 만드려면 Surface를 활용하는 것이 좋다.

@Composable
fun FavoriteCollectionCard(
    modifier: Modifier = Modifier
) {
    Surface(
        shape = MaterialTheme.shapes.small,
        modifier = modifier
    ) {
        Row {
            Image(
                painter = painterResource(R.drawable.fc2_nature_meditations),
                contentDescription = null
            )
            Text(
                text = stringResource(R.string.fc2_nature_meditations)
            )
        }
    }
}
  • shape→material 디자인을 활용해 모서리를 둥글게 처리.

동적으로 작동하는 코드는 다음과 같다.

@Composable
fun FavoriteCollectionCard(
    @DrawableRes drawable: Int,
    @StringRes text: Int,
    modifier: Modifier = Modifier
) {
    Surface(
        shape = MaterialTheme.shapes.small,
        modifier = modifier
    ) {
        Row(
            verticalAlignment = Alignment.CenterVertically,
            modifier = Modifier.width(192.dp)
        ) {
            Image(
                painter = painterResource(drawable),
                contentDescription = null,
                contentScale = ContentScale.Crop,
                modifier = Modifier.size(56.dp)
            )
            Text(
                text = stringResource(text),
                style = MaterialTheme.typography.h3,
                modifier = Modifier.padding(horizontal = 16.dp)
            )
        }
    }
}

@Preview(showBackground = true, backgroundColor = 0xFFF0EAE2)
@Composable
fun FavoriteCollectionCardPreview() {
    MySootheTheme {
        FavoriteCollectionCard(
            text = R.string.fc2_nature_meditations,
            drawable = R.drawable.fc2_nature_meditations,
            modifier = Modifier.padding(8.dp)
        )
    }
}

컴포저블 행 배치 (LazyRow)

LazyRow를 활용해서 스크롤이 가능한 행을 구현할 수 있다. xml에서 리사이클러뷰 같이. 열로 스크롤하려면 LazyColumn을 쓰면 된다.

기본 구조는 다음과 같다.

import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items

@Composable
fun AlignYourBodyRow(
    modifier: Modifier = Modifier
) {
    LazyRow(
        modifier = modifier
    ) {
        items(alignYourBodyData) { item ->
            AlignYourBodyElement(item.drawable, item.text)
        }
    }
}

LazyRow의 하위 요소는 컴포저블이 아니다. 대신 컴포저블을 목록 항목으로 내보내는 item 및 items와 같은 메서드를 제공하는 Lazy 목록 DSL을 사용한다. 제공된 alignYourBodyData의 각 항목에서, 앞에서 구현한 AlignYourBodyElement 컴포저블을 내보낸다.

  • LazyRow의 배치 방식 https://developer.android.com/static/codelabs/jetpack-compose-layouts/img/c1e6c40e30136af2.gif?hl=ko
  • LazyColumn의 배치 방식 https://developer.android.com/static/codelabs/jetpack-compose-layouts/img/df69881d07b064d0.gif?hl=ko

위 배치 방식 외에도 Arrangement.spacedBy()를 사용해 컴포저블 사이 고정 공간 추가가 가능하다.

import androidx.compose.foundation.layout.Arrangement

@Composable
fun AlignYourBodyRow(
   modifier: Modifier = Modifier
) {
   LazyRow(
       horizontalArrangement = Arrangement.spacedBy(8.dp),
       modifier = modifier
   ) {
       items(alignYourBodyData) { item ->
           AlignYourBodyElement(item.drawable, item.text)
       }
   }
}

이 코드를 프리뷰에서 확인하면 다음처럼 처음과 뒤에 여백이 생겨 잘리게 된다. 이런 경우를 방지하는 padding을 넣기 위해 contentPadding을 사용한다.

https://developer.android.com/static/codelabs/jetpack-compose-layouts/img/6b3f390040e2b7fd.gif?hl=ko

@Composable
fun AlignYourBodyRow(
   modifier: Modifier = Modifier
) {
   LazyRow(
       horizontalArrangement = Arrangement.spacedBy(8.dp),
       contentPadding = PaddingValues(horizontal = 16.dp),
       modifier = modifier
   ) {
       items(alignYourBodyData) { item ->
           AlignYourBodyElement(item.drawable, item.text)
       }
   }
}

https://developer.android.com/static/codelabs/jetpack-compose-layouts/img/25089e1f3e5eab4e.gif?hl=ko

컴포저블 그리드 배치 (LazyHorizontalGrid )

https://developer.android.com/static/codelabs/jetpack-compose-layouts/img/4378867d758590ae.gif?hl=ko

lazyRow에서와 기본 구조는 매우 비슷하다.

import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
import androidx.compose.foundation.lazy.grid.items

@Composable
fun FavoriteCollectionsGrid(
   modifier: Modifier = Modifier
) {
   LazyHorizontalGrid(
       rows = GridCells.Fixed(2),
       modifier = modifier
   ) {
       items(favoriteCollectionsData) { item ->
           FavoriteCollectionCard(item.drawable, item.text)
       }
   }
}

미리보기처럼 확인할 수 있는 코드는 다음과 같다.

@Composable
fun FavoriteCollectionsGrid(
   modifier: Modifier = Modifier
) {
   LazyHorizontalGrid(
       rows = GridCells.Fixed(2),
       contentPadding = PaddingValues(horizontal = 16.dp),
       horizontalArrangement = Arrangement.spacedBy(8.dp),
       verticalArrangement = Arrangement.spacedBy(8.dp),
       modifier = modifier.height(120.dp)
   ) {
       items(favoriteCollectionsData) { item ->
           FavoriteCollectionCard(
               drawable = item.drawable,
               text = item.text,
               modifier = Modifier.height(56.dp)
           )
       }
   }
}

홈 섹션 구현 (슬롯 API)

섹션은 각각 제목과 슬롯이 있다. 따라서 섹션에 따라 달라지는 동적 콘텐츠인 것이다. 이렇게 유연하게 슬롯을 구현하려면 슬롯 API를 사용해야 한다.

  • 슬롯 기반 레이아웃은 개발자가 원하는 대로 채울 수 있게 UI에 빈 공간을 둔다.

가장 첫번째 섹션(align your body)를 구현하는 기본 구조는 다음과 같다.

@Composable
fun HomeSection(
   @StringRes title: Int,
   modifier: Modifier = Modifier,
   content: @Composable () -> Unit
) {
   Column(modifier) {
       Text(stringResource(title))
       content()
   }
}

@Preview(showBackground = true, backgroundColor = 0xFFF0EAE2)
@Composable
fun HomeSectionPreview() {
   MySootheTheme {
       HomeSection(R.string.align_your_body) {
           AlignYourBodyRow()
       }
   }
}

여기에 제목을 대문자로 하고, h2 폰트와 padding을 조절해보자.

@Composable
fun HomeSection(
   @StringRes title: Int,
   modifier: Modifier = Modifier,
   content: @Composable () -> Unit
) {
   Column(modifier) {
       Text(
           text = stringResource(title).uppercase(Locale.getDefault()),
           style = MaterialTheme.typography.h2,
           modifier = Modifier
               .paddingFromBaseline(top = 40.dp, bottom = 8.dp)
               .padding(horizontal = 16.dp)
       )
       content()
   }
}

홈 화면 스크롤

모든 구성요소를 개별적으로 만들었으니, 하나의 화면에 합쳐봐야 한다.

검색창 아래에 섹션을 각각 배치하면 되는데, 디자인과 동일하게 하려면 간격을 추가해야 한다. 이것은 Spacer로 해결한다. Spacer를 쓰면 Column 내에 더 많은 공간을 확보할 수 있는데, 이것과 padding을 같이 쓰면 양 옆이 잘리는 현상을 방지할 수 있다.

@Composable
fun HomeScreen(modifier: Modifier = Modifier) {
   Column(modifier) {
       Spacer(Modifier.height(16.dp))
       SearchBar(Modifier.padding(horizontal = 16.dp))
       HomeSection(title = R.string.align_your_body) {
           AlignYourBodyRow()
       }
       HomeSection(title = R.string.favorite_collections) {
           FavoriteCollectionsGrid()
       }
       Spacer(Modifier.height(16.dp))
   }
}

위 코드를 사용하면 순대로 나열이 된다. 하지만 스크롤은 되지 않는다. 그러면 기기의 크기가 작을 경우 데이터가 잘려 보일 수 있다.

앞선 방법과 같이 lazy 레이아웃으로 스크롤을 자동 추가할 수 있지만, 언제나 이걸 사용하는 건 아니다. lazy 레이아웃은 대개 다음과 같은 경우에 사용한다.

  • 목록에 포함된 요소가 많거나
  • 로드해야 할 데이터 세트가 많아서
  • 모든 항목을 동시에 내보내면 성능이 저하되고 앱이 느려지게 되는 경우

그러니까 목록에 요소가 많지 않은 지금 같은 경우에는 Column이나 Row를 사용하고, 스크롤을 수동으로 추가하는 게 좋다. 이때 **verticalScroll 또는 `horizontalScroll** modifier를 사용한다. 이때 remember를 사용한 것과 같이, 스크롤 상태 수정에 필요한 **ScrollState`**가 필요하다.

하지만 지금 경우에는 스크롤 상태를 수정할 필요가 없으므로 **rememberScrollState**를 사용하여 영구 **ScrollState** 인스턴스를 만들자.

import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll

@Composable
fun HomeScreen(modifier: Modifier = Modifier) {
   Column(
       modifier
           .verticalScroll(rememberScrollState())
           .padding(vertical = 16.dp)
   ) {
       Spacer(Modifier.height(16.dp))
       SearchBar(Modifier.padding(horizontal = 16.dp))
       HomeSection(title = R.string.align_your_body) {
           AlignYourBodyRow()
       }
       HomeSection(title = R.string.favorite_collections) {
           FavoriteCollectionsGrid()
       }
       Spacer(Modifier.height(16.dp))
   }
}

프리뷰 화면에서 화면 크기를 제한하려면 다음과 같이 heightDp 요소를 추가한다.

@Preview(showBackground = true, backgroundColor = 0xFFF0EAE2, heightDp = 180)
@Composable
fun ScreenContentPreview() {
   MySootheTheme { HomeScreen() }
}

Bottom Navigation

기본 구조는 다음과 같다.

import androidx.compose.material.BottomNavigation
import androidx.compose.material.BottomNavigationItem
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.Spa

@Composable
private fun SootheBottomNavigation(modifier: Modifier = Modifier) {
   BottomNavigation(modifier) {
       BottomNavigationItem(
           icon = {
               Icon(
                   imageVector = Icons.Default.Spa,
                   contentDescription = null
               )
           },
           label = {
               Text(stringResource(R.string.bottom_navigation_home))
           },
           selected = true,
           onClick = {}
       )
       BottomNavigationItem(
           icon = {
               Icon(
                   imageVector = Icons.Default.AccountCircle,
                   contentDescription = null
               )
           },
           label = {
               Text(stringResource(R.string.bottom_navigation_profile))
           },
           selected = false,
           onClick = {}
       )
   }
}

배경색을 설정하면 다음과 같다!

@Composable
private fun SootheBottomNavigation(modifier: Modifier = Modifier) {
   BottomNavigation(
       backgroundColor = MaterialTheme.colors.background,
       modifier = modifier
   ) {
       BottomNavigationItem(
           icon = {
               Icon(
                   imageVector = Icons.Default.Spa,
                   contentDescription = null
               )
           },
           label = {
               Text(stringResource(R.string.bottom_navigation_home))
           },
           selected = true,
           onClick = {}
       )
       BottomNavigationItem(
           icon = {
               Icon(
                   imageVector = Icons.Default.AccountCircle,
                   contentDescription = null
               )
           },
           label = {
               Text(stringResource(R.string.bottom_navigation_profile))
           },
           selected = false,
           onClick = {}
       )
   }
}

전체 화면 구현 (scaffold)

이제 전체 화면을 구현해야 하는데, 이때는 scaffold를 사용한다. scaffold최상위 수준의 컴포저블이다.

import androidx.compose.material.Scaffold

@Composable
fun MySootheApp() {
   MySootheTheme {
       Scaffold(
           bottomBar = { SootheBottomNavigation() }
       ) { padding ->
           HomeScreen(Modifier.padding(padding))
       }
   }
}

Android Codelab Basic Layout

0개의 댓글