Instantly share code, notes, and snippets.
Created
November 4, 2015 12:12
-
Star
(1)
1
You must be signed in to star a gist -
Fork
(0)
0
You must be signed in to fork a gist
-
Save dimrsilva/39268f8185af37953e95 to your computer and use it in GitHub Desktop.
A simple FloatActionMenu implementation using design support FloatActionButton
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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