[Trouble Shooting] Compose Expandable Column With ModalBottomSheet

covy·2024년 5월 10일

문제상황

현재 바텀시트에는 다음과 같은 요구사항이 있었다.

  • Wrap한 height이다. (처음 보여줄 필터 갯수가 정해져있다)
  • 상단 타이틀과 하단 검색하기는 sticky view이다
  • 더보기, 접기를 통한 필터영역은 expandable, scrollable 해야한다.

ModalBottomSheet를 필터 화면으로 구현하고 있다가 문제가 발생했다.
wrap된 height에서 더보기를 했을 때 상단바를 뚫고 올라가는 것이었다.

접었을 때펼쳤을 때

원하는 동작은?

기존 접기 상태의 height를 고정하여 scrollable한 view로 만들고 싶었다.

고민한 방법들

  1. modalBottomSheet를 고정된 상수 높이로 선언한다.
  2. 하위 뷰 크기를 계산한다.
  3. 스크롤 영역의 뷰 크기를 고정시킬 방법을 찾는다.

1번은 빈 공간이 생기거나, 보여주어야할 뷰가 가릴 위험이 있었다.
2번도 충분히 해결 가능한 방법이지만, 뭔가 더 awesome한 해결 방법이 있을 것 같아 3번 위주로 찾아보았다.

뭔가 컴포즈에서 제공하는 awesome한 방법이 있을 것 같아 찾아보느라 이래저래 삽질을 길게 했는데, 못찾고 결국은 2,3번을 섞어서 해결했다.

핵심은 확대전에 height를 측정하여서 높이로 고정시키는 방법을 택했다.

<주의> 하단에서 없어질 코드입니다.

val density = LocalDensity.current
    val height = remember {
        mutableStateOf(0.dp)

modifier = if (height.value == 0.dp) modifier
            .verticalScroll(scrollState)
            .padding(horizontal = 24.dp)
            .onSizeChanged { height.value = with(density) { it.height.toDp() } }
        else modifier
            .height(height.value)
            .verticalScroll(scrollState)
            .padding(horizontal = 24.dp),

이렇게 해결된 듯 보였지만..!

해결하고 좋아하고 있었는데, 해결된 것이 아니었다.

먼저 폰트 크기를 키웠을 때 여전히 상단을 넘어갔고, 하단 sticky view도 안보이는 현상이 발현되었다. 아래처럼 말이다. (또한 해상도에 따라서도 똑같은 현상이 나타났다.)

상태바 먼저 걷어내보자

상태바를 걷어내는 것을 시도했다.

간단한 방법이었는데, configuration의 screenHeight를 가져와서 최대 높이로 적용해주었다.

screenHeightDp는 인셋값을 제외하여 사용가능한 스크린 높이를 구해준다.

val configuration = LocalConfiguration.current
val screenHeight = configuration.screenHeightDp.dp

Column(modifier = modifier.heightIn(0.dp, screenHeight))

하단 Sticky View 수정시도

마지막으로 하단 Sticky View를 해결하기 위해 2가지 방법을 떠올렸다.

  • Constraint 설정
  • Box와 하단 패딩 활용

1번을 시도해보았는데, 이 또한 바텀모달의 높이가 동적(wrap_size)이기 때문에, 설정이 되지 않았다.

마지막 남은 방법으로 Box를 시도해보았다.
구체적인 방법은 아래와 같다.

  • Sticky View의 높이를 측정한다 (modifier의 onSizeChanged).
  • 나머지 타이틀과, ScrollView는 Column으로 묶어 하단 패딩을 준다.

시도한 코드는 아래와 같다

@Composable
fun FilterBottomSheet(
    modifier: Modifier,
    selectedFilterUiState: PolicyFiltersUiModel,
    onClassificationCheckChanged: (ClassificationUiModel) -> Unit,
    onRegionCheckChanged: (RegionUiModel) -> Unit,
    onFilterAllOff: () -> Unit,
    onSearchWithFilter: () -> Unit,
    onCloseFilter: () -> Unit,
) {
    val filterListUiState = remember { mutableStateOf(FilterListUiState()) }
    val scrollState = rememberScrollState()
    val configuration = LocalConfiguration.current
    val screenHeight = configuration.screenHeightDp.dp
    var footerHeight by remember { mutableStateOf(0.dp) }
    val localDensity = LocalDensity.current
    Box(modifier = modifier.heightIn(0.dp, screenHeight)) {
        FilterFooter(
            modifier = modifier
                .align(Alignment.BottomCenter)
                .onSizeChanged {
                    footerHeight = with(localDensity) { it.height.toDp() }
                },
            onFilterAllOff = onFilterAllOff,
            onSearchWithFilter = onSearchWithFilter,
        )
        Column(
            modifier = modifier
                .align(Alignment.TopCenter)
                .padding(bottom = footerHeight),
        ) {
            FilterHeader(
                modifier = modifier,
                onCloseFilter = onCloseFilter,
            )
            ScrollableFilterSection(
                modifier = modifier,
                filterListUiState = filterListUiState.value,
                selectedFilterUiState = selectedFilterUiState,
                onClassificationCheckChanged = onClassificationCheckChanged,
                onRegionCheckChanged = onRegionCheckChanged,
                onClassificationMoreViewClick = {
                    filterListUiState.value =
                        filterListUiState.value.copy(isClassificationExpanded = !filterListUiState.value.isClassificationExpanded)
                },
                onRegionMoreViewClick = {
                    filterListUiState.value =
                        filterListUiState.value.copy(isRegionExpanded = !filterListUiState.value.isRegionExpanded)
                },
                scrollState = scrollState,
            )
        }
    }
}
  

onGloballyPositioned

해당 방법으로 뷰의 크기를 측정할 수도 있지만, 아래 코드를 사용함으로써 statusbar의 크기만큼 뷰의 크기도 제외되어서 보인다.

val configuration = LocalConfiguration.current
val screenHeight = configuration.screenHeightDp.dp

진짜 해결!

가로도 예쁘게 잘 보인다.
사실 오기가 생겨서 요구사항을 다 맞춰보았는데, 이런 경우에는 디자이너분과 상의해서 상단 고정 패딩주고 만들 것 같기도하다.

세로가로작은비율

0개의 댓글