이번 공부 노트는 안드로이드 컴포즈에서 바텀 시트를 예제를 통해 구현해 보도록 하겠다.
구현하는 방법은 ModalBottomSheetLayout, ModalBottomSheet 크게 두 가지가 있다.
하나하나씩 알아보자!
※ 본 예제는 MaterialDesign3 기준으로 작성했다.
해당 버튼을 누르면 과일 리스트가 바텀시트로 나오는 예제이다.
# ModalBottomSheetLayout
이 방식은 구버전 API로 콘텐츠와 바텀 시트를 함께 포함하는 레이아웃 컴포넌트이다.
일반적으로 화면 전체를 감싸는 방식으로 사용
따라서 콘텐츠와 바텀 시트를 함께 포함하고 싶다면 사용 추천!
ModalBottomSheetLayout 내부를 살펴보자
@Composable
@ExperimentalMaterialApi
fun ModalBottomSheetLayout(
sheetContent: @Composable ColumnScope.() -> Unit,
modifier: Modifier = Modifier,
sheetState: ModalBottomSheetState =
rememberModalBottomSheetState(Hidden),
sheetShape: Shape = MaterialTheme.shapes.large,
sheetElevation: Dp = ModalBottomSheetDefaults.Elevation,
sheetBackgroundColor: Color = MaterialTheme.colors.surface,
sheetContentColor: Color = contentColorFor(sheetBackgroundColor),
scrimColor: Color = ModalBottomSheetDefaults.scrimColor,
content: @Composable () -> Unit
)
각 각 매개변수가 어떤 것인지 중요한 것만 간략하게 아래 정리 해보았다.
sheetContent | 바텀 시트 내부에 표시될 콘텐츠를 정의하는 컴포저블 람다 함수 |
sheetState | 바텀 시트의 상태(열림/닫힘)를 관리하는 객체 기본은 닫힘(Hidden) |
content | 바텀 시트 이외의 메인 화면 콘텐츠를 정의하는 컴포저블 함수 |
어떤 것인지 알아봤다면 이제 구현해 보자!
## 1. rememberModalBottomSheetState 생성
// sheetState를 위해 rememberModalBottomSheetState 생성 (기본으로 닫힘 상태)
val bottomSheetState = rememberModalBottomSheetState(
initialValue = ModalBottomSheetValue.Hidden,
)
## 2. rememberCoroutineScope 추가
// show, hide 사용을 위해 추가
val coroutineScope = rememberCoroutineScope()
코루틴 스코프를 추가해 주는 이유는 바텀시트를 보여주고 숨기는 함수가 suspend이기 때문에 코루틴 스코프를 만들어준다.
보여주고 숨기는 함수는 위에서 만들어준 bottomSheetState안에 있다. (아래 참고)
<보여주는 함수>
coroutineScope.launch {
bottomSheetState.show()
}
<숨기는 함수>
coroutineScope.launch {
bottomSheetState.hide()
}
## 3. ModalBottomSheetLayout 추가
ModalBottomSheetLayout(
sheetState = bottomSheetState,
sheetShape = RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp),
sheetContent = {
// 바텀 시트 내부 콘텐츠
val fruitList = listOf("사과", "바나나", "포도", "오렌지", "토마토")
Column(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 15.dp, horizontal = 15.dp)
) {
fruitList.forEach { fruitName ->
Text(
modifier = Modifier.fillMaxWidth(),
text = fruitName,
fontSize = 14.sp
)
}
}
},
content = {
// 메인 콘텐츠
Box(modifier = Modifier.fillMaxSize()){
Button(onClick = {
coroutineScope.launch {
bottomSheetState.show()
}
}) {
Text(
text = "과일 리스트 바텀 시트"
)
}
}
}
)
이렇게 각 선언한 변수와 ModalBottomSheetLayout 매개변수에 맞게 넣고 구현해 주면 끝이 난다!
# ModalBottomSheet
이 방법은 Material3에서 새롭게 나온 API이다.
그래서 그런지 훨씬 사용하기 편하다.
ModalBottomSheetLayout와의 차이점이라면 ModalBottomSheet은 이름에서 볼 수 있다시피 Layout이 빠졌다.
그 의미는 콘텐츠와 바텀 시트를 함께 포함하지 않아도 된다는 의미이다.
구 버전 API를 사용하면 무조건 함께 포함해야 해서 기존 화면에 바텀 시트 추가를 하려면 번거로웠던 문제를 해결해 준 셈이다.
ModalBottomSheet 내부 코드이다.
@Composable
@ExperimentalMaterial3Api
fun ModalBottomSheet(
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
sheetState: SheetState = rememberModalBottomSheetState(),
sheetMaxWidth: Dp = BottomSheetDefaults.SheetMaxWidth,
shape: Shape = BottomSheetDefaults.ExpandedShape,
containerColor: Color = BottomSheetDefaults.ContainerColor,
contentColor: Color = contentColorFor(containerColor),
tonalElevation: Dp = BottomSheetDefaults.Elevation,
scrimColor: Color = BottomSheetDefaults.ScrimColor,
dragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() },
windowInsets: WindowInsets = BottomSheetDefaults.windowInsets,
properties: ModalBottomSheetProperties = ModalBottomSheetDefaults.properties(),
content: @Composable ColumnScope.() -> Unit,
) {
대부분 ModalBottomSheetLayout와 매개변수들은 똑같다. 하지만 자세히 보면 몇몇 개의 차이점이 있다.
sheetMaxWidth | 바텀 시트의 최대 너비를 제한 |
dragHandle | 바텀 시트 상단에 드래그 핸들 UI 표시 |
구현은 ModalBottomSheetLayout와 비슷하지만 살짝 다르다.
## 레이아웃 표시를 위한 remember state 변수 생성
ModalBottomSheet는 state를 통해 제어하지 않고 따로 변수를 만들어서 표시 유무를 컨트롤해 준다.
// 레이아웃 표시를 위한 변수
var isShowBottomSheet by remember{ mutableStateOf(false) }
## ModalBottomSheet 추가
메인 콘텐츠는 그대로 놔두고 Box와 ModalBottomSheet를 추가해 준다.
// 레이아웃 표시를 위한 변수
var isShowBottomSheet by remember{ mutableStateOf(false) }
Box(
modifier = Modifier.fillMaxSize()
) {
// 메인 콘텐츠
Button(
modifier = Modifier.align(Alignment.TopCenter),
onClick = {
isShowBottomSheet = !isShowBottomSheet
}) {
Text(
text = "과일 리스트 바텀 시트"
)
}
}
if (isShowBottomSheet){
ModalBottomSheet(
onDismissRequest = {
isShowBottomSheet = false
},
content = {
// 바텀 시트 내부 콘텐츠
val fruitList = listOf("사과", "바나나", "포도", "오렌지", "토마토")
Column(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 15.dp, horizontal = 15.dp)
) {
fruitList.forEach { fruitName ->
Text(
modifier = Modifier.fillMaxWidth(),
text = fruitName,
fontSize = 14.sp
)
}
}
}
)
}
}
보이는 것과 같이 똑같이 실행이 된다.
❗️여기서 rememberModalBottomSheetState는 사용 안 함?이라고 물을 수 있다.
ModalBottomSheet에서 BottomSheetState는 아래 코드와 같이 활용해서 사용할 수 있다.
val sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true,
)
// sheetState.currentValue == SheetValue.Hidden
// sheetState.currentValue == SheetValue.Expanded
// sheetState.currentValue == SheetValue.PartiallyExpanded
// Check the current state
when (sheetState.currentValue) {
SheetValue.Hidden -> {
Text("Sheet is Hidden")
}
SheetValue.Expanded -> {
Text("Sheet is Expanded")
}
SheetValue.PartiallyExpanded -> {
Text("Sheet is Partially Expanded")
}
}
처음부터 BottomSheet를 생각하는 게 아니라면 ModalBottomSheet를 사용하는 게 유지보수에 더 좋고 활용도가 더 좋을 거 같다는 생각이 들었다
참고
https://develop-oj.tistory.com/82
https://medium.com/@ramadan123sayed/bottom-sheets-in-jetpack-compose-modalbottomsheet-vs-bottomshee tscaffold-24751326 e0 ec