package com.werb.gankwithzhihu.widget;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewManager;
import android.view.ViewParent;
import android.view.animation.OvershootInterpolator;
import com.werb.gankwithzhihu.BuildConfig;
import com.werb.gankwithzhihu.R;
/**
* A simple view class that will display an enlarging icon animation. For best results the provided icon should have a
* solid, mono-color background with a transparent hole where the icon's silhouette is supposed to be
* @author yildizkabaran
*
*/
public class SplashView extends View {
private static final String TAG = "SplashView";
/**
* A simple interface to listen to the state of the splash animation
* @author yildizkabaran
*
*/
public static interface ISplashListener {
public void onStart();
public void onUpdate(float completionFraction);
public void onEnd();
}
/**
* Context constructor
* @param context
*/
public SplashView(Context context){
super(context);
initialize();
}
/**
* Context and attributes constructor
* @param context
* @param attrs
*/
public SplashView(Context context, AttributeSet attrs) {
super(context, attrs);
initialize();
setupAttributes(attrs);
}
/**
* Context, attributes, and style constructor
* @param context
* @param attrs
*/
public SplashView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize();
setupAttributes(attrs);
}
public static final int DEFAULT_HOLE_FILL_COLOR = Color.WHITE;
public static final int DEFAULT_ICON_COLOR = Color.rgb(23, 169, 229);
public static final int DEFAULT_DURATION = 500;
public static final boolean DEFAULT_REMOVE_FROM_PARENT_ON_END = true;
private static final int PAINT_STROKE_WIDTH = 2; // give a stroke width to the paint so that the rectangles get a little overlap
private Drawable mIcon; // most important item, cannot be null
private int mHoleFillColor = DEFAULT_HOLE_FILL_COLOR; // color to be shown in the transparent hole before the animation starts
private int mIconColor = DEFAULT_ICON_COLOR; // should be the same color of as the icon background
private long mDuration = DEFAULT_DURATION; // total duration, in ms, of the animation
private boolean mRemoveFromParentOnEnd = true; // a flag for removing the view from its parent once the animation is over
private float mCurrentScale = 1; // used for keeping track of how far along the animation we are
// cache some dimension values to make the onDraw method simpler looking
private int mWidth, mHeight;
private int mIconWidth, mIconHeight;
private float mMaxScale = 1;
// cache the paint object so that it doesn't need to be allocated in onDraw
private Paint mPaint = new Paint();
/**
* Setup custom attributes from XML
* @param attrs
*/
private void setupAttributes(AttributeSet attrs) {
Context context = getContext();
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SplashView);
int numAttrs = a.getIndexCount();
for (int i = 0; i < numAttrs; ++i) {
int attr = a.getIndex(i);
switch (attr) {
case R.styleable.SplashView_splashIcon:
setIconDrawable(a.getDrawable(i));
break;
case R.styleable.SplashView_iconColor:
setIconColor(a.getColor(i, DEFAULT_ICON_COLOR));
break;
case R.styleable.SplashView_holeFillColor:
setHoleFillColor(a.getColor(i, DEFAULT_HOLE_FILL_COLOR));
break;
case R.styleable.SplashView_duration:
setDuration(a.getInt(i, DEFAULT_DURATION));
break;
case R.styleable.SplashView_removeFromParentOnEnd:
setRemoveFromParentOnEnd(a.getBoolean(i, DEFAULT_REMOVE_FROM_PARENT_ON_END));
break;
}
}
a.recycle();
}
/**
* Initialized the view properties. No much is done in this method since most variables already have set defaults
*/
private void initialize(){
// make the background transparent so that the view does not automatically draw any unwanted colors
setBackgroundColor(Color.TRANSPARENT);
// set fill style on the paint so that the rectangles get filled
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mPaint.setStrokeWidth(PAINT_STROKE_WIDTH);
}
/**
* Set the fill color of the view that will be seen through the transparent hole of the icon before the animation starts
*/
public void setHoleFillColor(int bgColor){
mHoleFillColor = bgColor;
}
/**
* Set the color of the icon. This value will be used to draw 4 rectangles around the icon to fill the entire view.
* If not set, or set incorrectly the edges of the icon image will be visible. There are a few tricks to make this animation
* look right, and this is one of them. Make sure this color is set correctly.
* @param iconColor
*/
public void setIconColor(int iconColor){
mIconColor = iconColor;
}
/**
* Set the duration of the entire animation in milliseconds.
* @param duration
*/
public void setDuration(long duration){
if(duration < 0){
throw new IllegalArgumentException("duration cannot be less than 0");
}
mDuration = duration;
}
/**
* Set the resource id of the Drawable to be used as the icon. See setIconDrawable(Drawable) for more details.
* @param resId
*/
public void setIconResource(int resId){
Drawable icon = getResources().getDrawable(resId);
if(icon == null){
throw new IllegalArgumentException("no drawable found for the resId: " + resId);
}
setIconDrawable(icon);
}
/**
* Set the Drawable to be used as the icon. It can be any kind of Drawable that has an intrinsic width and height.
* So far changing the size of the Drawable is not supported but can be added in the future
* @param icon
*/
public void setIconDrawable(Drawable icon){
mIcon = icon;
if(mIcon != null){
mIconWidth = mIcon.getIntrinsicWidth();
mIconHeight = mIcon.getIntrinsicHeight();
// set the bounds of the drawable to its own dimensions
// canvas scaling will be used to change the bounds of the icon
Rect iconBounds = new Rect();
iconBounds.left = 0;
iconBounds.top = 0;
iconBounds.right = mIconWidth;
iconBounds.bottom = mIconHeight;
mIcon.setBounds(iconBounds);
} else {
mIconWidth = 0;
mIconHeight = 0;
}
setMaxScale();
}
/**
* Set the flag to remove or keep the view after the animation is over. This is set to true by default. The view must be inside a ViewManager
* (or ViewParent) for this to work. Otherwise, the view will not be removed and a warning log will be produced.
* @param shouldRemove
*/
public void setRemoveFromParentOnEnd(boolean shouldRemove){
mRemoveFromParentOnEnd = shouldRemove;
}
/**
* A helper method for determining for large the icon should be enlarged before the animation ends. There is a chance that the entire view will not become
* transparent by the end of the animation.
*/
private void setMaxScale(){
if(mIconWidth < 1 || mIconHeight < 1){
mMaxScale = 1;
return;
}
mMaxScale = 2 * Math.max((float) mWidth/mIconWidth, (float) mHeight/mIconHeight);
// just to make sure the animation does not actually work backwards
if(mMaxScale < 1){
mMaxScale = 1;
}
}
/**
* Starts the splash and disappear animation. If a listener is provided it will notify the listener on animation events
* @param listener
*/
public void splashAndDisappear(final ISplashListener listener){
// create an animator from scale 1 to max
final ValueAnimator animator = ValueAnimator.ofFloat(1, mMaxScale);
// set the duration
animator.setDuration(mDuration);
// set an overshoot interpolator with a low tension value so that the icon becomes a little smaller before it expands
animator.setInterpolator(new OvershootInterpolator(1F));
// add an update listener so that we draw the view on each update
animator.addUpdateListener(new AnimatorUpdateListener() {
@SuppressLint("NewApi")
@Override
public void onAnimationUpdate(ValueAnimator animation) {
// keep in mind that the animation runs in reverse to get the desired effect from the interpolator
// therefore we need to subtract to correct for this effect, and then add 1 so that the scale doesn't dip below 0
// this is NOT fool-proof, and therefore can be made better
mCurrentScale = 1 + mMaxScale - (Float) animation.getAnimatedValue();
// invalidate the view so that it gets redraw if it needs to be
invalidate();
// notify the listener if set
// for some reason this animation can run beyond 100%
if(listener != null){
listener.onUpdate((float) animation.getCurrentPlayTime() / mDuration);
}
}
});
// add a listener for the general animation events, use the AnimatorListenerAdapter so that we don't clutter the code
animator.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationStart(Animator animation){
// notify the listener of animation start (if listener is set)
if(listener != null){
listener.onStart();
}
}
@Override
public void onAnimationEnd(Animator animation){
// check if we need to remove the view on animation end
if(mRemoveFromParentOnEnd){
// get the view parent
ViewParent parent = getParent();
// check if a parent exists and that it implements the ViewManager interface
if(parent != null && parent instanceof ViewManager){
ViewManager viewManager = (ViewManager) parent;
// remove the view from its parent
viewManager.removeView(SplashView.this);
} else if(BuildConfig.DEBUG) {
// even though we had to remove the view we either don't have a parent, or the parent does not implement the method
// necessary to remove the view, therefore create a warning log (but only do this if we are in DEBUG mode)
Log.w(TAG, "splash view not removed after animation ended because no ViewManager parent was found");
}
}
// notify the listener of animation end (if listener is set)
if(listener != null){
listener.onEnd();
}
}
});
// start the animation using post so that the animation does not start if the view is not in foreground
post(new Runnable(){
@Override
public void run(){
// start the animation in reverse to get the desired effect from the interpolator
animator.reverse();
}
});
}
@Override
protected void onSizeChanged (int w, int h, int oldw, int oldh) {
// do whatever the super wants to do
super.onSizeChanged(w, h, oldw, oldh);
// cache the width and height for easy access
mWidth = w;
mHeight = h;
// re-set the max scale because the size has changed
setMaxScale();
}
@Override
protected void onDraw(Canvas canvas){
// calculate the scaled width and height
float iconWidth = mIconWidth * mCurrentScale;
float iconHeight = mIconHeight * mCurrentScale;
// calculate all corners of the icon rectangle with the icon in the middle
float mIconLeft = (mWidth - iconWidth) / 2;
float mIconRight = mIconLeft + iconWidth;
float mIconTop = (mHeight - iconHeight) / 2;
float mIconBottom = mIconTop + iconHeight;
// if the scale is less than 2, then don't enable the transparent hole yet
if(mCurrentScale < 2){
// draw a bgColored rectangle right underneath the icon, make the rectangle a little bigger using the threshold value
mPaint.setColor(mHoleFillColor);
canvas.drawRect(mIconLeft, mIconTop, mIconRight, mIconBottom, mPaint);
}
// draw 4 rectangles around the icon to cover the entire screen, use threshold value to expand and overlap the rectangles
mPaint.setColor(mIconColor);
canvas.drawRect(0, 0, mIconLeft, mHeight, mPaint);
canvas.drawRect(mIconLeft, 0, mIconRight, mIconTop, mPaint);
canvas.drawRect(mIconLeft, mIconBottom, mIconRight, mHeight, mPaint);
canvas.drawRect(mIconRight, 0, mWidth, mHeight, mPaint);
if(mIcon != null){
// save the current canvas state
canvas.save();
// translate the canvas to draw the icon
canvas.translate(mIconLeft, mIconTop);
// scale the canvas for the desired icon scale
canvas.scale(mCurrentScale, mCurrentScale);
// draw the icon on the canvas
mIcon.draw(canvas);
// restore the canvas to its original state
canvas.restore();
} else if(BuildConfig.DEBUG){
// if the icon is not set then log a warning message if we are in debug mode, this message will be logged every time onDraw is called
Log.w(TAG, "icon is not set when the view needs to be drawn");
}
}
}