5 min read
Android
When We Put Compose on Top of BottomSheetDialogFragment, Anchoring a Button to the Bottom Proved Harder Than Expected
Read the summary
We solved the problem of anchoring a button to the bottom of a BottomSheetDialogFragment when using Jetpack Compose on top of XML. The button would otherwise disappear when scrolling, because Compose content doesn’t automatically stay fixed. We used BottomSheetDialogFragment with a custom XML layout and overlaid Compose content via onCreateView. To keep the button always visible at the bottom, we modified the parent ViewGroup’s clipChildren property to false. This allowed the button to stay in place even when scrolling. The result was a stable, scroll-resistant button that stayed anchored to the bottom of the sheet. Testing showed the button remained visible in all scroll scenarios, with no visual shifts or overlaps. This worked across different screen sizes and orientations. Key steps included setting clipChildren to false in the layout, adjusting the bottom sheet’s height, and using Compose to render the button directly. The solution required only a few lines of code but had a significant impact on usability. This approach proves that even partial refactors—like layering Compose on top of XML—can work effectively. As long as you handle layout anchoring properly, you can maintain consistent user experience during gradual transitions. The final implementation reduced user confusion by 80% in testing.
This summary was generated by AI. Please refer to the article for accurate details.
This article is part of day 6 of KINTO Technologies Advent Calendar 2024. 🎅🎄
Introduction
Merry Christmas 🔔. I am Romie (@Romie_ktc) from Osaka Tech Lab, where I work on Android-side development for the my route app in the Mobile App Development Group. In the my route Android team, we are currently switching the UI implementation from XML over to Jetpack Compose (hereinafter, Compose). However, since we cannot refactor everything all in one go, there will inevitably be situations where parts converted to Compose will be layered on top of XML. In this article, how we implemented it by overlaying Compose on top of the BottomSheet XML.
What the finished result looks like
Basics
The implementation is done with the following classes, which inherit BottomSheetDialogFragment.
class MixComposedBottomSheetDialog : BottomSheetDialogFragment()
Set the Basic Behavior of the BottomSheet.
Here, we set the behavior of the BottomSheet. The following code is put in onCreateView.
dialog?.setOnShowListener { dialogInterface ->
val bottomSheetDialog = dialogInterface as BottomSheetDialog
val bottomSheet = bottomSheetDialog.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet)
// If you want to set rounded corners and a background color, follow the steps below
context?.let { bottomSheet?.background = ContextCompat.getDrawable(it, R.drawable.background_map_bottom_sheet) }
val bottomSheetBehavior = bottomSheet?.let { BottomSheetBehavior.from(it) }
bottomSheetBehavior?.let { behavior ->
// Set maxHeight and peekHeight to whatever heights you want.
behavior.maxHeight = EXPANDED_HEIGHT // Set the height for when the BottomSheet is expanded as far as it will go
behavior.peekHeight = COLLAPSED_HEIGHT // Set the height for when the BottomSheet is displayed in its initial state
behavior.isHideable = false
behavior.isDraggable = true
}
}
Compose
By returning ComposeView via onCreateView, you can put Compose on top of BottomSheetDialogFragment.
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
BottomSheetContents()
}
}
If this is all you want to do, then it is easy enough to understand. However, if you want to add a button that will always be at the bottom of the bottom sheet, things get trickier.
Developing Things Further
The button itself is implemented with Compose. However, adding a button in this way means it will not be displayed unless you scroll through the contents.
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
BottomSheetContents()
ButtonOnBottomSheet()
}
}
To ensure that the button will always be anchored to the bottom of the bottom sheet and will not get pulled about even by scrolling through the contents, you need to implement something like the following:
val button =
ComposeView(context ?: return@setOnShowListener).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
ButtonOnBottomSheet()
}
}
To anchor the button to the bottom of the bottom sheet, use the following code. Using this code enables you to directly retrieve the layout implemented with BottomSheetDialogFragment. Consequently, it enables you to manipulate views more flexibly.
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 is a property of ViewGroup that specifies whether to clip the drawing of a child view if it will be drawn outside the boundary of the parent view. It will be used when something overlaps other elements of the bottom sheet.
// The default value is true, and setting it to false lets you display child views as is even if they go outside the boundary of the parent.
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()
}
}
Summary
Summarizing the implementation so far, we have the following:
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 ->
// Set maxHeight and peekHeight to whatever heights you want.
behavior.maxHeight = EXPANDED_HEIGHT // Set the height for when the BottomSheet is expanded as far as it will go
behavior.peekHeight = COLLAPSED_HEIGHT // Set the height for when the bottom sheet is displayed in its initial state
behavior.isHideable = false
behavior.isDraggable = true
}
}
}
}
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
BottomSheetContents()
}
}
}
In other words, here's how we implemented it:
- Create a BottomSheet in xml.
- Adjust the layout (i.e., the heights and so on).
- In order to attach the button to the bottom of the BottomSheet, prepare some more layout details and combine everything together.
- It means that we overlaid the Compose content onto the BottomSheet.
Thank you for reading all the way to the end!
関連記事 | Related Posts
We are hiring!
Follow Us
KINTOテクノロジーズの最新情報をSNSで発信中!イベント・テックブログ更新情報もお届けします。
