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

<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>
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)
...
}
Modal Bottom Sheet 방식으로 구현된 BottomSheet는 아무리 높이를 설정해도 9:16(가로:세로) 비율 이상으로 크기를 조절할 수 없다.
요구사항은 이 BottomSheet를 접거나 펼칠 수 있는 Step이 3단계(완전히 접힌 상태, 중간, 펼쳐진 상태) 로 3가지 상태를 만들어야 했다.
위 문제를 해결한 방법을 소개한다.
Material에서 제공하는 BottomSheetDialogFragment는 여러가지 상태를 조절할 수 있도록 behavior 라는것을 제공한다.
behavior에 대한 속성은 워낙 많으므로 모두 설명하지 않고 현재 문제를 해결하는데 필요한 것들만 소개한다.
먼저, behavior에는 크게 6가지 상태가 존재한다.

여러가지 상태가 존재하지만 펼친상태를 2단계로 사용하기 위해 STATE_COLLAPSED(접힌상태)와 STATE_EXPANDED(펼쳐진상태)를 사용할 것이다.
그리고 behavior의 peekHeight라는 변수를 사용할 것이다.
peekHeight는 STATE_COLLAPSED상태의 높이를 지정하는 변수이다.

STATE_COLLAPSED 상태의 높이를 적용하는 방법은 아래와 같다
먼저 아래와 같은 방법으로 현재 디바이스의 height pixel을 구한다
@Px
fun Fragment.getHeightPixels(): Int = resources.displayMetrics.heightPixels
fun BottomSheetDialogFragment.getBehavior(): BottomSheetBehavior<*>? =
(dialog as? BottomSheetDialog)?.behavior
behavior?.peekHeight = (heightPixels * 0.5).toInt()
// 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