[Compose] compose 레이아웃 이모저모
해당 게시글은 compose의 기본 레이아웃 코드랩을 진행하며 그 과정과 기억해둘 내용을 코드 기반으로 정리한 게시글입니다.
🙋♀️ 학습 내용
코드랩을 통해 학습할 내용은 크게 다음과 같다.
- Modifier
- 크기, 레이아웃, 동작, 모양변경
- 접근성 라벨과 같은 정보 추가
- 사용자 입력처리
- 클릭, 스크롤, 드래그 등 높은 수준의 상호작용
- Layout
- 레이아웃 정렬옵션
- Alignment의 종류, 설정하는 법
- Arrangement를 사용하여 배치하는 법
- Slot API
- 사용자 정의 레이어를 가져오기 위한 패턴
- Material 요소
- 미리 제공되는 요소들로 인해 컴포즈의 생산성 향상
- Surface, BottomNavigation, Scaffold
그러다면 본격적으로 코드랩에 들어가기 앞서 우선 해당 코드랩을 통해 구현할 디자인을 살펴보자.
여기서 디자인을 진행할 부분은 크게 다음과 같이 세가지 부분이다.
- 검색창
- 신체의 조화 섹션
- 즐겨찾는 컬렉션 섹션
또한, 컴포저블을 잘 활용하기 위해서 재사용되는 하위 구성요소를 파악해보면 다음과 같이 두가지이다.
- 신체의 조화 섹션에서의 요소
- 즐겨찾기 컬렉션 섹션에서의 요소
그렇다면 이제 각 요소들에 해당하는 composable을 구성해보자!!
🙋♀️ 검색창 - Modifier 학습
@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)
)
}
TextField를 통해서 검색창을 구현한다.leadingIcon을 통해 내부 아이콘을 설정한다. TextField의 의미가 설명되어 있으므로 해당 아이콘의 콘텐츠 설명은 필요하지 않는다.heightIn을 통해서 최소 높이만 설정한다. 사용자가 설정한 글꼴의 크기에 따라 텍스트 필드 크기가 변하게 된다.
- SearchBar 컴포저블의
Modifier를TextField에 전달하여 메서드의 호출자가 컴포저블의 디자인과 분위기를 수정할 수 있도록 하여 유연성을 높이고 재사용을 가능하게 한다.
🙋♀️ 신체의 조화 섹션 요소 - 정렬 학습
@Composable
fun AlignYourBodyElement(
@DrawableRes drawable: Int, // 컴포저블을 동적으로 만들기 위해 매개변수를 사용한다.
@StringRes text: Int,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally // 상위 컨테이너 내부에서 컴포저블 정렬, Column이므로 하위 요소 가로 정렬 방법 지정
) {
Image(
painter = painterResource(drawable),
contentDescription = null,
contentScale = ContentScale.Crop, // 이미지를 조정(fit, fillbounds, crop)
modifier = Modifier
.size(88.dp)
.clip(CircleShape) // clip을 통해 컨테이너의 모양을 지정
)
Text(
text = stringResource(text),
style = MaterialTheme.typography.h3,
modifier = Modifier.paddingFromBaseline(
top = 24.dp, bottom = 8.dp
) // 문자가 놓여 있는 기준선을 기준으로 상단, 하단 간격 설정
)
}
}
- 각 요소의 이미지는 장식용으로
contentDescriptino을 null로 설정한다.- 이미지 아래 텍스트만으로 의미가 충분히 전달되기 때문이다.
clip수정자를 통해 컴포저블의 모양을 조정한다.contentScale매개변수를 사용해 이미의 크기를 조정한다.- Crop을 통해 컨테이너 내부에 맞게 이미지를 자르게 된다.
- 상위 컨테이너 내부에서 컴포저블을 정렬하려면 상위 컨테이너의
alignment를 설정한다.Column의 경우 가로 정렬 방법을 지정,Row의 경우 세로 정렬 방법을 지정한다.- 상위 컨테이너인
Column내부의 컴포저블을 가로로 정렬하기 위해horizontalAlignment를 사용한다.
🙋♀️ 즐겨찾는 컬렉션 카드
@Composable
fun FavoriteCollectionCard(
@DrawableRes drawable: Int, // 동적으로 요소를 생성하기 위해 매개변수를 사용한다.
@StringRes text: Int,
modifier: Modifier = Modifier
) {
Surface(// 모서리가 둥근 컨테이너를 처리하기 위해 Surface를 사용한다.
shape = MaterialTheme.shapes.small,
modifier = modifier
) {
Row(
verticalAlignment = Alignment.CenterVertically, // 상위 컨테이너 내부에서 하위 컴포저블 정렬, Row이므로 하위 세로 정렬 방법 지정
modifier = Modifier.width(192.dp) // Row의 너비를 설정한다.
) {
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)
)
}
}
}
🙋♀️ 신체의 조화 section 배치 - LazyRow 적용
@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)
}
}
}
- 목록 속 하위 컴포저블 배치 방식
- Row
- Column
- 위 배치 방식을 사용하지 않고
Arrangement.spaceBy()를 통해 하위 컴포저블 사이 고정된 공간을 추가한다.
🙋♀️ 즐겨찾는 컬렉션 섹션 - Lazy Grid 적용
LazyRow속 두개의 요소를Column을 통해 배치하며 해당 섹션을 구현할 수 있다.
🙃 여기서는 항목-그리드 요소 매핑을 효과적으로 진행할 수 있는 LazyHorizontalGrid를 사용할 것이다.
@Composable
fun FavoriteCollectionsGrid(
modifier: Modifier = Modifier
) {
LazyHorizontalGrid( // 항목-그리드 요소 매핑을 효과적으로 진행할 수 있다.
rows = GridCells.Fixed(2), // 행을 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)
)
}
}
}
- 여기서도
LazyHorizontalGrid자체에 좌우 패딩을 추가하면 항목이 잘리는 현상이 일어난다.contentPadding을 통해 패딩을 유지하며 경계 안에서 항목이 잘리지 않도록 한다.
🙋♀️ 홈 화면 구성 - 슬롯 API 적용
위 화면을 구성하기 전에 우리는 하나의 패턴을 확인할 수 있다. 제목-섹션의 패턴이 반복되며 화면을 구성하고, 각 섹션마다 다른 콘텐츠가 표시된다.
🙃 여기서 우리는 슬롯을 기반으로 레이아웃을 구성할 수 있다.
슬롯 기반 레이아웃? => 사용자 정의 레이어를 가져오기 위한 패턴 개발자가 원하는 대로 채울 수 있도록 UI에 빈 공간을 남겨 둔다. 슬롯 기반 레이아웃을 사용하여 유연한 레이아웃을 만들 수 있고, 코틀린의 고차함수처럼 컴포저블 함수를 구성할 수 있다.
@Composable
fun HomeSection(
@StringRes title: Int,
modifier: Modifier = Modifier,
content: @Composable () -> Unit // 컴포저블의 슬롯으로 content 매개변수를 통해 원하는 섹션을 받아온다.
) {
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() // 빈 공간을 원하는 섹션으로 채워넣을 수 있다.
}
}
- 특정 컴포저블을 넘길 때는 일반적으로
content컴포저블 람다(content: @Composable () -> Unit)를 사용한다.
🙋♀️ 홈 화면 - 목록이 아닌 항목 스크롤시키기
디자인을 바탕으로 화면 구성 방식을 한 번 정리해보자!
Row를 활용해 홈 화면을 구성한다.- 일반적으로 목록에 포함된 요소가 많거나 로드해야 할 데이터 세트가 많을 경우
Lazy 레이아웃을 사용한다.- 모든 항목을 동시에 내보내면 성능이 저하되고 앱이 느려지기 때문이다.
- 일반적으로 목록에 포함된 요소가 많거나 로드해야 할 데이터 세트가 많을 경우
Row를 통해 화면을 구성한 후, 스크롤 동작을 추가한다.- Lazy 레이아웃은 자동으로 스크롤 동작이 추가된다.
- 목록의 개수가 많지 않은 경우에는 간단하게
Column또는Row를 사용하고 스크롤 동작을 추가하면 된다.
@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))
}
}
Spacer를 사용하지 않고 Column에 padding을 설정하게 되면 요소가 잘리는 현상이 발생한다. 따라서 Spacer를 사용해 Column 내부에 공간을 둔다.Lazy 레이아웃에서는 이러한 현상을 해결하기 위해서contentPadding을 사용했다.
🙃 여기서 스크롤 동작을 추가해야 한다. 이를 위해 우리는 verticalScroll/horizontalScroll 수정자를 사용한다.
- 스크롤의 현재 상태를 포함하며 외부에서 스크롤 상태를 수정하는 데 사용되는
ScrollState가 필요하다. - 여기서는 스크롤 상태를 수정할 필요가 없으므로
rememberScrollState를 사용하여 영구 ScrollState 인스턴스를 만들면 된다.
@Composable
fun HomeScreen(modifier: Modifier = Modifier) {
Column(
modifier
.verticalScroll(rememberScrollState())
.padding(vertical = 16.dp)
) {
SearchBar(Modifier.padding(horizontal = 16.dp))
HomeSection(title = R.string.align_your_body) {
AlignYourBodyRow()
}
HomeSection(title = R.string.favorite_collections) {
FavoriteCollectionsGrid()
}
}
}
- 스크롤 동작을 추가하면서 Column에 padding을 넣어도 요소가 잘리지 않게 된다.
🙋♀️ 하단 탐색바 - ButtonNavigation 컴포저블 사용
하단 네비게이션 바는 Compose Material 라이브러리의 BottomNavigation 컴포저블을 사용하면 된다. 컴포저블 내에 여러 개의 ButtonNavigationItem 요소를 추가할 수 있다.
@Composable
private fun SootheBottomNavigation(modifier: Modifier = Modifier) {
BottomNavigation( // 하단 탐색바를 쉽게 구현할 수 있는 Material 라이브러리의 BottomNavigation 컴포저블 사용
backgroundColor = MaterialTheme.colors.background,
modifier = modifier
) {
// 하나의 BottomNavigationItem이 탐색바의 하나의 요소가 된다.
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이다. 이를 사용할 경우, 하단 메뉴에 대한 슬롯을 설정할 수 있다.
@Composable
fun MySootheApp() {
MySootheTheme {
Scaffold(
bottomBar = { SootheBottomNavigation() }
) { padding ->
HomeScreen(Modifier.padding(padding))
}
}
}
- 최상위 수준의 컴포저블 즉, 전체 레이아웃을 나타내는 컴포저블이므로 Material 테마를 적용한다.
Scaffold컴포저블을 사용한다.- 하단 메뉴 슬롯에 우리가 제작한 SootheBottomNavigation을 배치한다.
- content 슬롯에 전체 화면을 나타내는 HomeScreen을 배치한다.
🙂 이렇게 해서 Compose의 기본 레이아웃 코드랩을 마무리했다! 하나부터 열까지 다 새로운 내용이지만 코드랩을 따라하는 과정에서 컴포즈를 어떻게 활용해야 할지 감을 잡을 수 있었던 것 같다! 코드랩 안에서 적용되는 개념들이 추후 많이 사용되는 개념일 것 같아 코드 기반으로 정리했더니 설명보다는 코드가 많은 비중을 차지한 것 같다..! 추후 컴포즈를 사용하게 된다면 해당 코드랩에 대한 내용을 많이 참고할 것 같다!!
댓글남기기