/* * Copyright (C) 2014 Sergej Shafarenka, halfbit.de * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package de.halfbit.fabuless; import android.animation.Animator; import android.animation.Animator.AnimatorListener; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Paint.Style; import android.graphics.Path; import android.graphics.Path.Direction; import android.graphics.PointF; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.shapes.OvalShape; import android.os.Build; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver.OnGlobalLayoutListener; import android.view.ViewTreeObserver.OnPreDrawListener; import android.view.animation.AccelerateInterpolator; import android.widget.ImageView; public class FabView extends ImageView { private static final int TOP_LEFT = 1; private static final int TOP_RIGHT = 2; private static final int BOTTOM_LEFT = 3; private static final int BOTTOM_RIGHT = 4; private static final int NORMAL = 1; private static final int SMALL = 2; private static final int BORDER = 1; private static final int INSIDE = 2; protected ShapeDrawable mBackgroundDrawable; protected int mFabAttachTo; protected int mFabAttachAt; protected int mFabAttachType; protected int mFabSize; protected int mFabAttachPadding; protected int mFabRevealAfterMs; protected int mBackgroundColor; protected int mBackgroundColorDarker; protected int mShadowOffset; protected int mFabRadius; protected View mAttachedToView; private FabRevealer mFabRevealer; private TouchSpotAnimator mTouchSpotAnimator; //-- constructors public FabView(Context context) { this(context, null, 0); } public FabView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public FabView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initializeView(context, attrs); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public FabView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); initializeView(context, attrs); } private void initializeView(Context context, AttributeSet attrs) { setScaleType(ScaleType.CENTER_INSIDE); // http://www.google.com/design/spec/patterns/promoted-actions.html#promoted-actions-floating-action-button final float density = getResources().getDisplayMetrics().density; final TypedArray styles = context.obtainStyledAttributes(attrs, R.styleable.FabView, 0, 0); mFabAttachTo = styles.getResourceId(R.styleable.FabView_fabuless_attachTo, 0); mFabAttachAt = styles.getInt(R.styleable.FabView_fabuless_attachAt, TOP_RIGHT); mFabAttachType = styles.getInt(R.styleable.FabView_fabuless_attachType, BORDER); mFabSize = styles.getInt(R.styleable.FabView_fabuless_size, NORMAL); mFabAttachPadding = (int) styles.getDimension(R.styleable.FabView_fabuless_padding, 16 * density); mFabRevealAfterMs = styles.getInteger(R.styleable.FabView_fabuless_revealAfterMs, -1); styles.recycle(); switch (mFabSize) { case SMALL: mShadowOffset = (int) (2 * density); mFabRadius = (int) (20 * density); break; case NORMAL: default: mShadowOffset = (int) (3 * density); mFabRadius = (int) (28 * density); break; } mBackgroundDrawable = new ShapeDrawable(new OvalShape()); final Paint paint = mBackgroundDrawable.getPaint(); paint.setShadowLayer(mShadowOffset, 0f, 0f * density, 0x60000000); paint.setColor(mBackgroundColor); setLayerType(LAYER_TYPE_SOFTWARE, paint); mTouchSpotAnimator = new TouchSpotAnimator(); mFabRevealer = new FabRevealer(); if (mFabRevealAfterMs > -1 && getVisibility() == VISIBLE) { setVisibility(INVISIBLE); getViewTreeObserver().addOnPreDrawListener(mFabRevealer); } } //-- overrides @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final int size = 2 * mShadowOffset + 2 * mFabRadius; final int sizeSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY); super.onMeasure(sizeSpec, sizeSpec); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); mBackgroundDrawable.setBounds(left + mShadowOffset, top + mShadowOffset, right - mShadowOffset, bottom - mShadowOffset); mTouchSpotAnimator.layout(left, top, right, bottom); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); setPivotX(w / 2); setPivotY(h / 2); } private OnGlobalLayoutListener mLayoutListener = new OnGlobalLayoutListener() { @Override public void onGlobalLayout() { if (mAttachedToView == null) { ViewGroup parent = (ViewGroup) getParent(); mAttachedToView = parent.findViewById(mFabAttachTo); if (mAttachedToView == null) { throw new IllegalArgumentException("cannot find view to attach"); } } int transY, transX; if (mFabAttachType == BORDER) { // put to border switch (mFabAttachAt) { case TOP_LEFT: { transY = mAttachedToView.getTop() - getHeight() / 2; transX = mAttachedToView.getLeft() + mFabAttachPadding - mShadowOffset; break; } case TOP_RIGHT: { transY = mAttachedToView.getTop() - getHeight() / 2; transX = mAttachedToView.getRight() - getWidth() - mFabAttachPadding + mShadowOffset; break; } case BOTTOM_LEFT: { transY = mAttachedToView.getBottom() - getHeight() / 2; transX = mAttachedToView.getLeft() + mFabAttachPadding - mShadowOffset; break; } case BOTTOM_RIGHT: default: { transY = mAttachedToView.getBottom() - getHeight() / 2; transX = mAttachedToView.getRight() - getWidth() - mFabAttachPadding + mShadowOffset; break; } } } else if (mFabAttachType == INSIDE) { // put inside switch (mFabAttachAt) { case TOP_LEFT: { transY = mAttachedToView.getTop() + mFabAttachPadding - mShadowOffset; transX = mAttachedToView.getLeft() + mFabAttachPadding - mShadowOffset; break; } case TOP_RIGHT: { transY = mAttachedToView.getTop() + mFabAttachPadding - mShadowOffset; transX = mAttachedToView.getRight() - getHeight() - mFabAttachPadding + mShadowOffset; break; } case BOTTOM_LEFT: { transY = mAttachedToView.getBottom() - getHeight() - mFabAttachPadding + mShadowOffset; transX = mAttachedToView.getLeft() + mFabAttachPadding - mShadowOffset; break; } case BOTTOM_RIGHT: default: { transY = mAttachedToView.getBottom() - getHeight() - mFabAttachPadding + mShadowOffset; transX = mAttachedToView.getRight() - getHeight() - mFabAttachPadding + mShadowOffset; break; } } } else { throw new IllegalArgumentException("unsupported attachType: " + mFabAttachType); } setY(transY); setX(transX); } }; @Override protected void onDraw(Canvas canvas) { mBackgroundDrawable.draw(canvas); mTouchSpotAnimator.draw(canvas); super.onDraw(canvas); } @Override public boolean onTouchEvent(MotionEvent event) { mTouchSpotAnimator.onTouchEvent(event); return super.onTouchEvent(event); } @Override public void setBackgroundColor(int color) { mBackgroundColor = color; mBackgroundColorDarker = getDarkerColor(color); if (mBackgroundDrawable != null) { mBackgroundDrawable.getPaint().setColor(color); } invalidate(); } @Override public void setBackgroundDrawable(Drawable background) { if (background instanceof ColorDrawable) { ColorDrawable cd = (ColorDrawable) background; setBackgroundColor(cd.getColor()); } else { throw new UnsupportedOperationException("only color drawables are supported for now"); } } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); getViewTreeObserver().addOnGlobalLayoutListener(mLayoutListener); } @Override @SuppressWarnings("deprecation") protected void onDetachedFromWindow() { getViewTreeObserver().removeGlobalOnLayoutListener(mLayoutListener); super.onDetachedFromWindow(); } public void setVisibleAnimated(boolean visible) { if (visible) { mFabRevealer.show(true); } else { mFabRevealer.hide(true); } } //-- inner classes protected class FabRevealer implements Runnable, OnPreDrawListener { public void show(boolean animate) { if (getVisibility() == VISIBLE) { return; } if (animate) { setScaleX(0f); setScaleY(0f); setVisibility(VISIBLE); animate() .setInterpolator(new AccelerateInterpolator()) .setDuration(300) .scaleX(1f) .scaleY(1f); } else { setVisibility(VISIBLE); } } public void hide(final boolean animate) { if (getVisibility() != VISIBLE) { return; } if (animate) { animate() .setInterpolator(new AccelerateInterpolator()) .setDuration(300) .scaleX(0f) .scaleY(0f) .setListener(new AnimatorListener() { @Override public void onAnimationEnd(Animator animation) { setScaleX(1f); setScaleY(1f); setVisibility(INVISIBLE); animate().setListener(null); } @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } @Override public void onAnimationCancel(Animator animation) { } }); } else { setVisibility(INVISIBLE); } } //-- utility methods @Override public void run() { show(true); } @Override public boolean onPreDraw() { getHandler().postDelayed(mFabRevealer, mFabRevealAfterMs); getViewTreeObserver().removeOnPreDrawListener(this); return true; } } protected class TouchSpotAnimator implements AnimatorUpdateListener, AnimatorListener { private static final int ANIM_DURATION = 300; private final PointF mTouchPoint = new PointF(); private final Paint mTouchSpotPaint = new Paint(); private final Path mClipPath = new Path(); private final int mTargetRadius; private final int mTouchSpotColor; private int mTouchSpotRadius = 0; private ValueAnimator mTouchSpotAnimator; public TouchSpotAnimator() { mTouchSpotColor = 0x00000000; mTouchSpotPaint.setColor(mTouchSpotColor); mTouchSpotPaint.setStyle(Style.FILL); mTargetRadius = mFabRadius * 5 / 3; } public void layout(int left, int top, int right, int bottom) { mClipPath.reset(); mClipPath.addCircle((right - left) / 2, (bottom - top) / 2, mFabRadius, Direction.CW); } public void onTouchEvent(MotionEvent event) { final int action = event.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: mTouchPoint.x = event.getX(); mTouchPoint.y = event.getY(); break; case MotionEvent.ACTION_UP: animate(); break; } } public void draw(Canvas canvas) { if (mTouchSpotRadius > 0) { canvas.save(); canvas.clipPath(mClipPath); canvas.drawCircle(mTouchPoint.x, mTouchPoint.y, mTouchSpotRadius, mTouchSpotPaint); canvas.restore(); } } public void animate() { if (mTouchSpotAnimator == null) { mTouchSpotAnimator = new ValueAnimator(); mTouchSpotAnimator.setInterpolator(new AccelerateInterpolator()); mTouchSpotAnimator.addUpdateListener(this); mTouchSpotAnimator.addListener(this); } else { if (mTouchSpotAnimator.isRunning()) { mTouchSpotAnimator.cancel(); } } mTouchSpotAnimator.setFloatValues(0.2f, 1f); mTouchSpotAnimator.setDuration(ANIM_DURATION); mTouchSpotAnimator.start(); } @Override public void onAnimationUpdate(ValueAnimator animation) { final float factor = animation.getAnimatedFraction(); mTouchSpotRadius = (int) (mTargetRadius * factor); mTouchSpotPaint.setColor(transformAlpha(mTouchSpotColor, 0x00, 0x38, factor)); mBackgroundDrawable.getPaint().setColor(transformColor(mBackgroundColorDarker, mBackgroundColor, factor)); invalidate(); } @Override public void onAnimationEnd(Animator animation) { mTouchSpotRadius = 0; } @Override public void onAnimationCancel(Animator animation) { mTouchSpotRadius = 0; } @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } } //-- utility methods protected static int transformColor(int fromColor, int toColor, float factor) { final float defactor = 1f - factor; final int alpha = (int) (defactor * Color.alpha(fromColor) + factor * Color.alpha(toColor)); final int red = (int) (defactor * Color.red(fromColor) + factor * Color.red(toColor)); final int green = (int) (defactor * Color.green(fromColor) + factor * Color.green(toColor)); final int blue = (int) (defactor * Color.blue(fromColor) + factor * Color.blue(toColor)); return Color.argb(alpha, red, green, blue); } protected static int transformAlpha(int color, int fromAlpha, int toAlpha, float factor) { final float defactor = 1f - factor; final int alpha = (int) (factor * fromAlpha + defactor * toAlpha); return setAlpha(color, alpha); } protected static int setAlpha(int color, int alpha) { return ((color & 0x00FFFFFF) | (alpha << 24)); } protected static int getDarkerColor(int color) { float[] hsv = new float[3]; Color.colorToHSV(color, hsv); hsv[2] *= 0.85f; return Color.HSVToColor(hsv); } }