Modal BottomSheetDialogFragment 크기 (feat 2 step, full screen)

chen-studio·2024년 12월 7일

BottomSheetDialogFragment

Android Material에서 제공하는 스와이프 가능한 BottomSheetDialogFragment를 구현하는 방법은 크게 2가지이다. Compose에서 제공하는 ModalBottomSheetLayout은 이 글에서 다루지 않는다.

Standard Bottom Sheet

  • Standard Bottom Sheet는 현재 주 UI영역에 함께 포함되어 현재 UI와 상호작용이 가능한 BottomSheet이다. 예를들어, BottomSheet의 확장된 상태에 따라 내가 보고있는 UI를 동적으로 변경할 수 있다.

  • Standard Bottom SheetCoordinatorLayout과 함께 사용해야 하며 기존에 사용하던 UI에 이 방식의 Bottom Sheet를 도입하려고 하면 기존 UI구조의 변경이 불가피하다.
<androidx.coordinatorlayout.widget.CoordinatorLayout
  ...>

  <FrameLayout
    ...
    android:id="@+id/standard_bottom_sheet"
    app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">

    <!-- Bottom sheet contents. -->

  </FrameLayout>

</androidx.coordinatorlayout.widget.CoordinatorLayout>
  • 좀더 유연한 UI를 만들고 싶다면 이 방식을 선택하는것이 맞다. 하지만 BottomSheet없이 구현되어 있는 기존의 복잡한 UI에서 이 방식을 도입하려고 하면 구조의 변경이 불가피하여 어떤 SideEffect가 발생할 지 예측하기 어렵다.
  • 따라서 필자는 아래에서 설명할 Modal Bottom Sheet로 구현하는 방법을 택했다.
  • Modal Bottom Sheet는 현재 UI에 종속되지 않고 새로운 Window를 가지는 별도의 Fragment이다. BottomSheetDialogFragment를 상속받은 Fragment를 구현하고 show하는 것으로 BottomSheet를 사용할 수 있다.
class ModalBottomSheet : BottomSheetDialogFragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? = inflater.inflate(R.layout.modal_bottom_sheet_content, container, false)

    companion object {
        const val TAG = "ModalBottomSheet"
    }
}

class MainActivity : AppCompatActivity() {
    ...
    val modalBottomSheet = ModalBottomSheet()
    modalBottomSheet.show(supportFragmentManager, ModalBottomSheet.TAG)
    ...
}
  • 필자가 이 방식을 도입해본 결과 크게 2가지 문제가 있었다.
  1. Modal Bottom Sheet 방식으로 구현된 BottomSheet는 아무리 높이를 설정해도 9:16(가로:세로) 비율 이상으로 크기를 조절할 수 없다.

  2. 요구사항은 이 BottomSheet를 접거나 펼칠 수 있는 Step이 3단계(완전히 접힌 상태, 중간, 펼쳐진 상태) 로 3가지 상태를 만들어야 했다.

  • 위 문제를 해결한 방법을 소개한다.

  • Material에서 제공하는 BottomSheetDialogFragment는 여러가지 상태를 조절할 수 있도록 behavior 라는것을 제공한다.

  • behavior에 대한 속성은 워낙 많으므로 모두 설명하지 않고 현재 문제를 해결하는데 필요한 것들만 소개한다.

  • 먼저, behavior에는 크게 6가지 상태가 존재한다.

  • 여러가지 상태가 존재하지만 펼친상태를 2단계로 사용하기 위해 STATE_COLLAPSED(접힌상태)와 STATE_EXPANDED(펼쳐진상태)를 사용할 것이다.

  • 그리고 behaviorpeekHeight라는 변수를 사용할 것이다.

  • peekHeight는 STATE_COLLAPSED상태의 높이를 지정하는 변수이다.

  • STATE_COLLAPSED 상태의 높이를 적용하는 방법은 아래와 같다

  • 먼저 아래와 같은 방법으로 현재 디바이스의 height pixel을 구한다

@Px
fun Fragment.getHeightPixels(): Int = resources.displayMetrics.heightPixels
  • 위에서 설명한 peekHeight는 BottomSheetDialogFragment가 아닌 BottomSheetDialog의 속성이다
  • 따라서 BottomSheetDialog로부터 behavior를 구하는 확장함수를 정의했다
fun BottomSheetDialogFragment.getBehavior(): BottomSheetBehavior<*>? =
    (dialog as? BottomSheetDialog)?.behavior
  • 그럼 behavior의 peekHeight를 원하는 pixel값으로 설정하여 원하는 높이를 설정할 수 있다
  • 예를들어, 전체 화면의 50%만큼의 높이를 접힌상태의 높이로 정의하고 싶은 경우 아래와 같이 설정할 수 있다
behavior?.peekHeight = (heightPixels * 0.5).toInt()
  • 이제 STATE_EXPANDED 상태의높이를 정의하려면 내가 사용하고자 하는 View의 높이를 STATE_COLLAPSED를 적용했던 것과 같은 방법으로 적용한다
  • 예를들어, 펼쳐진 상태에서 화면을 가득 채우고 싶은 경우 아래와 같이 설정할 수 있다
// root는 BottomSheetDialogFragment의 root view, viewBinding 사용중이라면 binding.root
root.layoutParams.height = (heightPixels * expandedRatio).toInt()

결과

  • 위와같은 내용을 바탕으로 펼쳐진 상태를 2step으로 나눌 수 있으며 처음 bottom sheet가 보일때 상태를 정의하는 parameter까지 추가하여 아래와 같은 확장함수를 정의할 수 있다

  • 또한 collapsedRatio와 expandedRatio를 같은 값 (예를들어 1.0F)로 설정한 경우, 기존에 9:16 이상으로 확장되지 않던 높이 문제도 해결할 수 있으며 같은 값을 가지는 경우 2step이 아닌 1step으로 동작하게 된다

@Px
fun Fragment.getHeightPixels(): Int = resources.displayMetrics.heightPixels

fun BottomSheetDialogFragment.getBehavior(): BottomSheetBehavior<*>? =
    (dialog as? BottomSheetDialog)?.behavior

/**
 * Set BottomSheetDialogFragment's height into 2 step
 *
 * If the values of collapsedRatio and expandedRatio are the same, it works only 1 step regardless of initialState
 *
 * @param initialState STATE_COLLAPSED or STATE_EXPANDED
 * @param collapsedRatio height ratio when bottom sheet is collapsed
 * @param expandedRatio height ratio when bottom sheet is expanded
 */
fun BottomSheetDialogFragment.setHeightRatio(
    root: View,
    initialState: Int = STATE_COLLAPSED,
    @FloatRange(from = 0.1, to = 1.0) collapsedRatio: Float = 0.7F,
    @FloatRange(from = 0.1, to = 1.0) expandedRatio: Float = 1.0F
) {
    validateParams(initialState, collapsedRatio, expandedRatio)

    root.layoutParams.height = (getHeightPixels() * expandedRatio).toInt()
    getBehavior()?.peekHeight = (getHeightPixels() * collapsedRatio).toInt()
    getBehavior()?.state = initialState
}

private fun validateParams(initialState: Int, collapsedRatio: Float, expandedRatio: Float) =
    when {
        initialState != STATE_COLLAPSED && initialState != STATE_EXPANDED ->
            throw IllegalArgumentException(
                "initialState should be STATE_COLLAPSED or STATE_EXPANDED"
            )

        collapsedRatio.toBigDecimal() < 0.1.toBigDecimal() ->
            throw IllegalArgumentException("collapsedRatio should be more than 0.1")

        expandedRatio.toBigDecimal() < collapsedRatio.toBigDecimal() ->
            throw IllegalArgumentException("expandedRatio should be more than collapsedRatio")

        else -> Unit
    }
  • parameter가 올바른 값을 가지는지 검증하기 위한 validateParams 를 구현하였으며 위 메소드를 BottomSheetDialogFragment의 onViewCreated() 이후에 설정하면 정상적으로 동작한다

  • 샘플 예제가 궁금하다면 아래 github를 참고하세요
    https://github.com/chen-studio/android-modal-bottomsheet

profile
Android Engineer

0개의 댓글