package com.marverenic.music.view; import android.content.Context; import android.os.Build; import android.support.annotation.DrawableRes; import android.support.annotation.StringRes; import android.support.design.widget.CoordinatorLayout; import android.support.design.widget.FloatingActionButton; import android.support.design.widget.Snackbar; import android.support.v4.view.ViewCompat; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; import android.view.animation.AnimationSet; import android.view.animation.AnimationUtils; import android.view.animation.Transformation; import android.view.animation.TranslateAnimation; import android.widget.FrameLayout; import android.widget.TextView; import com.marverenic.music.R; import java.util.ArrayList; import java.util.List; import timber.log.Timber; public class FABMenu extends FloatingActionButton implements View.OnClickListener { private static final int SIZE_L_DP = 56; private static final int SIZE_S_DP = 40; private FrameLayout screen; private final List<FloatingActionButton> children = new ArrayList<>(); private final List<TextView> labels = new ArrayList<>(); private boolean childrenVisible = false; private Runnable delayedRunnable; public FABMenu(Context context) { super(context); setOnClickListener(this); buildScreen(context); } public FABMenu(Context context, AttributeSet attrs) { super(context, attrs); setOnClickListener(this); buildScreen(context); } public FABMenu(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); setOnClickListener(this); buildScreen(context); } private void buildScreen(Context context) { screen = new FrameLayout(context); screen.setLayoutParams( new FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); //noinspection deprecation screen.setBackgroundColor(context.getResources().getColor(R.color.screen_overlay)); screen.setOnClickListener(this); } public void addChild(@DrawableRes int icon, OnClickListener onClickListener, String label) { children.add(buildChild(icon, onClickListener, label)); labels.add(buildChildLabel(label)); } public void addChild(@DrawableRes int icon, OnClickListener onClickListener, @StringRes int label) { final String name = getResources().getString(label); children.add(buildChild(icon, onClickListener, name)); labels.add(buildChildLabel(name)); } private FloatingActionButton buildChild(@DrawableRes int icon, final OnClickListener onClickListener, String label) { FloatingActionButton button = (FloatingActionButton) LayoutInflater.from(getContext()) .inflate(R.layout.mini_fab, (ViewGroup) getParent(), true) .findViewWithTag("fab-null"); button.setTag("fab-" + label); button.setImageResource(icon); button.setVisibility(GONE); button.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { onClickListener.onClick(v); hideChildren(); } }); if (getParent() instanceof CoordinatorLayout) { final float padding = getResources().getDimension(R.dimen.fab_margin); final float dpScale = getResources().getDisplayMetrics().density; CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) button.getLayoutParams(); if (ViewUtils.isRtl(getContext())) { params.leftMargin += padding; } else { params.rightMargin += padding; } params.bottomMargin = (int) (SIZE_L_DP * dpScale + padding * (2 + children.size()) + SIZE_S_DP * dpScale * children.size()); // For some reason, the children are 12dp higher and 18dp further to the left on pre-L // devices than on L+ devices. I don't know for sure what causes this (I suspect it's // the drop shadow or elevation compatibility code), but this takes care of it. // // There's probably a better way to fix this, but this was the easiest. If for some // reason this changes in an update to one of the support libraries, just remeasure // these offsets and update them here. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { if (ViewUtils.isRtl(getContext())) { params.leftMargin -= 12 * dpScale; } else { params.rightMargin -= 12 * dpScale; } params.bottomMargin -= 18 * dpScale; } button.setLayoutParams(params); } else { Timber.e("Parent must be a CoordinatorLayout to properly set margin"); } // When children aren't visible on screen, remove them from the view hierarchy completely // If we don't do this, then the FloatingActionButton Behaviors conflict for some reason // and Snackbars won't slide the FAB up which is kind of an important detail. // // FABMenu.Behavior takes care of some of the left over discrepancies like overlapping FAB's // // Additionally, the screen is functionally important because it prevents the user // from doing something that could generate a Snackbar when the FAB's are visible // which can cause the main FAB to be overlapped. ((ViewGroup) button.getParent()).removeView(button); return button; } private TextView buildChildLabel(String name) { TextView label = (TextView) LayoutInflater.from(getContext()) .inflate(R.layout.mini_fab_label, (ViewGroup) getParent(), true) .findViewWithTag("fab-label-null"); label.setTag("fab-label-" + label); label.setText(name); label.setVisibility(GONE); if (getParent() instanceof CoordinatorLayout) { final float padding = getResources().getDimension(R.dimen.fab_margin); final float dpScale = getResources().getDisplayMetrics().density; CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) label.getLayoutParams(); if (ViewUtils.isRtl(getContext())) { params.leftMargin += padding + 40 * dpScale; } else { params.rightMargin += padding + 40 * dpScale; } params.bottomMargin = (int) (SIZE_L_DP * dpScale + 4 * dpScale + padding * (2 + labels.size()) + SIZE_S_DP * dpScale * labels.size()); label.setLayoutParams(params); } else { Timber.e("Parent must be a CoordinatorLayout to properly set margin"); } ((ViewGroup) label.getParent()).removeView(label); return label; } public void show() { Animation fabAnim = AnimationUtils.loadAnimation(getContext(), R.anim.fab_in); fabAnim.setDuration(300); fabAnim.setInterpolator(getContext(), android.R.interpolator.decelerate_quint); startAnimation(fabAnim); // Make sure the FAB is visible when the animation starts setVisibility(View.VISIBLE); } public void hide() { if (childrenVisible) { hideChildren(); childrenVisible = false; } Animation fabAnim = AnimationUtils.loadAnimation(getContext(), R.anim.fab_out); fabAnim.setDuration(300); fabAnim.setInterpolator(getContext(), android.R.interpolator.accelerate_quint); fabAnim.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) {} @Override public void onAnimationEnd(Animation animation) { // Make sure to hide the FAB after the animation finishes and reset its rotation setVisibility(View.GONE); setRotation(0f); } @Override public void onAnimationRepeat(Animation animation) {} }); startAnimation(fabAnim); } public void showChildren() { if (childrenVisible || delayedRunnable != null) { return; } childrenVisible = true; // Start a sliding animation on each child for (int i = 0; i < children.size(); i++) { final FloatingActionButton child = children.get(i); ((ViewGroup) getParent()).addView(child); final float padding = getResources().getDimension(R.dimen.fab_margin); final float dpScale = getResources().getDisplayMetrics().density; final float dY = 28 * dpScale + padding + (padding + 40 * dpScale) * i; TranslateAnimation translateAnim = new TranslateAnimation(0, 0, dY, 0); AlphaAnimation fadeAnim = new AlphaAnimation(0, 1); AnimationSet slideFadeAnim = new AnimationSet(true); slideFadeAnim.addAnimation(translateAnim); slideFadeAnim.addAnimation(fadeAnim); slideFadeAnim.setInterpolator(getContext(), android.R.interpolator.decelerate_quint); slideFadeAnim.setDuration(300 + 25 * i); child.startAnimation(slideFadeAnim); // Make sure the FABs are visible when the animation starts child.setVisibility(VISIBLE); } //Delay the label animation delayedRunnable = new Runnable() { @Override public void run() { final AlphaAnimation fadeAnim = new AlphaAnimation(0, 1); fadeAnim.setDuration(400); fadeAnim.setInterpolator(getContext(), android.R.interpolator.decelerate_quint); for (TextView l : labels) { ((ViewGroup) getParent()).addView(l); l.setVisibility(VISIBLE); l.startAnimation(fadeAnim); } delayedRunnable = null; } }; postDelayed(delayedRunnable, 300); // Rotate the main FAB icon by 45 degrees to form a close button Animation rotateAnim = new Animation() { @Override protected void applyTransformation(float interpolatedTime, Transformation t) { setRotation(45 * interpolatedTime); } }; rotateAnim.setInterpolator(getContext(), android.R.interpolator.decelerate_quint); rotateAnim.setDuration(300); startAnimation(rotateAnim); // Make the list inactive by showing a screen over it ((ViewGroup) getParent()).addView(screen, ((ViewGroup) getParent()).indexOfChild(this)); AlphaAnimation fadeAnimation = new AlphaAnimation(0, 1); fadeAnimation.setInterpolator(getContext(), android.R.interpolator.decelerate_quint); fadeAnimation.setDuration(300); screen.startAnimation(fadeAnimation); } public void hideChildren() { if (!childrenVisible || delayedRunnable != null) { return; } childrenVisible = false; Animation fabAnim = AnimationUtils.loadAnimation(getContext(), R.anim.fab_out); fabAnim.setDuration(300); fabAnim.setInterpolator(getContext(), android.R.interpolator.accelerate_quint); Animation labelAnim = AnimationUtils.loadAnimation(getContext(), R.anim.abc_fade_out); labelAnim.setDuration(300); labelAnim.setInterpolator(getContext(), android.R.interpolator.accelerate_quint); for (FloatingActionButton c : children) { c.startAnimation(fabAnim); } for (TextView l : labels) { l.startAnimation(labelAnim); } // Make sure to hide the FABs and screen after the animation finishes delayedRunnable = new Runnable() { @Override public void run() { for (FloatingActionButton c : children) { c.setVisibility(GONE); ((ViewGroup) c.getParent()).removeView(c); } for (TextView l : labels) { l.setVisibility(GONE); ((ViewGroup) l.getParent()).removeView(l); } ((ViewGroup) screen.getParent()).removeView(screen); delayedRunnable = null; } }; postDelayed(delayedRunnable, 300); // Rotate the main FAB icon by 45 degrees to invert the original rotation Animation rotateAnim = new Animation() { @Override protected void applyTransformation(float interpolatedTime, Transformation t) { setRotation(45 + 45 * interpolatedTime); } }; rotateAnim.setInterpolator(getContext(), android.R.interpolator.decelerate_quint); rotateAnim.setDuration(300); startAnimation(rotateAnim); // Make the list active again by removing the screen over it AlphaAnimation fadeAnimation = new AlphaAnimation(1, 0); fadeAnimation.setInterpolator(getContext(), android.R.interpolator.accelerate_quint); fadeAnimation.setDuration(300); screen.startAnimation(fadeAnimation); } @Override public void onClick(View v) { if (v == this) { if (childrenVisible) { hideChildren(); } else { showChildren(); } } else if (v == screen) { hideChildren(); } } // A lot of code here is copied from FloatingActionButton.Behavior because I can't override the // methods since Google made them private. The only code that's actually functionally different // is in updateFabTranslationForSnackbar( ... ) @SuppressWarnings("unused") public static class Behavior extends FloatingActionButton.Behavior { public Behavior() { super(); } public Behavior(Context context, AttributeSet attrs) { super(); } @Override public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton child, View dependency) { if (dependency instanceof Snackbar.SnackbarLayout) { updateFabTranslationForSnackbar(parent, child, dependency); return false; } else { return super.onDependentViewChanged(parent, child, dependency); } } private void updateFabTranslationForSnackbar(CoordinatorLayout parent, FloatingActionButton fab, View snackbar) { float translationY = this.getFabTranslationYForSnackbar(parent, fab); ViewCompat.setTranslationY(fab, translationY); for (FloatingActionButton child : ((FABMenu) fab).children) { ViewCompat.setTranslationY(child, translationY); } for (TextView label : ((FABMenu) fab).labels) { ViewCompat.setTranslationY(label, translationY); } } private float getFabTranslationYForSnackbar(CoordinatorLayout parent, FloatingActionButton fab) { float minOffset = 0.0F; List dependencies = parent.getDependencies(fab); int i = 0; for (int z = dependencies.size(); i < z; ++i) { View view = (View) dependencies.get(i); if (view instanceof Snackbar.SnackbarLayout && parent.doViewsOverlap(fab, view)) { minOffset = Math.min( minOffset, ViewCompat.getTranslationY(view) - (float) view.getHeight()); } } return minOffset; } } }