Animation / Animation Set / Button / Floating Action Button / Material Design

Floating Action Button – Part 3

In the previous article we animated our Floating Action Button between two states and in this concluding article we’ll create some mini actions which will be shown and hidden depending on the state of the FAB itself.

According to the material design guidelines a mini FAB should have the same icon size as a standard FAB, but be 40dp in diameter as opposed to the standard 56dp. So we need to define a new style for the mini FAB:

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <style name="AppTheme" parent="android:Theme.Material.Light.DarkActionBar">
        <item name="android:colorPrimary">@color/sa_green</item>
        <item name="android:colorPrimaryDark">@color/sa_green_dark</item>
        <item name="android:colorAccent">@color/sa_green</item>
        <item name="android:colorControlHighlight">@color/sa_green_transparent</item>
    </style>

    <style name="FloatingActionButton" parent="android:Widget.Material.Button">
        <item name="android:background">@drawable/fab_background</item>
        <item name="android:stateListAnimator">@anim/fab_state_list_animator</item>
        <item name="android:layout_width">56dp</item>
        <item name="android:layout_height">56dp</item>
    </style>

    <style name="FloatingActionButton.Mini">
        <item name="android:layout_width">40dp</item>
        <item name="android:layout_height">40dp</item>
        <item name="android:layout_margin">8dp</item>
    </style>
</resources>

This style adds an appropriate margin to the mini FAB so that the full bounds (including the margin) match the dimensions of the standard FAB. This will make things easier when laying out and animating things later on.

Next we’ll create a layout which we can include in other layouts which contain the standard FAB and three mini FABs in the expanded state.

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">

    <RelativeLayout
        android:id="@+id/fab_container"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="@dimen/activity_vertical_margin"
        android:clipChildren="false" >

        <ImageButton
            android:id="@+id/fab_action_3"
            style="@style/FloatingActionButton.Mini"
            android:src="@drawable/plus"
            android:layout_above="@+id/fab_action_2"
            android:layout_alignEnd="@+id/fab"
            android:contentDescription="@null"
            android:onClick="fabAction3" />

        <ImageButton
            android:id="@id/fab_action_2"
            style="@style/FloatingActionButton.Mini"
            android:src="@drawable/plus"
            android:layout_above="@+id/fab_action_1"
            android:layout_alignEnd="@id/fab"
            android:contentDescription="@null"
            android:onClick="fabAction2" />

        <ImageButton
            android:id="@id/fab_action_1"
            style="@style/FloatingActionButton.Mini"
            android:src="@drawable/plus"
            android:layout_above="@id/fab"
            android:layout_alignEnd="@id/fab"
            android:contentDescription="@null"
            android:onClick="fabAction1" />

        <ImageButton
            android:id="@id/fab"
            style="@style/FloatingActionButton"
            android:src="@drawable/plus"
            android:layout_alignParentEnd="true"
            android:layout_alignParentBottom="true"
            android:contentDescription="@null"
            android:visibility="visible"
            android:layout_marginTop="8dp" />

    </RelativeLayout>
</merge>

One important thing to note here is on fab_container (the parent RelativeLayout) we set android:clipChildren="false". This is necessary because of the elevation shadows that will be created for us. If we omit this parameter we’ll see some horrible clipping of the shadows.

Another thing worth noting is that the layout is structured so that the mini FABs are before the main FAB in the layout definition. This means that the main FAB will be drawn last, so if we move the mini FABs to align with the main FAB they will disappear underneath the main FAB. Aligning them is made easy thanks to the margin that we added to the mini FAB style earlier.

Next we include this in our main layout:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:clipChildren="false"
    android:clipToPadding="false"
    tools:context=".MainActivity"
    android:tag="main"
    android:transitionName="content">

    <include layout="@layout/fab" />

</RelativeLayout>

Thats the basics in place what remains is to create the transitions between the expanded and collapsed states. One way of doing this would be to create two layouts: One for the expanded state, and the other for the collapsed state. Then use Scene transitions to perform the transition animation. I tried this approach when developing the code for this series and encountered some real problems with the mini FABs being promoted to the ViewOverlay, but not being restored to the full View hierarchy on completion of the transition. Eventually I abandoned this approach and opted for the one that follows.

The first thing that we need to do is create the collapsed state. For this we need to calculate a Y translation for each of the mini FABs to align it with the main FAB, thus hiding the mini FABs behind the main one. We do this by registering an OnPreDrawListener with fab_container which will get called before it is drawn but after the measurement and layout passes have been completed. It is important to de-register the OnPreDrawListener as soon as it has been called otherwise it will be invoked for each animation frame and reduce our frame rate.

At the point where this is called, all of the child views (i.e. the main FAB and all of the mini FABs) have their correct positions within the layout defined, so we can calculate the offset of each of the mini FABs from the main FAB and apply a translation to position each one behind the main FAB. We also store this offsets as we’ll need to re-use them when transitioning from the expanded state back to the collapsed state.

private ImageButton fab;

private boolean expanded = false;

private View fabAction1;
private View fabAction2;
private View fabAction3;

private float offset1;
private float offset2;
private float offset3;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    final ViewGroup fabContainer = (ViewGroup) findViewById(R.id.fab_container);
    fab = (ImageButton) findViewById(R.id.fab);
    fabAction1 = findViewById(R.id.fab_action_1);
    fabAction2 = findViewById(R.id.fab_action_2);
    fabAction3 = findViewById(R.id.fab_action_3);
    fab.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            expanded = !expanded;
            if (expanded) {
                expandFab();
            } else {
                collapseFab();
            }
        }
    });
    fabContainer.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
        @Override
        public boolean onPreDraw() {
            fabContainer.getViewTreeObserver().removeOnPreDrawListener(this);
            offset1 = fab.getY() - fabAction1.getY();
            fabAction1.setTranslationY(offset1);
            offset2 = fab.getY() - fabAction2.getY();
            fabAction2.setTranslationY(offset2);
            offset3 = fab.getY() - fabAction3.getY();
            fabAction3.setTranslationY(offset3);
            return true;
        }
    });
}

Finally we need to create the transitions themselves:

private void collapseFab() {
    fab.setImageResource(R.drawable.animated_minus);
    AnimatorSet animatorSet = new AnimatorSet();
    animatorSet.playTogether(createCollapseAnimator(fabAction1, offset1),
            createCollapseAnimator(fabAction2, offset2),
            createCollapseAnimator(fabAction3, offset3));
    animatorSet.start();
    animateFab();
}

private void expandFab() {
    fab.setImageResource(R.drawable.animated_plus);
    AnimatorSet animatorSet = new AnimatorSet();
    animatorSet.playTogether(createExpandAnimator(fabAction1, offset1),
            createExpandAnimator(fabAction2, offset2),
            createExpandAnimator(fabAction3, offset3));
    animatorSet.start();
    animateFab();
}

private static final String TRANSLATION_Y = "translationY";

private Animator createCollapseAnimator(View view, float offset) {
    return ObjectAnimator.ofFloat(view, TRANSLATION_Y, 0, offset)
            .setDuration(getResources().getInteger(android.R.integer.config_mediumAnimTime));
}

private Animator createExpandAnimator(View view, float offset) {
    return ObjectAnimator.ofFloat(view, TRANSLATION_Y, offset, 0)
            .setDuration(getResources().getInteger(android.R.integer.config_mediumAnimTime));
}

private void animateFab() {
    Drawable drawable = fab.getDrawable();
    if (drawable instanceof Animatable) {
        ((Animatable) drawable).start();
    }
}

We define a couple of methods which get invoked from the onClick() handler for the main FAB to toggle the state. Then there are a couple of methods which construct the expand and collapse animators for each mini FAB, and finally a method to begin the main FAB transition animation that we defined in the previous article.

This is now everything hooked up, and we can see we now have a very smooth FAB with expanding mini FABs:

That just about brings us to the end of this exploration of how to implement a FAB. However, it is probably worth mentioning that given that this is a prominent new control type introduced as part of material design, there is a good chance that libraries will appear (both third-party libraries and, hopefully, an official Android support library) with FAB implementations.

One final thing worth mentioning is that, as I mentioned at the beginning of the series, I opted for minSdkVersion="21" in order to be able to use all of the available APIs that were introduced in Lollipop in support of material design. As a result of this I feel that the implementation detailed here sticks pretty close to the material design guidelines. However in order to create a backwardly compatible version will require changes in implementation which will make it much harder to adhere as closely to the material design guidelines. For example we don’t have AnimatedVectorDrawable (which made the plus / minus icon transition so easy); neither do we have StateListAnimator (which made the rise to the touch behaviour so easy); and even if we use the appcompat support library to get material themes, we’ll need to do create the shadows manually within the assets.

So let’s hope that Google are working on support libraries for some of these APIs or it will be a few years before we can reasonable write apps to minSdkVersion="21" and therefore effectively create fully material UIs (with all of the necessary subtleties).

The source code for this article is available here.

© 2015, Mark Allison. All rights reserved.

Copyright © 2015 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

3 Comments

  1. Great tutorial… thank you.
    Now I have a doubt: how can I change the distance between the mini fab (fab_action_1, fab_action_2, fab_action_3) when are shown?

    Thank you again.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.