morristech
1/12/2018 - 12:43 PM

Code to support a floating action button using Material design and animations that expands to show additional actions; supports API 14+

Code to support a floating action button using Material design and animations that expands to show additional actions; supports API 14+

package com.bigoven.android.widgets;

import android.animation.ObjectAnimator;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;

import com.bigoven.android.R;
import com.bigoven.android.utilities.UiHelper;

import java.util.List;

/**
 *
    This class is a custom layout that allows the user to add a primary floating action button with secondary actions
    that expand when the primary button is clicked.
    Copyright (C) 2015 Chantell Osejo

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>
 */
public class ExpandableFloatingActionButtonLayout extends RelativeLayout {
    private final static int DEFAULT_MARGIN = UiHelper.convertDpToPixel(16);
    private final static int SECONDARY_DEFAULT_MARGIN = UiHelper.convertDpToPixel(8);
    private final LinearLayout.LayoutParams mPrimaryLayoutParams = new LinearLayout.LayoutParams(FloatingActionButton.GOOGLE_DESIGN_GUIDELINE_RECOMMENDED_DIAMETER_PX, FloatingActionButton.GOOGLE_DESIGN_GUIDELINE_RECOMMENDED_DIAMETER_PX);
    private final LinearLayout.LayoutParams mSecondaryLayoutParams = new LinearLayout.LayoutParams(FloatingActionButton.GOOGLE_DESIGN_GUIDELINE_RECOMMENDED_MINI_DIAMETER_PX, FloatingActionButton.GOOGLE_DESIGN_GUIDELINE_RECOMMENDED_MINI_DIAMETER_PX);
    private LinearLayout mButtonContainer;
    private boolean mExpanded = false;

    private CompositeOnClickListener mPrimaryButtonOnClickListener;

    public ExpandableFloatingActionButtonLayout(Context context) {
        this(context, null);
    }

    public ExpandableFloatingActionButtonLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ExpandableFloatingActionButtonLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public ExpandableFloatingActionButtonLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context);
    }

    private void init(Context context) {
        LayoutInflater.from(context).inflate(R.layout.expandable_floating_action_button_layout, this);
        mButtonContainer = (LinearLayout) findViewById(R.id.container);

        mPrimaryLayoutParams.setMargins(DEFAULT_MARGIN, DEFAULT_MARGIN, DEFAULT_MARGIN, DEFAULT_MARGIN);
        mSecondaryLayoutParams.setMargins(0, SECONDARY_DEFAULT_MARGIN, 0, SECONDARY_DEFAULT_MARGIN);
        mPrimaryButtonOnClickListener = new CompositeOnClickListener();
    }

    public void addFloatingActionButtons(final FloatingActionButton primaryFloatingActionButton, final List<FloatingActionButton> secondaryActionButtons) {
        // If this gets called more than once, then we will clean-slate it first.
        mButtonContainer.removeAllViews();
        mButtonContainer.addView(primaryFloatingActionButton, mPrimaryLayoutParams);

        final ObjectAnimator rotate = ObjectAnimator.ofFloat(primaryFloatingActionButton, ROTATION, 0f, 180f);
        rotate.setDuration(200);

        for (FloatingActionButton secondaryActionButton : secondaryActionButtons) {
            secondaryActionButton.setVisibility(View.GONE);
            mButtonContainer.addView(secondaryActionButton, 0, mSecondaryLayoutParams);
        }

        mPrimaryButtonOnClickListener.addOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                mExpanded = !mExpanded;

                for (int i = 0; i < secondaryActionButtons.size(); i++) {
                    // Increase the delay for every additional button, so we get the effect of a "ripple" of buttons being added
                    // We'll reverse this delay if we're hiding them
                    secondaryActionButtons.get(mExpanded ? i : secondaryActionButtons.size() - 1 - i).setVisibilityAfterDelay(mExpanded ? View.VISIBLE : View.GONE, (i + 1) * 100);
                }
                rotate.start();
            }
        });

        primaryFloatingActionButton.setOnClickListener(mPrimaryButtonOnClickListener);

        setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                if (motionEvent.getAction() == MotionEvent.ACTION_UP && mExpanded) {
                    primaryFloatingActionButton.performClick();
                }

                return false;
            }
        });
    }

    public void setPrimaryButtonOnClickListener(OnClickListener onClickListener) {
        mPrimaryButtonOnClickListener.addOnClickListener(onClickListener);
    }
}