Coordinating floating action button along Bottom Sheet

Adam Styrc
TechTalks@Vattenfall
3 min readJun 15, 2020

At InCharge app, we currently work hard on a full app redesign and so far I can tell we’ll use Bottom Sheets heavily 😎 Bottom Sheet itself is not that hard to add, assuming we use CoordinatorLayout as a parent.

Let’s look at a case study I’m going to focus on today. We want sticking out bottom sheet with a list of items on a map if any. The bottom sheet could be dragged down or expanded to a fullscreen size. And above the sheet we want a Floating Action Button that will move along our bottom sheet, except when expanding the bottom sheet, it will get overlapped.

We’ll start from nesting the XML layout and as I mentioned we will need a CoordinatorLayout because thanks to the Behaviours we can describe any views moves dependencies!

In the xml

Instead of using official Floating Action Button, I’ll just use ImageView.

<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="72dp">

<ImageButton
android:id="@+id/ivMyLocation"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="bottom|end"
android:layout_marginEnd="@dimen/general_margin"
android:layout_marginBottom="56dp"
android:background="@drawable/circle_incharge_white"
android:src="@drawable/ic_my_location_blue" app:layout_behavior="com.nuon.laadpalen.ui.MapFloatingButtonBehavior"/>

<androidx.core.widget.NestedScrollView
android:id="@+id/svBottomSheetChargingLocationsList"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/rounded_top_incharge_white"
android:orientation="vertical"
app:behavior_hideable="true"
app:behavior_peekHeight="@dimen/charging_locations_list_peek_height" app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">

<LinearLayout
android:id="@+id/layoutChargingLocationsListContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
...
</LinearLayout>

</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

So as we see there are 2 behaviours:

  1. com.google.android.material.bottomsheet.BottomSheetBehavior — it will handle bottom sheet scroll behaviour for us. It is built in the Android dependency ‘androidx.coordinatorlayout:coordinatorlayout:1.1.0’. Note that we have to put additional parameters to make the bottom sheet collapsable to desired height (sticking out height):
app:behavior_hideable="true"   app:behavior_peekHeight="@dimen/charging_locations_list_peek_height"

2. com.nuon.laadpalen.ui.MapFloatingButtonBehavior this is something custom, we will need to create MapFloatingButtonBehavior class as a subclass of CoordinatorLayout.Behavior.

⚠️ When passing a custom Behavior make sure to put full path of the class instead of e.g. “ui.MapFloatingButtonBehavior” ! ️⚠️

🧨 Otherwise, you might not get any futher than:

android.view.InflateException: Binary XML file line … Could not inflate Behavior subclass ui.MapFloatingButtonBehavior 🥺

Implementing the Behavior

Finally, we get to the MapFloatingButtonBehavior implementation. If we plan on using the Befhavior just from the xml, a 2-argument constructor (Context, AttributeSet) is sufficient.

In the custom Behavior we need to indicate a dependency with our bottom sheet view (via layoutDependsOn) and then add a callback onDependentViewChanged which will only refer to the bottom sheet changes:

class MapFloatingButtonBehavior(
context: Context,
attrs: AttributeSet?
) : CoordinatorLayout.Behavior<View>(context, attrs) {
private val locationsListBottomSheetPeekHeight =
context.resources.getDimension(R.dimen.charging_locations_list_peek_height)
private val margin = context.resources.getDimension(R.dimen.small_margin)

override fun layoutDependsOn(
parent: CoordinatorLayout,
child: View,
dependency: View
): Boolean {
return dependency.id == R.id.svBottomSheetChargingLocationsList
}

override fun onDependentViewChanged(
parent: CoordinatorLayout,
child: View,
dependency: View
): Boolean {
if (dependency.y < parent.height - locationsListBottomSheetPeekHeight) {
child.y = parent.height - locationsListBottomSheetPeekHeight - (child.height + margin)
} else {
child.y = dependency.y - (child.height + margin)
}
return false
}
}

Quite simple, isn’t it ?️️ 👯‍♀️ 🕺🏻 👯‍♀️

And here’s how it works:

--

--