FloatingActionsMenu & FloatingActionButton Behavior

Nikola Despotoski
4 min readSep 19, 2015

I find FloatingActionsMenu library suitable for chaining floating actions buttons. Regardless it isn’t maintained regularly, it still does the work, if you don’t have time writing your own layout for the same purpose.

Beside it is being limited in features, it lacks Behavior that will make it behave in CoordinatorLayout.

So why not write ourselves one, huh?

What I needed from the Behavior

I intended to write a Behavior that will toggle the FloatingActionsMenu when the content is scrolled.
1. Hide the normal sized FAB when the content is scrolled up.
2. Show the normal sized FAB when the content is scrolled down.
3. (Optional) Collapse the menu, if you have more than one FAB.

Behavior

We are interested in acting upon vertical scroll only. When the nested scroll is started, we need to signal if we are interested in the nested scrolling, so our FloatingActionsMenu becomes target that will receive nested scroll events.

@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionsMenu child, View directTargetChild, View target, int nestedScrollAxes) {
return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL;
}

Next, we need to react on nested scrolls, by differentiating between scrolling up and down. We accumulate total vertical scrolled distances, upon which we should course our actions. Also it is important to handle when there is change in scroll directions. When we are scrolling up our mTotalDy is always positive, since dyConsumed is positive. When scrolling down, these two are holding negative values, hence the ternary statement.

If you want to take actions upon slightest change in nested scroll events, you might consider dropping the conditions about total scrolled value and menu height and just check for direction changes.

@Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionsMenu child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
mTotalDy = dyConsumed < 0 && mTotalDy > 0 || dyConsumed > 0 && mTotalDy < 0 ? 0 : mTotalDy;
attemptCancelAnimation(child);
mTotalDy += dyConsumed;
updateFloatingActionMenu(child);
}

There is important case to handle, that is the case when the NestedScrollView is flinged. First we need to check if we have fling on y-axis, which is our interest. Velocity on y-axis is negative when flinging up, this is the same case when scrolling up, contrary velocity is positive when flinging down. We are not interested in specific velocity value, but only if it is up or down fling.

@Override
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, final FloatingActionsMenu child, View target, float velocityX, float velocityY) {
if (Math.abs(velocityY) < Math.abs(velocityX)) return false;

final int childCount = child.getChildCount();
final View firstFab = child.getChildAt(childCount - 1);
if (velocityY < 0 && isScaleX(firstFab,0f)) {
scaleTo(firstFab, 1f, null);
firstFab.setClickable(true);
} else if (velocityY > 0) {
if (child.isExpanded()) {
child.collapse();
child.setOnFloatingActionsMenuUpdateListener(new FloatingActionsMenu.OnFloatingActionsMenuUpdateListener() {
@Override
public void onMenuExpanded() {

}

@Override
public void onMenuCollapsed() {
if (isScaleX(firstFab,1f)) {
scaleTo(firstFab, 0f, null);
firstFab.setClickable(false);
}
}
});
} else {
if (isScaleX(firstFab,1f)) {
scaleTo(firstFab, 0f, null);
firstFab.setClickable(false);
}
}

}
return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
}

What happens next in updateFloatingActionMenu() is not magic. Simply, we need to collapse the floating menu once we have scrolled more than height of our expanded menu and start planning to hide the remaining normal sized FAB. If we don’t have more than one FAB in the chain, we should immediately move to scaling the normal sized FAB.

private void updateFloatingActionMenu(final FloatingActionsMenu child) {
int totalHeight = child.getHeight();
View firstFab = child.getChildAt(0);
if (child.getChildCount() > 1) {
if (child.isExpanded()) {
totalHeight -= firstFab.getHeight();
if (mTotalDy >= totalHeight && !isAnimating) {
isAnimating = true;
child.collapse();
child.setOnFloatingActionsMenuUpdateListener(mFloatingMenuUpdateListener);
} else if (mTotalDy < 0 && !child.isExpanded()) {
if (isScaleX(child,0f)) {
scaleTo(child, 1f, null);
firstFab.setClickable(true);
}
}
} else {
totalHeight = firstFab.getHeight();
firstFab = child.getChildAt(child.getChildCount() - 1);
updateFirstFloatingActionButton(firstFab, totalHeight);
}
} else {
totalHeight = child.getHeight();
updateFirstFloatingActionButton(child.getChildAt(0), totalHeight);
}
}

If we are left with only one FAB, same condition applies for the remaining FAB to be hidden.

private void updateFirstFloatingActionButton(View firstFab, int totalHeight) {
if (mTotalDy >= totalHeight && isScaleX(firstFab, 1f)) {
firstFab.setClickable(false);
scaleTo(firstFab, 0f, null);
} else if (mTotalDy < 0 && Math.abs(mTotalDy) >= totalHeight && isScaleX(firstFab, 0f)) {
scaleTo(firstFab, 1f, null);
firstFab.setClickable(true);
}
}

Bonus

In order to have complete behavior, we also need to take cover the case when a SnackBar is shown. This is exactly the same behavior seen in FloatinActionButton.Behavior in design support library.

@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionsMenu child, View dependency) {
if (dependency instanceof Snackbar.SnackbarLayout) {
updateFabTranslationForSnackbar(child, dependency);
}
return false;
}

private void updateFabTranslationForSnackbar(FloatingActionsMenu child, View dependency) {
float translationY = Math.min(0, ViewCompat.getTranslationY(dependency) - dependency.getHeight());
ViewCompat.setTranslationX(child, translationY);
}
@Override
public void onDependentViewRemoved(CoordinatorLayout parent, FloatingActionsMenu child, View dependency) {
if (dependency instanceof Snackbar.SnackbarLayout && ViewCompat.getTranslationY(child) != 0.0F) {
ViewCompat.animate(child).translationY(0.0F).scaleX(1.0F).scaleY(1.0F).alpha(1.0F).setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR).start();
}

}

Layout

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/preview_top_parent"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.AppBarLayout ...>
<android.support.v4.widget.NestedScrollView ...
app:layout_behavior="@string/appbar_scrolling_view_behavior"
 ...>
<include

android:id="@+id/floating_actions_holder"
layout="@layout/include_floating_actions_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right|bottom"
android:layout_marginBottom="24dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
app:layout_behavior=".view.FloatingActionMenuBehavior"
/>
<android.support.v7.widget.Toolbar ...>
</android.support.design.widget.CoordinatorLayout>

Outcome

Don’t forget to take look at the complete gist here.

Video:

P.S: I know that my floating actions chain lacks another fab to match Material Design criteria of having at least 3 options in the chain. :)

--

--