Making most out of Android Bottom Sheet

Mihir Patel
5 min readSep 1, 2019

BottomSheet was released a few years back and it’s a material component that slides up from the bottom of the screen to reveal more content. Though there have been a few years since the release, I strongly believe it is still one of the most underutilised material component. You can find more detailed information on Bottom Sheet on Google Material Design guidelines.

As per the material design guideline for BottomSheet, there are two types of BottomSheets:

  1. Modal bottom sheets: It Can be implemented using BottomSheetDialog or BottomSheetDialogFragment. These are an alternative to inline menus or simple dialogs on mobile and provide room for additional items, longer descriptions, and iconography. They must be dismissed in order to interact with the underlying content.
  2. Persistent bottom sheets: It can be implemented using BottomSheetBehavior in conjunction with a CoordinatorLayout.

We’ll primarily focus on Modal bottom sheets.

Using BottomsheetDialogFragment is fairly very easy, and it is a very strong alternative to conventional widgets i.e. PopupWindow, AlertDialog, DialogFragment. As it slides up from the bottom and is well within reach of a thumb, it’s a perfect solution for asking the user to choose from multiple options. Conventional widgets are somewhat useless these days as the average size of smartphones is 6" and requires the use of both hands for 100% screen reachability.

When implemented smartly, the very same component can be reused across the application for different use cases.

Many apps have adopted BottomSheetDialogFragment beautifully to fit in their use cases.

Folks at Twitter has almost eliminated the context menu from their app and have replaced it with BottomSheetDialogFragment.

In terms of implementation, using BottomSheetDialogFragment is not at all a difficult task, All you need to do is extend your class with BottomSheetDialogFragment and call it just like an ordinary fragment.

You may have to add Gradle dependency if not using material components already.

//ANDROID X
implementation 'com.google.android.material:material:1.1.0-alpha09'
OR//SUPPORT LIBRARY
implementation 'com.android.suppor:design:28.0.8'

Let’s start with very basic BottomSheetDialogFragment.

#1 No bells and whistles, just a plain BottomSheet fragment.
code sample for #1

Now, there’s no straight forward way to add more customisations to basic BottomSheet.

Say, you would like to have top corners on BottomSheet to be curved. Something like this,

BottomSheet with curved top corners

If you just apply the background with curved top corners it won’t work. You’ll get something ugly like this,

Those ugly stubborn white corners are there because by default bottomsheet takes parent view background colour. To solve this parent view needs a transparent background, which we can set by overriding onActivityCreated method of BottomSheetDialogFragment and set view background colour as transparent.

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
(view?.parent as View).setBackgroundColor(Color.TRANSPARENT)
}

Easy!

What if, your design needs margin on sides too? Will applying just margin on your fragment layout will do? Not really, it won’t work. BottomSheet takes margins also from parent view. Just like background colour we also have to override margins.

BottomSheet with curved top corners and margins on sides.
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
(view?.parent as View).setBackgroundColor(Color.TRANSPARENT)
val resources = resources

if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
assert(view != null)
val parent = view?.parent as View
val layoutParams = parent.layoutParams as CoordinatorLayout.LayoutParams
layoutParams.setMargins(
resources.getDimensionPixelSize(R.dimen.screen_margin), // LEFT 16dp
0,
resources.getDimensionPixelSize(R.dimen.screen_margin), // RIGHT 16dp
0
)
parent.layoutParams = layoutParams
}
}

There’s a lot more can be done using BottomSheets, everything you do with normal fragments can be done with BottomSheetDialogFragment. However, due to its swipe down to dismiss gesture, it may create usability issues when multiline EditTexts are used. When a user has written multiple lines and tries to move the cursor or up/down, user might end up dismissing the fragment itself.

By default, BottomSheetDialogFragments doesn’t expand to full screen. Only some content of it made visible to user and is expanded to full screen only when user makes swipe up gesture.

Only portion of entire content is visible initially.

To open BottomSheetDialogFragment on full screen, there’s no straight forward property available. onCreateDialog method needs to be overridden and have to set “state” of BottomSheetBehavior as “STATE_EXPANDED”.

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val bottomSheetDialog = super.onCreateDialog(savedInstanceState) as BottomSheetDialog
bottomSheetDialog.setOnShowListener {
val dialog = it as BottomSheetDialog
val bottomSheet = dialog.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet)
BottomSheetBehavior.from(bottomSheet).state = BottomSheetBehavior.STATE_EXPANDED
}
return bottomSheetDialog
}

Again, fairly easy!

Story still doesn’t end here, if you are using scrollview, dynamic views OR recyclerview in your fragment and contents are too small for covering entire EXPANDED state and too large for fit into a partially expanded state. When you don’t have enough content to fit into EXPANDED state of a fragment, too much of whitespace looks horrible. Solving this problem is most tricky as it only happens when views are dynamic.

Here, in onStart method of fragment, views are made visible to the user. Which means, you can calculate the height of contents and set peekHeight of BottomSheetBehavior. This more or less works as wrap_content.

override fun onStart() {
super.onStart()
dialog?.also {
val bottomSheet = dialog?.findViewById<View>(R.id.design_bottom_sheet)
bottomSheet?.layoutParams?.height = ViewGroup.LayoutParams.WRAP_CONTENT
val behavior = BottomSheetBehavior.from<View>(bottomSheet)
val layout = root?.findViewById(R.id.rootLayout) as LinearLayout //rootLayout is root of your fragment layout.
layout.viewTreeObserver?.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
try {
layout.viewTreeObserver.removeGlobalOnLayoutListener(this)
behavior.peekHeight = layout.height
view
?.requestLayout()
} catch (e: Exception) {
}
}
})
}
}

I’ve covered most of the problems which didn’t had any out of the box solution. If you also have faced any problem and have/don’t have solution to it, put on a response to this article!

Thanks for reading! 🙂

--

--