Skip to content

Instantly share code, notes, and snippets.

@dimrsilva
Created November 4, 2015 12:12
Show Gist options
  • Save dimrsilva/39268f8185af37953e95 to your computer and use it in GitHub Desktop.
Save dimrsilva/39268f8185af37953e95 to your computer and use it in GitHub Desktop.
A simple FloatActionMenu implementation using design support FloatActionButton
<?xml version="1.0" encoding="utf-8"?>
<net.andersonribeiro.customviews.FloatActionMenu xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/floating_action_menu"
style="@style/AppTheme.FloatActionMenu"
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:tag="background"
style="@style/AppTheme.FloatActionMenu.Background"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
<TextView
android:id="@+id/label_action4"
style="@style/AppTheme.FloatActionMenu.Label.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toLeftOf="@+id/fab_action4"
android:layout_alignTop="@+id/fab_action4"
android:text="New Collection"/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab_action4"
style="@style/AppTheme.FloatActionMenu.FloatActionButton.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@+id/fab_action3"
android:layout_alignLeft="@+id/fab_action1" />
<TextView
android:id="@+id/label_action3"
style="@style/AppTheme.FloatActionMenu.Label.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toLeftOf="@+id/fab_action3"
android:layout_alignTop="@+id/fab_action3"
android:text="Invite People"/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab_action3"
style="@style/AppTheme.FloatActionMenu.FloatActionButton.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@+id/fab_action2"
android:layout_alignLeft="@+id/fab_action1" />
<TextView
android:id="@+id/label_action2"
style="@style/AppTheme.FloatActionMenu.Label.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toLeftOf="@+id/fab_action2"
android:layout_alignTop="@+id/fab_action2"
android:text="Create Team"/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab_action2"
style="@style/AppTheme.FloatActionMenu.FloatActionButton.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@+id/fab_action1"
android:layout_alignLeft="@+id/fab_action1" />
<TextView
android:id="@+id/label_action1"
style="@style/AppTheme.FloatActionMenu.Label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toLeftOf="@+id/fab_action1"
android:layout_alignTop="@+id/fab_action1"
android:text="Create Post"/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab_action1"
style="@style/AppTheme.FloatActionMenu.FloatActionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
/>
</net.andersonribeiro.customviews.FloatActionMenu>
package net.andersonribeiro.customviews;
import android.animation.Animator;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.support.design.widget.FloatingActionButton;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewAnimationUtils;
import android.view.animation.OvershootInterpolator;
import android.widget.RelativeLayout;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class FloatActionMenu extends RelativeLayout {
private static final int CIRCULAR_ANIMATION_DURATION = 450;
private static final int ANIMATION_DURATION = 150;
private static final int LAST_ANIMATION_DELAY = 120;
private OnClickListener onOptionMenuClickListener;
private OnClickListener onOptionMenuClickListenerWrapper = new OnClickListener() {
@Override
public void onClick(View v) {
if (onOptionMenuClickListener != null) {
hide(true);
onOptionMenuClickListener.onClick(v);
}
}
};
private View viewBackground;
private List<TextView> labels = new ArrayList<>();
private FloatingActionButton fabMenu;
private List<FloatingActionButton> buttons = new ArrayList<>();
private boolean shown;
public FloatActionMenu(Context context) {
super(context);
}
public FloatActionMenu(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FloatActionMenu(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public FloatActionMenu(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
viewBackground = findViewWithTag("background");
int count = getChildCount();
for(int i=0; i < count; ++i) {
View child = getChildAt(i);
if (child instanceof TextView) {
labels.add((TextView)child);
} else if (child instanceof FloatingActionButton) {
buttons.add((FloatingActionButton)child);
} else if (viewBackground != child) {
throw new RuntimeException("No supported view type");
}
}
fabMenu = (FloatingActionButton) buttons.get(buttons.size() - 1);
buttons.remove(fabMenu);
viewBackground.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
hide(true);
}
});
for (View button : buttons) {
button.setOnClickListener(onOptionMenuClickListenerWrapper);
}
fabMenu.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (shown) {
onOptionMenuClickListenerWrapper.onClick(v);
}
else {
show(true);
}
}
});
shown = false;
viewBackground.setVisibility(INVISIBLE);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
viewBackground.setAlpha(0);
}
hideAfterInflate(labels);
hideAfterInflate(buttons);
}
public void show(boolean animate) {
if (!shown) {
shown = true;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
int centerX = fabMenu.getLeft() + (fabMenu.getMeasuredWidth() / 2);
int centerY = fabMenu.getTop() + (fabMenu.getMeasuredHeight() / 2);
int startRadius = 0;
int endRadius = (int) Math.hypot(centerX, centerY);
Animator animator = ViewAnimationUtils
.createCircularReveal(viewBackground, centerX, centerY, startRadius, endRadius)
.setDuration(animate ? CIRCULAR_ANIMATION_DURATION : 0);
animator.addListener(createAnimationListener(viewBackground, false));
animator.start();
}
else {
viewBackground.animate()
.alpha(1f)
.setDuration(animate ? ANIMATION_DURATION : 0)
.setListener(createAnimationListener(viewBackground, false))
.start();
}
Collections.reverse(buttons);
bubbleViews(animate, buttons);
Collections.reverse(buttons);
Collections.reverse(labels);
bubbleViews(animate, labels);
Collections.reverse(labels);
}
}
public void hide(boolean animate) {
if (shown) {
shown = false;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
int centerX = fabMenu.getLeft() + (fabMenu.getMeasuredWidth() / 2);
int centerY = fabMenu.getTop() + (fabMenu.getMeasuredHeight() / 2);
int startRadius = (int) Math.hypot(centerX, centerY);
int endRadius = 0;
Animator animator = ViewAnimationUtils
.createCircularReveal(viewBackground, centerX, centerY, startRadius, endRadius)
.setDuration(animate ? CIRCULAR_ANIMATION_DURATION / 2 : 0);
animator.addListener(createAnimationListener(viewBackground, true));
animator.start();
}
else {
viewBackground.animate()
.alpha(0)
.setDuration(animate ? ANIMATION_DURATION : 0)
.setListener(createAnimationListener(viewBackground, true))
.start();
}
hideViews(animate, buttons);
hideViews(animate, labels);
}
}
public void setOnOptionMenuClickListener(OnClickListener onOptionMenuClickListener) {
this.onOptionMenuClickListener = onOptionMenuClickListener;
}
public boolean isShown() {
return shown;
}
private static <T extends View> void bubbleViews(boolean animate, List<T> views) {
int length = views.size() - 1;
int index = 0;
for (View view : views) {
view.animate()
.alpha(1f)
.scaleX(1f)
.scaleY(1f)
.setDuration(animate ? ANIMATION_DURATION : 0)
.setStartDelay(animate ? ((LAST_ANIMATION_DELAY / length) * index) : 0)
.setListener(createAnimationListener(view, false))
.setInterpolator(new OvershootInterpolator())
.start();
index++;
}
}
private static <T extends View> void hideViews(boolean animate, List<T> views) {
int length = views.size() - 1;
int index = 0;
for (final View view : views) {
view.animate()
.alpha(0)
.scaleX(0)
.scaleY(0)
.setDuration(animate ? ANIMATION_DURATION : 0)
.setStartDelay(animate ? ((LAST_ANIMATION_DELAY / length) * index) : 0)
.setListener(createAnimationListener(view, true))
.start();
index++;
}
}
private static <T extends View> void hideAfterInflate(List<T> views) {
for (View view : views) {
view.setVisibility(INVISIBLE);
view.setAlpha(0);
view.setScaleX(0);
view.setScaleY(0);
}
}
private static Animator.AnimatorListener createAnimationListener(final View view, final boolean hide) {
return new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
view.setVisibility(VISIBLE);
}
@Override
public void onAnimationEnd(Animator animation) {
if (hide) {
view.setVisibility(INVISIBLE);
}
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
};
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment