BottomSheetDialogFragmentの上にComposeを載っけると下部にボタンを固定するのが想像以上に難しかった件
この記事は KINTOテクノロジーズアドベントカレンダー2024 の6日目の記事です🎅🎄
はじめに
Merry Christmas🔔
モバイルアプリ開発Gでmy routeアプリのAndroid側の開発を担当しておりますOsaka Tech LabのRomie(@Romie_ktc)です。
現在、my routeのAndroidチームではxmlからJetpack Compose(以下Compose)へUIの実装を切り替えております。
ただ、一気に全てをリファクタリングできませんので、どうしてもxmlの上にCompose化したパーツが乗っかるということも出てきてしまいます。
今回は、BottomSheetのxmlの上にComposeを載っける形で実装したお話をします。

完成イメージ
基本編
BottomSheetDialogFragmentを継承した以下のクラスにて実装します。
class MixComposedBottomSheetDialog : BottomSheetDialogFragment()
BottomSheetの基本的な挙動を設定する
BottomSheetの挙動を設定します。
以下のコードをonCreateViewに記載します。
dialog?.setOnShowListener { dialogInterface ->
val bottomSheetDialog = dialogInterface as BottomSheetDialog
val bottomSheet = bottomSheetDialog.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet)
// 角丸や背景色を設定したい場合は以下で対応します
context?.let { bottomSheet?.background = ContextCompat.getDrawable(it, R.drawable.background_map_bottom_sheet) }
val bottomSheetBehavior = bottomSheet?.let { BottomSheetBehavior.from(it) }
bottomSheetBehavior?.let { behavior ->
// maxHeightやpeekHeightは任意の高さを設定してください。
behavior.maxHeight = EXPANDED_HEIGHT // BottomSheetが最大限まで拡張されたときの高さを設定
behavior.peekHeight = COLLAPSED_HEIGHT // BottomSheetが初期状態で表示される高さを設定
behavior.isHideable = false
behavior.isDraggable = true
}
}
Compose
onCreateViewにてComposeViewをreturnすることによって、BottomSheetDialogFragmentの上にComposeを載せることができます。
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
BottomSheetContents()
}
}
これだけならわかりやすいですが、ここにBottomSheet下部に常に固定されたButtonを追加するとなると難易度が上がります。
発展編
Button自体はComposeで実装します。
ただしこのようにButtonを追加しても、ContentsをスクロールしないとButtonは表示されません。
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
BottomSheetContents()
ButtonOnBottomSheet()
}
}
常にButtonをBottomSheetの下部に固定させ、Contentsをスクロールしても引っ張られることなく表示させるためには、以下のような実装が必要です。
val button =
ComposeView(context ?: return@setOnShowListener).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
ButtonOnBottomSheet()
}
}
BottomSheetの下部にButtonを固定させるためには、以下のコードを使います。
このコードを使用することで、BottomSheetDialogFragmentで実装されているレイアウトを直接取得できます。したがってより柔軟にViewの操作が可能になります。
val containerLayout = dialogInterface.findViewById<FrameLayout>(com.google.android.material.R.id.container)
val coordinatorLayout = dialogInterface.findViewById<CoordinatorLayout>(com.google.android.material.R.id.coordinator)
clipChildrenはViewGroupのプロパティで、子Viewが親Viewの範囲外に描画される場合に描画をクリップするかどうかを指定します。
BottomSheetの他の要素と重なる場合に用いられます。
// デフォルト値はtrueで、falseに設定すると子Viewが親の境界を超えてもそのまま表示できます。
button.clipChildren = false
button.layoutParams =
FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT,
).apply {
gravity = Gravity.BOTTOM
}
containerLayout?.addView(button)
button.post {
val layoutParams = coordinatorLayout?.layoutParams as? ViewGroup.MarginLayoutParams
layoutParams?.apply {
button.measure(
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
)
this.bottomMargin = button.measuredHeight
containerLayout?.requestLayout()
}
}
まとめ
これまでの実装をまとめますと、以下の通りです。
override fun onCreateView(): View {
dialog?.setOnShowListener { dialogInterface ->
val bottomSheetDialog = dialogInterface as BottomSheetDialog
val containerLayout = dialogInterface.findViewById<FrameLayout>(com.google.android.material.R.id.container)
val coordinatorLayout = dialogInterface.findViewById<CoordinatorLayout>(com.google.android.material.R.id.coordinator)
val bottomSheet = bottomSheetDialog.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet)
context?.let { bottomSheet?.background = ContextCompat.getDrawable(it, R.drawable.background_map_bottom_sheet) }
val button =
ComposeView(context ?: return@setOnShowListener).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
ButtonOnBottomSheet()
}
}
button.clipChildren = false
button.layoutParams =
FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT,
).apply {
gravity = Gravity.BOTTOM
}
containerLayout?.addView(button)
button.post {
val layoutParams = coordinatorLayout?.layoutParams as? ViewGroup.MarginLayoutParams
layoutParams?.apply {
button.measure(
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
)
this.bottomMargin = button.measuredHeight
containerLayout?.requestLayout()
val bottomSheetBehavior = bottomSheet?.let { BottomSheetBehavior.from(it) }
bottomSheetBehavior?.let { behavior ->
// maxHeightやpeekHeightは任意の高さを設定してください。
behavior.maxHeight = EXPANDED_HEIGHT // BottomSheetが最大限まで拡張されたときの高さを設定
behavior.peekHeight = COLLAPSED_HEIGHT // BottomSheetが初期状態で表示される高さを設定
behavior.isHideable = false
behavior.isDraggable = true
}
}
}
}
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
BottomSheetContents()
}
}
}
つまりどうしたかと言いますと
- xmlでBottomSheetを作成し
- 高さなどのレイアウトを調整し
- BottomSheetの下部にButtonをくっつけるために更にレイアウトを用意し合体させ
- Compose化したコンテンツをBottomSheetに上乗せした
ということです。
ここまで読んでいただきありがとうございました!
関連記事 | Related Posts
BottomSheetDialogFragmentの上にComposeを載っけると下部にボタンを固定するのが想像以上に難しかった件

Android View から Jetpack Compose への移行の第一歩

First Steps When Migrating From Android View to Jetpack Compose
myroute Android AppでのJetpack Compose

Jetpack Compose Animation Techniques: Enhance App Impressions with Minimal Code Changes
Compose超初心者のPreview感動体験
We are hiring!
ビジネスアナリスト(マーケティング/事業分析)/分析プロデュースG/東京・大阪・福岡
デジタル戦略部 分析プロデュースグループについて本グループは、『KINTO』において経営注力のもと立ち上げられた分析組織です。決まった正解が少ない環境の中で、「なぜ」を起点に事業と向き合い、分析を軸に意思決定を前に進める役割を担っています。
【PdM】オープンポジション/東京・名古屋・大阪
募集背景KINTOテクノロジーズでは新たな事業展開と共に開発するプロダクトが拡大しています。サービスの新規立ち上げ、立ち上げたプロダクトのグロースを推進し、KINTOの事業展開を支えるプロダクトマネージャーを求めています。



