Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android FloatingActionButton Speed Dial

I am currently working on an Android app in which I use a FloatingActionButton. I would like to use the speed dial to have multiple actions that spin/jump out of the action button as described in this page by Google on Android design, or as could be seen in an earlier version of the Keep app (sorry, but I can only post one link). I am using the Android Design Support library specifically version 23.1.1 (com.android.support:design:23.1.1). I already searched using Google and looked at the reference for the FloatingActionButton but couldn't find anything concerning the speed dial.

I would like to know if there is a way to easily achieve this using the default FloatingActionButton, or if I have to program all transitions/animations manually?

Additionally I would like to have little labels next to the buttons, describing the action, if possible.

Thank you in advance!

like image 229
Sneaky Tinkerton Avatar asked Feb 13 '16 01:02

Sneaky Tinkerton


3 Answers

This library is implementing the Speed Dial from the Material Design guidelines:

https://github.com/leinardi/FloatingActionButtonSpeedDial

enter image description here

like image 24
Roberto Leinardi Avatar answered Nov 10 '22 21:11

Roberto Leinardi


I'm here to add my 2 cents because this is where I landed after Googling for that exact title.

I hope it doesn't come too late to help someone like me.

First off, the solution comes from here, so is not mine. I just tried and it works nicely. So i thought i share with you in a single post rather have you go dig the code up from there.

The solution uses com.android.support:design:25.3.1 library so be sure to add that to build.gradle and it requires API 21 onwards.

The bad news is that it is composed from several small moving parts: 5 animators, 5 drawables plus the icons and layouts and of course, the code, the good news is that it works as it should, is highly customizable and doesn't require any coding outside MainActivity.

Some notes:

  • The big fab's image morphs between more and minus signs and rotates when tapped.
  • Buttons can have text, provided you put both the text and each small fab inside a LineaLayout and move the button id to the LinearLayout so it gets animated instead of the fab, but it requires code to hide and show the text when necessary.

This is the result:

enter image description here

So, the ingredients:

  1. Drawables (res/drawable/).

animated_minus.xml

<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:viewportHeight="24"
    android:viewportWidth="24"
    android:width="24dp"
    android:height="24dp">

    <group android:name="plus_group" android:pivotX="12" android:pivotY="12">
        <path
            android:name="plus_path"
            android:strokeColor="@android:color/white"
            android:strokeWidth="3"
            android:pathData="M12,0L12,24M0,12,L24,12" />
    </group>
</vector>

animated_plus.xml

<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/plus">
    <target
        android:animation="@animator/rotate_clockwise"
        android:name="plus_group" />
    <target
        android:animation="@animator/plus_to_minus"
        android:name="plus_path" />
</animated-vector>

fab_background.xml

<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
    android:color="?android:colorControlHighlight">
    <item>
        <shape android:shape="oval">
            <solid android:color="?android:colorAccent" />
        </shape>
    </item>
</ripple>

minus.xml

<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:viewportHeight="24"
    android:viewportWidth="24"
    android:width="24dp"
    android:height="24dp">
    <group android:name="plus_group" android:pivotX="12" android:pivotY="12">
        <path
            android:name="plus_path"
            android:strokeColor="@android:color/white"
            android:strokeWidth="3"
            android:pathData="M12,12L12,12M0,12,L24,12" />
    </group>
</vector>

plus.xml

<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:viewportHeight="24"
    android:viewportWidth="24"
    android:width="24dp"
    android:height="24dp">
    <group android:name="plus_group" android:pivotX="12" android:pivotY="12">
        <path
            android:name="plus_path"
            android:strokeColor="@android:color/white"
            android:strokeWidth="3"
            android:pathData="M12,0L12,24M0,12,L24,12" />
    </group>
</vector>
  1. Animators (res/animator/).

fab_state_list_animator.xml

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:state_pressed="true"
        android:state_enabled="true">
        <set>
            <objectAnimator
                android:propertyName="translationZ"
                android:duration="100"
                android:valueTo="3dp"
                android:valueType="floatType" />
            <objectAnimator
                android:propertyName="elevation"
                android:duration="0"
                android:valueTo="5dp"
                android:valueType="floatType" />
        </set>
    </item>
    <!-- base state -->
    <item android:state_enabled="true">
        <set>
            <objectAnimator
                android:propertyName="translationZ"
                android:duration="100"
                android:valueTo="0"
                android:startDelay="100"
                android:valueType="floatType" />
            <objectAnimator
                android:propertyName="elevation"
                android:duration="0"
                android:valueTo="5dp"
                android:valueType="floatType" />
        </set>
    </item>
    <item>
        <set>
            <objectAnimator
                android:propertyName="translationZ"
                android:duration="0"
                android:valueTo="0"
                android:valueType="floatType" />
            <objectAnimator
                android:propertyName="elevation"
                android:duration="0"
                android:valueTo="0"
                android:valueType="floatType" />
        </set>
    </item>
</selector>

minus_to_plus.xml

<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
    android:propertyName="pathData"
    android:valueFrom="M12,0L12,24M12,12,L12,12"
    android:valueTo="M12,0L12,24M0,12,L24,12"
    android:valueType="pathType"
    android:duration="@android:integer/config_mediumAnimTime" />

plus_to_minus.xml

<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
    android:propertyName="pathData"
    android:valueFrom="M12,0L12,24M0,12,L24,12"
    android:valueTo="M12,0L12,24M12,12,L12,12"
    android:valueType="pathType"
    android:duration="@android:integer/config_mediumAnimTime" />

rotate_anticlockwise.xml

<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
    android:propertyName="rotation"
    android:valueFrom="90"
    android:valueTo="0"
    android:valueType="floatType"
    android:duration="@android:integer/config_mediumAnimTime" />

rotate_clockwise.xml

<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
    android:propertyName="rotation"
    android:valueFrom="0"
    android:valueTo="90"
    android:valueType="floatType"
    android:duration="@android:integer/config_mediumAnimTime" />
  1. Layouts. (res/layout/)

fab.xml. All fabs are declared here. Replace android:src with your own icon on the first 3 ImageButtons.

<?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:layout_marginEnd="@dimen/activity_horizontal_margin"
        android:clipChildren="false" >

        <!-- Please note that the @id are defined the first time they're referenced from top to bottom -->
        <ImageButton
            android:id="@+id/fab_action_3"
            style="@style/FloatingActionButton.Mini"
            android:src="@drawable/ic_volume_up_white_24dp"
            android:layout_above="@+id/fab_action_2"
            android:layout_alignEnd="@+id/fab"
            android:contentDescription="@null"
            android:backgroundTint="@color/sa_gray"
            android:width="24dp"
            android:height="24dp"
            android:onClick="fabAction3" />

        <ImageButton
            android:id="@id/fab_action_2"
            style="@style/FloatingActionButton.Mini"
            android:src="@drawable/ic_credit_card_white_24dp"
            android:layout_above="@+id/fab_action_1"
            android:layout_alignEnd="@id/fab"
            android:contentDescription="@null"
            android:backgroundTint="@color/sa_gray"
            android:width="24dp"
            android:height="24dp"
            android:onClick="fabAction2" />

        <ImageButton
            android:id="@id/fab_action_1"
            style="@style/FloatingActionButton.Mini"
            android:src="@drawable/ic_add_shopping_cart_white_24dp"
            android:layout_above="@id/fab"
            android:layout_alignEnd="@id/fab"
            android:contentDescription="@null"
            android:backgroundTint="@color/sa_gray"
            android:width="24dp"
            android:height="24dp"
            android:onClick="fabAction1" />

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

    </RelativeLayout>
</merge>

And lastly.

  1. The code (java//MainActivity.java)

a) Some declarations:

private static final String TAG = "Floating Action Button";
private static final String TRANSLATION_Y = "translationY";
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;

b) Delete the usual fab code on MainActivity's onCreate:

  FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
    fab.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
                    .setAction("Action", null).show();
        }
    });

and replace it with:

final ViewGroup fabContainer = (ViewGroup) findViewById(R.id.fab_container);
fab = (ImageButton) findViewById(R.id.fab);
fabAction1 = findViewById(R.id.fab_action_1);
// insert onClickListener here
fabAction2 = findViewById(R.id.fab_action_2);
// insert onClickListener here
fabAction3 = findViewById(R.id.fab_action_3);
// insert onClickListener here
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;
    }
});

c) Add supporting functions on MainActivity (animation code mostly and the 3 small fab's onClick methods):

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 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();
    }
}

public void fabAction1(View view) {
    Log.d(TAG, "Action 1");
    Toast.makeText(this, "Go shopping!", Toast.LENGTH_SHORT).show();
}

public void fabAction2(View view) {
    Log.d(TAG, "Action 2");
    Toast.makeText(this, "Gimme money!", Toast.LENGTH_SHORT).show();
}

public void fabAction3(View view) {
    Log.d(TAG, "Action 3");
    Toast.makeText(this, "Turn it up!", Toast.LENGTH_SHORT).show();
}

d) Reference the fab.xml layout from res/layout/activity_main.xml

Delete the fab declaration:

<android.support.design.widget.FloatingActionButton
    android:id="@+id/fab"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="bottom|end"
    android:layout_margin="@dimen/fab_margin"
    app:srcCompat="@android:drawable/ic_dialog_email" />

Replace with:

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

Last notes:

  1. Feel free to scrap the onClick code for the fabs and replace it with onClickListener. Those should go right where the comment that says // insert onClickListener here. Just remember to delete the onClick attribute for each fab in fab.xml file and get rid of the last 3 functions in MainActivity (fabAction1, fabAction2 and fabAction3).
  2. Most measures, dimensions, etc. I put them right in the code to avoid including even more files.
  3. The code is not optimized on changed in any way.

I hope this helps someone and sorry for the wall of text.

like image 64
Iker Hoëk Avatar answered Nov 10 '22 23:11

Iker Hoëk


I would like to know if there is a way to easily achieve this using the default FloatingActionButton

FAB from Design Library does not have this feature. You need to look for 3rd party FABs (there's a few on android-arsenal to choose from)

like image 28
Marcin Orlowski Avatar answered Nov 10 '22 23:11

Marcin Orlowski