package com.rey.material.widget;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.RadialGradient;
import android.graphics.RectF;
import android.graphics.Shader;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.SystemClock;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AnimationUtils;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.widget.FrameLayout;
import android.widget.RelativeLayout;
import com.rey.material.R;
import com.rey.material.drawable.LineMorphingDrawable;
import com.rey.material.drawable.RippleDrawable;
import com.rey.material.util.ThemeUtil;
import com.rey.material.util.ViewUtil;
public class FloatingActionButton extends View {
private OvalShadowDrawable mBackground;
private Drawable mIcon;
private Drawable mPrevIcon;
private int mAnimDuration;
private Interpolator mInterpolator;
private SwitchIconAnimator mSwitchIconAnimator;
private int mIconSize;
private RippleManager mRippleManager;
public static FloatingActionButton make(Context context, int resId){
return new FloatingActionButton(context, null, resId);
}
public FloatingActionButton(Context context) {
super(context);
init(context, null, 0, 0);
}
public FloatingActionButton(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs, 0, 0);
}
public FloatingActionButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr, 0);
}
public FloatingActionButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr, defStyleRes);
}
private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
setClickable(true);
mSwitchIconAnimator = new SwitchIconAnimator();
applyStyle(context, attrs, defStyleAttr, defStyleRes);
}
public void applyStyle(int resId){
applyStyle(getContext(), null, 0, resId);
}
private void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FloatingActionButton, defStyleAttr, defStyleRes);
int radius = a.getDimensionPixelSize(R.styleable.FloatingActionButton_fab_radius, ThemeUtil.dpToPx(context, 28));
int elevation = a.getDimensionPixelSize(R.styleable.FloatingActionButton_fab_elevation, ThemeUtil.dpToPx(context, 4));
int bgColor = a.getColor(R.styleable.FloatingActionButton_fab_backgroundColor, ThemeUtil.colorAccent(context, 0xFFFAFAFA));
int iconSrc = a.getResourceId(R.styleable.FloatingActionButton_fab_iconSrc, 0);
int iconLineMorphing = a.getResourceId(R.styleable.FloatingActionButton_fab_iconLineMorphing, 0);
mIconSize = a.getDimensionPixelSize(R.styleable.FloatingActionButton_fab_iconSize, ThemeUtil.dpToPx(context, 24));
mAnimDuration = a.getInteger(R.styleable.FloatingActionButton_fab_animDuration, context.getResources().getInteger(android.R.integer.config_mediumAnimTime));
int resId = a.getResourceId(R.styleable.FloatingActionButton_fab_interpolator, 0);
if(resId != 0)
mInterpolator = AnimationUtils.loadInterpolator(context, resId);
else if(mInterpolator == null)
mInterpolator = new DecelerateInterpolator();
a.recycle();
mBackground = new OvalShadowDrawable(radius, bgColor, elevation, elevation);
mBackground.setBounds(0, 0, getWidth(), getHeight());
if(iconLineMorphing != 0)
setIcon(new LineMorphingDrawable.Builder(context, iconLineMorphing).build(), false);
else if(iconSrc != 0)
setIcon(context.getResources().getDrawable(iconSrc), false);
getRippleManager().onCreate(this, context, attrs, defStyleAttr, defStyleRes);
Drawable background = getBackground();
if(background != null && background instanceof RippleDrawable){
RippleDrawable drawable = (RippleDrawable)background;
drawable.setBackgroundDrawable(null);
drawable.setMask(RippleDrawable.Mask.TYPE_OVAL, 0, 0, 0, 0, (int)mBackground.getPaddingLeft(), (int)mBackground.getPaddingTop(), (int)mBackground.getPaddingRight(), (int)mBackground.getPaddingBottom());
}
setClickable(true);
}
/**
* @return The radius of the button.
*/
public int getRadius(){
return mBackground.getRadius();
}
/**
* Set radius of the button.
* @param radius The radius in pixel.
*/
public void setRadius(int radius){
if(mBackground.setRadius(radius))
requestLayout();
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public float getElevation() {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
return super.getElevation();
return mBackground.getShadowSize();
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public void setElevation(float elevation) {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
super.setElevation(elevation);
else if(mBackground.setShadow(elevation, elevation))
requestLayout();
}
/**
* @return The line state of LineMorphingDrawable that is used as this button's icon.
*/
public int getLineMorphingState(){
if(mIcon != null && mIcon instanceof LineMorphingDrawable)
return ((LineMorphingDrawable)mIcon).getLineState();
return -1;
}
/**
* Set the line state of LineMorphingDrawable that is used as this button's icon.
* @param state The line state.
* @param animation Indicate should show animation when switch line state or not.
*/
public void setLineMorphingState(int state, boolean animation){
if(mIcon != null && mIcon instanceof LineMorphingDrawable)
((LineMorphingDrawable)mIcon).switchLineState(state, animation);
}
/**
* @return The background color of this button.
*/
public int getBackgroundColor(){
return mBackground.getColor();
}
/**
* @return The drawable is used as this button's icon.
*/
public Drawable getIcon(){
return mIcon;
}
/**
* Set the drawable that is used as this button's icon.
* @param icon The drawable.
* @param animation Indicate should show animation when switch drawable or not.
*/
public void setIcon(Drawable icon, boolean animation){
if(icon == null)
return;
if(animation) {
mSwitchIconAnimator.startAnimation(icon);
invalidate();
}
else{
if(mIcon != null){
mIcon.setCallback(null);
unscheduleDrawable(mIcon);
}
mIcon = icon;
float half = mIconSize / 2f;
mIcon.setBounds((int)(mBackground.getCenterX() - half), (int)(mBackground.getCenterY() - half), (int)(mBackground.getCenterX() + half), (int)(mBackground.getCenterY() + half));
mIcon.setCallback(this);
invalidate();
}
}
@Override
public void setBackgroundColor(int color){
mBackground.setColor(color);
invalidate();
}
/**
* Show this button at the specific location. If this button isn't attached to any parent view yet,
* it will be add to activity's root view. If not, it will just update the location.
* @param activity The activity that this button will be attached to.
* @param x The x value of anchor point.
* @param y The y value of anchor point.
* @param gravity The gravity apply with this button.
*
* @see android.view.Gravity
*/
public void show(Activity activity, int x, int y, int gravity){
if(getParent() == null){
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(mBackground.getIntrinsicWidth(), mBackground.getIntrinsicHeight());
updateParams(x, y, gravity, params);
activity.getWindow().addContentView(this, params);
}
else
updateLocation(x, y, gravity);
}
/**
* Show this button at the specific location. If this button isn't attached to any parent view yet,
* it will be add to activity's root view. If not, it will just update the location.
* @param parent The parent view. Should be {@link android.widget.FrameLayout} or {@link android.widget.RelativeLayout}
* @param x The x value of anchor point.
* @param y The y value of anchor point.
* @param gravity The gravity apply with this button.
*
* @see android.view.Gravity
*/
public void show(ViewGroup parent, int x, int y, int gravity){
if(getParent() == null){
ViewGroup.LayoutParams params = parent.generateLayoutParams(null);
params.width = mBackground.getIntrinsicWidth();
params.height = mBackground.getIntrinsicHeight();
updateParams(x, y, gravity, params);
parent.addView(this, params);
}
else
updateLocation(x, y, gravity);
}
/**
* Update the location of this button. This method only work if it's already attached to a parent view.
* @param x The x value of anchor point.
* @param y The y value of anchor point.
* @param gravity The gravity apply with this button.
*
* @see android.view.Gravity
*/
public void updateLocation(int x, int y, int gravity){
if(getParent() != null)
updateParams(x, y, gravity, getLayoutParams());
else
Log.v(FloatingActionButton.class.getSimpleName(), "updateLocation() is called without parent");
}
private void updateParams(int x, int y, int gravity, ViewGroup.LayoutParams params){
int horizontalGravity = gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
switch (horizontalGravity) {
case Gravity.LEFT:
setLeftMargin(params, (int)(x - mBackground.getPaddingLeft()));
break;
case Gravity.CENTER_HORIZONTAL:
setLeftMargin(params, (int)(x - mBackground.getCenterX()));
break;
case Gravity.RIGHT:
setLeftMargin(params, (int)(x - mBackground.getPaddingLeft() - mBackground.getRadius() * 2));
break;
default:
setLeftMargin(params, (int)(x - mBackground.getPaddingLeft()));
break;
}
int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
switch (verticalGravity) {
case Gravity.TOP:
setTopMargin(params, (int)(y - mBackground.getPaddingTop()));
break;
case Gravity.CENTER_VERTICAL:
setTopMargin(params, (int)(y - mBackground.getCenterY()));
break;
case Gravity.BOTTOM:
setTopMargin(params, (int)(y - mBackground.getPaddingTop() - mBackground.getRadius() * 2));
break;
default:
setTopMargin(params, (int)(y - mBackground.getPaddingTop()));
break;
}
setLayoutParams(params);
}
private void setLeftMargin(ViewGroup.LayoutParams params, int value){
if(params instanceof FrameLayout.LayoutParams)
((FrameLayout.LayoutParams)params).leftMargin = value;
else if(params instanceof RelativeLayout.LayoutParams)
((RelativeLayout.LayoutParams)params).leftMargin = value;
else
Log.v(FloatingActionButton.class.getSimpleName(), "cannot recognize LayoutParams: " + params);
}
private void setTopMargin(ViewGroup.LayoutParams params, int value){
if(params instanceof FrameLayout.LayoutParams)
((FrameLayout.LayoutParams)params).topMargin = value;
else if(params instanceof RelativeLayout.LayoutParams)
((RelativeLayout.LayoutParams)params).topMargin = value;
else
Log.v(FloatingActionButton.class.getSimpleName(), "cannot recognize LayoutParams: " + params);
}
/**
* Remove this button from parent view.
*/
public void dismiss(){
if(getParent() != null)
((ViewGroup)getParent()).removeView(this);
}
@Override
protected boolean verifyDrawable(Drawable who) {
return super.verifyDrawable(who) || mBackground == who || mIcon == who || mPrevIcon == who;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(mBackground.getIntrinsicWidth(), mBackground.getIntrinsicHeight());
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
mBackground.setBounds(0, 0, w, h);
if(mIcon != null){
float half = mIconSize / 2f;
mIcon.setBounds((int)(mBackground.getCenterX() - half), (int)(mBackground.getCenterY() - half), (int)(mBackground.getCenterX() + half), (int)(mBackground.getCenterY() + half));
}
if(mPrevIcon != null){
float half = mIconSize / 2f;
mPrevIcon.setBounds((int)(mBackground.getCenterX() - half), (int)(mBackground.getCenterY() - half), (int)(mBackground.getCenterX() + half), (int)(mBackground.getCenterY() + half));
}
}
@Override
public void draw(@NonNull Canvas canvas) {
mBackground.draw(canvas);
super.draw(canvas);
if(mPrevIcon != null)
mPrevIcon.draw(canvas);
if(mIcon != null)
mIcon.draw(canvas);
}
protected RippleManager getRippleManager(){
if(mRippleManager == null){
synchronized (RippleManager.class){
if(mRippleManager == null)
mRippleManager = new RippleManager();
}
}
return mRippleManager;
}
@Override
public void setOnClickListener(OnClickListener l) {
RippleManager rippleManager = getRippleManager();
if (l == rippleManager)
super.setOnClickListener(l);
else {
rippleManager.setOnClickListener(l);
setOnClickListener(rippleManager);
}
}
@Override
public boolean onTouchEvent(@NonNull MotionEvent event) {
int action = event.getActionMasked();
if(action == MotionEvent.ACTION_DOWN && ! mBackground.isPointerOver(event.getX(), event.getY()))
return false;
boolean result = super.onTouchEvent(event);
return getRippleManager().onTouchEvent(event) || result;
}
private class OvalShadowDrawable extends Drawable{
private Paint mShadowPaint;
private Paint mGlowPaint;
private Paint mPaint;
private int mRadius;
private float mShadowSize;
private float mShadowOffset;
private Path mShadowPath;
private Path mGlowPath;
private RectF mTempRect = new RectF();
private int mColor;
private boolean mNeedBuildShadow = true;
private static final int COLOR_SHADOW_START = 0x4C000000;
private static final int COLOR_SHADOW_END = 0x00000000;
public OvalShadowDrawable(int radius, int color, float shadowSize, float shadowOffset){
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mPaint.setStyle(Paint.Style.FILL);
setColor(color);
setRadius(radius);
setShadow(shadowSize, shadowOffset);
}
public boolean setRadius(int radius){
if(mRadius != radius){
mRadius = radius;
mNeedBuildShadow = true;
invalidateSelf();
return true;
}
return false;
}
public boolean setShadow(float size, float offset){
if(mShadowSize != size || mShadowOffset != offset){
mShadowSize = size;
mShadowOffset = offset;
mNeedBuildShadow = true;
invalidateSelf();
return true;
}
return false;
}
public void setColor(int color){
if(mColor != color){
mColor = color;
mPaint.setColor(mColor);
invalidateSelf();
}
}
public int getColor(){
return mColor;
}
public int getRadius(){
return mRadius;
}
public float getShadowSize(){
return mShadowSize;
}
public float getShadowOffset(){
return mShadowOffset;
}
public float getPaddingLeft(){
return mShadowSize;
}
public float getPaddingTop(){
return mShadowSize;
}
public float getPaddingRight(){
return mShadowSize;
}
public float getPaddingBottom(){
return mShadowSize + mShadowOffset;
}
public float getCenterX(){
return mRadius + mShadowSize;
}
public float getCenterY(){
return mRadius + mShadowSize;
}
public boolean isPointerOver(float x, float y){
float distance = (float)Math.sqrt(Math.pow(x - getCenterX(), 2) + Math.pow(y - getCenterY(), 2));
return distance < mRadius;
}
@Override
public int getIntrinsicWidth() {
return (int)((mRadius + mShadowSize) * 2 + 0.5f);
}
@Override
public int getIntrinsicHeight() {
return (int)((mRadius + mShadowSize) * 2 + mShadowOffset + 0.5f);
}
private void buildShadow(){
if(mShadowSize <= 0)
return;
if(mShadowPaint == null){
mShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mShadowPaint.setStyle(Paint.Style.FILL);
mShadowPaint.setDither(true);
}
float startRatio = (float)mRadius / (mRadius + mShadowSize + mShadowOffset);
mShadowPaint.setShader(new RadialGradient(0, 0, mRadius + mShadowSize,
new int[]{COLOR_SHADOW_START, COLOR_SHADOW_START, COLOR_SHADOW_END},
new float[]{0f, startRatio, 1f}
, Shader.TileMode.CLAMP));
if(mShadowPath == null){
mShadowPath = new Path();
mShadowPath.setFillType(Path.FillType.EVEN_ODD);
}
else
mShadowPath.reset();
float radius = mRadius + mShadowSize;
mTempRect.set(-radius, -radius, radius, radius);
mShadowPath.addOval(mTempRect, Path.Direction.CW);
radius = mRadius - 1;
mTempRect.set(-radius, -radius - mShadowOffset, radius, radius - mShadowOffset);
mShadowPath.addOval(mTempRect, Path.Direction.CW);
if(mGlowPaint == null){
mGlowPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mGlowPaint.setStyle(Paint.Style.FILL);
mGlowPaint.setDither(true);
}
startRatio = (mRadius - mShadowSize / 2f) / (mRadius + mShadowSize / 2f);
mGlowPaint.setShader(new RadialGradient(0, 0, mRadius + mShadowSize / 2f,
new int[]{COLOR_SHADOW_START, COLOR_SHADOW_START, COLOR_SHADOW_END},
new float[]{0f, startRatio, 1f}
, Shader.TileMode.CLAMP));
if(mGlowPath == null){
mGlowPath = new Path();
mGlowPath.setFillType(Path.FillType.EVEN_ODD);
}
else
mGlowPath.reset();
radius = mRadius + mShadowSize / 2f;
mTempRect.set(-radius, -radius, radius, radius);
mGlowPath.addOval(mTempRect, Path.Direction.CW);
radius = mRadius - 1;
mTempRect.set(-radius, -radius, radius, radius);
mGlowPath.addOval(mTempRect, Path.Direction.CW);
}
@Override
public void draw(Canvas canvas) {
if(mNeedBuildShadow){
buildShadow();
mNeedBuildShadow = false;
}
int saveCount;
if(mShadowSize > 0){
saveCount = canvas.save();
canvas.translate(mShadowSize + mRadius, mShadowSize + mRadius + mShadowOffset);
canvas.drawPath(mShadowPath, mShadowPaint);
canvas.restoreToCount(saveCount);
}
saveCount = canvas.save();
canvas.translate(mShadowSize + mRadius, mShadowSize + mRadius);
if(mShadowSize > 0)
canvas.drawPath(mGlowPath, mGlowPaint);
mTempRect.set(-mRadius, -mRadius, mRadius, mRadius);
canvas.drawOval(mTempRect, mPaint);
canvas.restoreToCount(saveCount);
}
@Override
public void setAlpha(int alpha) {
mShadowPaint.setAlpha(alpha);
mPaint.setAlpha(alpha);
}
@Override
public void setColorFilter(ColorFilter cf) {
mShadowPaint.setColorFilter(cf);
mPaint.setColorFilter(cf);
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
}
@Override
protected Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
ss.state = getLineMorphingState();
return ss;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
if(ss.state >= 0)
setLineMorphingState(ss.state, false);
requestLayout();
}
static class SavedState extends BaseSavedState {
int state;
/**
* Constructor called from {@link Slider#onSaveInstanceState()}
*/
SavedState(Parcelable superState) {
super(superState);
}
/**
* Constructor called from {@link #CREATOR}
*/
private SavedState(Parcel in) {
super(in);
state = in.readInt();
}
@Override
public void writeToParcel(@NonNull Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeInt(state);
}
@Override
public String toString() {
return "FloatingActionButton.SavedState{"
+ Integer.toHexString(System.identityHashCode(this))
+ " state=" + state + "}";
}
public static final Creator<SavedState> CREATOR
= new Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
class SwitchIconAnimator implements Runnable{
boolean mRunning = false;
long mStartTime;
public void resetAnimation(){
mStartTime = SystemClock.uptimeMillis();
mIcon.setAlpha(0);
mPrevIcon.setAlpha(255);
}
public boolean startAnimation(Drawable icon) {
if(mIcon == icon)
return false;
mPrevIcon = mIcon;
mIcon = icon;
float half = mIconSize / 2f;
mIcon.setBounds((int)(mBackground.getCenterX() - half), (int)(mBackground.getCenterY() - half), (int)(mBackground.getCenterX() + half), (int)(mBackground.getCenterY() + half));
mIcon.setCallback(FloatingActionButton.this);
if(getHandler() != null){
resetAnimation();
mRunning = true;
getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION);
}
else {
mPrevIcon.setCallback(null);
unscheduleDrawable(mPrevIcon);
mPrevIcon = null;
}
invalidate();
return true;
}
public void stopAnimation() {
mRunning = false;
mPrevIcon.setCallback(null);
unscheduleDrawable(mPrevIcon);
mPrevIcon = null;
mIcon.setAlpha(255);
if(getHandler() != null)
getHandler().removeCallbacks(this);
invalidate();
}
@Override
public void run() {
long curTime = SystemClock.uptimeMillis();
float progress = Math.min(1f, (float)(curTime - mStartTime) / mAnimDuration);
float value = mInterpolator.getInterpolation(progress);
mIcon.setAlpha(Math.round(255 * value));
mPrevIcon.setAlpha(Math.round(255 * (1f - value)));
if(progress == 1f)
stopAnimation();
if(mRunning) {
if(getHandler() != null)
getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION);
else
stopAnimation();
}
invalidate();
}
}
}