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)
)
}
}
추가로 이미지와 텍스트를 동적으로 구현하게 하면 다음과 같다.
@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)
)
}
}
화면의 배경과 다른 색을 적용하고, 모서리는 둥글게 처리된 컨테이너를 만드려면 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
컴포저블을 내보낸다.
위 배치 방식 외에도 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
을 사용한다.
@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)
}
}
}
LazyHorizontalGrid
)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를 사용해야 한다.
가장 첫번째 섹션(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() }
}
기본 구조는 다음과 같다.
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))
}
}
}