Android动画篇(四):最终效果篇CircleProgressSuperBar

前言

今天终于有时间把最后的成果分享给大家了,为了提高一下博客的逼格,我也找了一个专门做原型、导图的在线网站:processon(www.processon.com),这个工具真的很棒,也很方便,这里给他点个赞。

CircleProgressSuperBar为了完成最终的效果,我也是踩了一些坑,今天把我总结的最清晰的思路分享给大家,首先我们回顾一下我们的效果图:

Android动画篇(四):最终效果篇CircleProgressSuperBar_第1张图片

正文

首先我们来分析一下,CircleProgressSuperBar总共分为几种状态:

Android动画篇(四):最终效果篇CircleProgressSuperBar_第2张图片

这四种状态,我们不关心是怎么切换的,我们只关心动画是怎么过渡的。

首先我们知道准备一些类:

1、CircleProgressSuperBar,这个是主类,也是最终要完成的view。
2、四个状态的Drawable,我们分别命名为:NormalDrawable、LoadingDrawable、CompleteDrawable和ErrorDrawable。

最终为了方便扩展和解耦,我最终实现的架构图是这样的:

Android动画篇(四):最终效果篇CircleProgressSuperBar_第3张图片

我的主要目的:

1、在View和Drawable之间创建工厂类,即能降低类之间的耦合,也可以方便扩展更多状态的Drawable。

2、各种状态Drawble的基类BaseStatusDrawable,封装公共的属性和方法,让View直接使用BaseStatusDrawable类型,而不去具体关心具体的Drawable的实现。

3、BaseStatusDrawable内部带有样式的信息,防止和内部的画笔有关的颜色弄混。

那我们就从最基础的部分,首先新建CircleProgressSuperInfo:

public class CircleProgressSuperInfo {

    /**
     * 宽, 在设置动画的时候需要知道宽
     */
    private int mWidth;

    /**
     * 高,在设置动画的时候需要知道高
     */
    private int mHeight;

    /**
     * 圆角
     */
    private int mRadius;

    /**
     * 背景颜色
     */
    private int mBgColor;

    /**
     * 边框颜色
     */
    private int mBorderColor;

    /**
     * 边框的宽度
     */
    private int mBorderWidth;

    /**
     * 最大的间距
     * */
    private float mPadding;

    public CircleProgressSuperInfo(int bgColor, int borderColor, int borderWidth) {
        this.mBgColor = bgColor;
        this.mBorderColor = borderColor;
        this.mBorderWidth = borderWidth;
    }

    ... 
    // 此处省略setter和getter方法
}

然后就是需要BaseStatusDrawable,我们直接把之前写好的形状变化的ChangeShapeAndColorButton进行改造,变成我们需要的BaseStatusDrawable:

/**
 * Created by li.zhipeng on 2017/7/12.
 * 

* 所有的状态图片需要实现此接口 */ public abstract class BaseStatusDrawable extends Drawable { protected CircleProgressSuperInfo mInfo; /** * 宽, 在设置动画的时候需要知道宽 */ protected int mWidth; /** * 高,在设置动画的时候需要知道高 */ protected int mHeight; /** * 圆角 */ protected float mRadius; /** * 文字 */ protected String mText; /** * 文字颜色 */ protected int mTextColor = Color.parseColor("#ffffff"); /** * 文字大小 */ protected int mTextSize; /** * 画笔 */ protected Paint mPaint; /** * 形状 */ protected RectF mRectF; /** * 背景颜色 */ protected int mBgColor; /** * 边框颜色 */ protected int mBorderColor; /** * 边框的宽度 */ protected int mBorderWidth; /** * 偏移值,也就是大小要发生的变化值 */ protected float mPadding; /** * 最小大小 */ protected float mMinSize = -1; /** * 正在动画在中 */ protected boolean isAnim; public BaseStatusDrawable(CircleProgressSuperInfo info) { this.mInfo = info; // 初始化画笔 mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setDither(true); mRectF = new RectF(); } // 此处省略各种setter和getter方法 ... public CircleProgressSuperInfo getInfo(){return this.mInfo;}; /** * 回到后台,调用此方法 */ public abstract void release(); /** * 绘制 */ @Override public void draw(@NonNull Canvas canvas) { // 这是绘制过渡动画 if (isAnim()) { drawTransition(canvas); } // 绘制正常状态,例如loading就要使用绘制旋转的圆圈 else { drawSelf(canvas); } } /** * 绘制过度动画 */ protected void drawTransition(Canvas canvas) { // 先画出背景,背景是居中的 // 判断宽高 int width = getWidth(); int height = getHeight(); // 计算左右的间距值,并且判断不能小于minSize float paddingLR = width - mPadding * 2 < mMinSize ? (width - mMinSize) / 2 : mPadding; float paddingTB = height - mPadding * 2 < mMinSize ? (height - mMinSize) / 2 : mPadding; // 绘制描边 mRectF.set(paddingLR + mBorderWidth / 2, paddingTB + mBorderWidth / 2, getWidth() - paddingLR - mBorderWidth / 2, getHeight() - paddingTB - mBorderWidth / 2); // 开始画后面的背景 mPaint.setStyle(Paint.Style.FILL); mPaint.setColor(mBgColor); canvas.drawRoundRect(mRectF, mRadius, mRadius, mPaint); mPaint.setStyle(Paint.Style.STROKE); mPaint.setColor(mBorderColor); mPaint.setStrokeWidth(mBorderWidth); canvas.drawRoundRect(mRectF, mRadius, mRadius, mPaint); // 居中绘制文字 if (!TextUtils.isEmpty(mText)) { float textDescent = mPaint.getFontMetrics().descent; float textAscent = mPaint.getFontMetrics().ascent; float delta = Math.abs(textAscent) - textDescent; mPaint.setColor(mTextColor); mPaint.setTextSize(mTextSize); float textWidth = mPaint.measureText(mText); canvas.drawText(mText, (width - textWidth) / 2, height / 2 + delta / 2, mPaint); } } /** * 绘制正常状态 */ public abstract void drawSelf(Canvas canvas); @Override public void setAlpha(@IntRange(from = 0, to = 255) int i) { } @Override public void setColorFilter(@Nullable ColorFilter colorFilter) { } @Override public int getOpacity() { return PixelFormat.TRANSLUCENT; } }

这里要强调几点:

1、继承Drawable的原因:主要是为了重绘,例如之后的加载状态,是需要不断重绘的才会有转圈的动画,但是已经和View分离了,就无法借助View.invaliate(),所以这里继承了Drawable。

2、drawSelf()是绘制非过渡动画的状态,这里主要是给LoadingDrawable用的。

3、getOpacity()方法的作用:返回图片的质量类型。

为了理解getOpacity的作用,我们就先看一下他的源码和注释:

// 截取的注释,getOpacity值能返回一下几个值
PixelFormat.UNKNOWN
PixelFormat.TRANSLUCENT
PixelFormat.TRANSPARENT
PixelFormat.OPAQUE

// 系统自动适配
public static final int UNKNOWN     = 0;

// 简单的说就是支持半透明
public static final int TRANSLUCENT = -3;

// 支持全透明
public static final int TRANSPARENT = -2;

// 不支持透明
public static final int OPAQUE      = -1;

属性的命名规则就是见字如见人,字面意思就是这样。其他两个方法,这里没有用到就不说明了,有兴趣的可以自己去研究研究。

这个时候去就可以创建四个状态的Drawable了,普通状态、完成状态还有错误状态都是一样的,所以就只贴一个类的代码了:

/**
 * Created by li.zhipeng on 2017/7/12.
 * 

* 正常状态下的图片 */ public class NormalDrawable extends BaseStatusDrawable { public NormalDrawable(CircleProgressSuperInfo info) { super(info); // 普通状态的颜色要设置,其他的不用设置 setBgColor(info.getBgColor()); setBorderColor(info.getBorderColor()); } @Override public void drawSelf(Canvas canvas) { drawTransition(canvas); } @Override public void release() { } }

重点是LoadingDrawable,其实也很简单,因为继承的关系,现在只需要关心绘制加载状态就足够了,这个时候把我们第一篇CircleProgressBar进行改造:

/**
 * Created by li.zhipeng on 2017/7/12.
 * 

* 加载状态或是进度条的drawable */ public class LoadingDrawable extends BaseStatusDrawable { /** * 圆周的角度 */ private static final Float CIRCULAR = 360f; /** * 进度 */ private float mProgress = 50; /** * 最大进度 */ private int mMaxProgress = 100; /** * 边框颜色,也就是进度的颜色 */ private int mProgressColor = Color.parseColor("#ff00ff"); /** * 绘制的不全进度的颜色 */ private int mDrawBorderColor; /** * 要绘制的进度条的颜色 */ private int mDrawProgressColor; /** * 是否打开过度模式,也就是我们平时看到的类似追赶的效果 */ private boolean mIsIntermediateMode = true; /** * 最小弧度,进度条过度模式最小的弧度 */ private int mMinProgress = 5; /** * 过度动画的时间 */ private static final int DURATION = 1000; /** * 过度动画 */ private ValueAnimator valueAnimator; /** * 开始角度,在过度动画中使用 */ private float mStartAngle = -90f; public LoadingDrawable(CircleProgressSuperInfo info) { super(info); } /** * 设置进度 */ public void setProgress(float progress) { this.mProgress = progress; } /** * 获取进度条的颜色 */ public int getProgressColor() { return this.mProgressColor; } /** * 设置进度条的颜色 */ public void setProgressColor(int color) { this.mProgressColor = color; } /** * 设置进度条的颜色 */ public void setDrawProgressColor(int color) { this.mDrawProgressColor = color; } public int getDrawProgressColor() { return mDrawProgressColor; } public int getDrawBorderColor() { return mDrawBorderColor; } public void setDrawBorderColor(int mDrawBorderColor) { this.mDrawBorderColor = mDrawBorderColor; } /** * 是否是过度模式 */ public boolean isIntermediateMode() { return mIsIntermediateMode; } /** * 设置绘制区域 */ @Override public void setRadius(float radius) { super.setRadius(radius); // 计算要绘制的区域 mRectF.set(mWidth / 2 - mRadius + mBorderWidth / 2, mHeight / 2 - mRadius + mBorderWidth / 2, mWidth / 2 + mRadius - mBorderWidth / 2, mHeight / 2 + mRadius - mBorderWidth / 2); } /** * 设置loading模式 */ public void setIntermediateMode(boolean intermediateMode) { if (mIsIntermediateMode != intermediateMode) { this.mIsIntermediateMode = intermediateMode; // 取消动画 if (!mIsIntermediateMode) { valueAnimator.cancel(); } else { //这里要开启动画 startIntermediateAnim(); } } } @Override public void drawSelf(Canvas canvas) { // 是否要显示loading状态 if (mIsIntermediateMode) { startIntermediateAnim(); drawIntermediateProgress(canvas); } // 绘制进度条 else { drawProgress(canvas); } } /** * 绘制过度进度条 */ private void drawIntermediateProgress(Canvas canvas) { // 首先画出背景圆 mPaint.setColor(mBgColor); mPaint.setStyle(Paint.Style.FILL); // 这里减去了边框的宽度 canvas.drawCircle(mWidth / 2, mHeight / 2, mRadius - mBorderWidth, mPaint); // 画出进度条 mPaint.setColor(mDrawProgressColor); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(mBorderWidth); // 计算圆弧划过的角度 float angle = CIRCULAR / mMaxProgress * mProgress; // 这里要画圆弧 canvas.drawArc(mRectF, mStartAngle, angle, false, mPaint); // 画出另一部分的进度条 mPaint.setColor(mDrawBorderColor); mPaint.setStrokeWidth(mBorderWidth); // 这里要画圆弧 canvas.drawArc(mRectF, mStartAngle + angle, CIRCULAR - angle, false, mPaint); } /** * 绘制进度条 */ private void drawProgress(Canvas canvas) { // 开始画进度条 // 首先画出背景圆 mPaint.setColor(mBgColor); mPaint.setStyle(Paint.Style.FILL); // 这里减去了边框的宽度 canvas.drawCircle(mWidth / 2, mHeight / 2, mRadius - mBorderWidth, mPaint); // 画出进度条 mPaint.setColor(mDrawProgressColor); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(mBorderWidth); // 计算圆弧划过的角度 float angle = CIRCULAR / mMaxProgress * mProgress; // 这里要画圆弧 canvas.drawArc(mRectF, -90, angle, false, mPaint); // 画出另一部分的进度条 mPaint.setColor(mBorderColor); mPaint.setStrokeWidth(mBorderWidth); // 这里要画圆弧 canvas.drawArc(mRectF, -90 + angle, CIRCULAR - angle, false, mPaint); } /** * 开始过度动画 */ private synchronized void startIntermediateAnim() { if (valueAnimator != null && valueAnimator.isStarted()) { return; } if (valueAnimator == null) { valueAnimator = new ValueAnimator().ofFloat(mMinProgress, mMaxProgress - mMinProgress); valueAnimator.setDuration(DURATION); valueAnimator.setInterpolator(new AccelerateDecelerateInterpolator()); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { float value = (float) valueAnimator.getAnimatedValue(); setProgress(value); mStartAngle += 2; invalidateSelf(); } }); valueAnimator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animator) { } @Override public void onAnimationEnd(Animator animator) { } @Override public void onAnimationCancel(Animator animator) { // 充值旋转的角度 mStartAngle = -90; } @Override public void onAnimationRepeat(Animator animator) { // 互换颜色和位置 mStartAngle = mStartAngle - CIRCULAR / mMaxProgress * mMinProgress; int color = getDrawProgressColor(); setDrawProgressColor(getDrawBorderColor()); setDrawBorderColor(color); } }); } // 开始动画的时候,要重新设置颜色,否则颜色可能会错乱,因为在动画的过程已经互换 setDrawProgressColor(mProgressColor); setDrawBorderColor(mBorderColor); valueAnimator.setRepeatCount(-1); valueAnimator.start(); } /** * 停止动画 */ private void stopAnim() { if (valueAnimator != null && valueAnimator.isRunning()) { valueAnimator.cancel(); } } @Override public void release() { stopAnim(); } }

几乎是没有什么变化,增加了开始动画和结束动画方法,这样其他的状态时,可以节省系统资源。

然后是工厂类:

/**
 * Created by li.zhipeng on 2017/7/13.
 * 

* 生产不同状态的Drawable的生产类 */ public class StatusDrawableFactory { private static StatusDrawableFactory mInstance; public synchronized static StatusDrawableFactory getInstance() { if (mInstance == null) { mInstance = new StatusDrawableFactory(); } return mInstance; } /** * 返回指定状态的drawable * */ public BaseStatusDrawable getDrawable(int status, CircleProgressSuperInfo info) { BaseStatusDrawable drawable = null; switch (status) { case Status.NORMAL: drawable = new NormalDrawable(info); break; case Status.LOADING: drawable = new LoadingDrawable(info); break; case Status.COMPLETE: drawable = new CompleteDrawable(info); break; case Status.ERROR: drawable = new ErrorDrawable(info); break; } return drawable; } }

非常简单的单例模式,返回指定的BaseStatusDrawable类型。

最后就是CircleProgressSuperBar:

/**
 * Created by li.zhipeng on 2017/7/12.
 * 

* 具有多状态的CircleProgressBar,整合前两个控件的效果 */ public class CircleProgressSuperBar extends View { /** * 保存四张状态的Drawable */ private BaseStatusDrawable[] drawables = new BaseStatusDrawable[4]; /** * 测试就只要一个xml的构造方法就足够了 */ public CircleProgressSuperBar(Context context, @Nullable AttributeSet attrs) { super(context, attrs); // 初始化信息类 drawables[Status.NORMAL] = StatusDrawableFactory.getInstance().getDrawable(Status.NORMAL, new CircleProgressSuperInfo(Color.parseColor("#3399ff"), Color.parseColor("#3399ff"), 10)); drawables[Status.LOADING] = StatusDrawableFactory.getInstance().getDrawable(Status.LOADING, new CircleProgressSuperInfo(Color.parseColor("#ff0000"), Color.parseColor("#000000"), 10)); drawables[Status.COMPLETE] = StatusDrawableFactory.getInstance().getDrawable(Status.COMPLETE, new CircleProgressSuperInfo(Color.parseColor("#ffcc00"), Color.parseColor("#ffcc00"), 10)); drawables[Status.ERROR] = StatusDrawableFactory.getInstance().getDrawable(Status.ERROR, new CircleProgressSuperInfo(Color.parseColor("#ff3300"), Color.parseColor("#ff3300"), 10)); drawables[Status.NORMAL].setText("Normal"); drawables[Status.ERROR].setText("Error"); drawables[Status.COMPLETE].setText("Complete"); drawables[Status.NORMAL].setTextSize(42); drawables[Status.ERROR].setTextSize(42); drawables[Status.COMPLETE].setTextSize(42); drawables[Status.LOADING].setBorderWidth(20); // 设置重绘回调 drawables[Status.LOADING].setCallback(this); } /** * 动画时长 */ private int mDuration = 500; /** * 目前的状态t */ private int mCurrentStatus = Status.NORMAL; /** * 是否正在动画中 */ private boolean isAnim; @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); // 这里设置一些初始值 int width = getMeasuredWidth(); int height = getMeasuredHeight(); drawables[Status.NORMAL].setWidth(width); drawables[Status.NORMAL].setHeight(height); drawables[Status.LOADING].setWidth(width); drawables[Status.LOADING].setHeight(height); drawables[Status.ERROR].setWidth(width); drawables[Status.ERROR].setHeight(height); drawables[Status.COMPLETE].setWidth(width); drawables[Status.COMPLETE].setHeight(height); // 设置的Radius int radius = width > height ? height / 2 : width / 2; drawables[Status.NORMAL].setMinSize(radius * 2); drawables[Status.LOADING].setMinSize(radius * 2); drawables[Status.ERROR].setMinSize(radius * 2); drawables[Status.COMPLETE].setMinSize(radius * 2); drawables[Status.LOADING].getInfo().setRadius(radius); drawables[Status.LOADING].getInfo().setPadding(width > height ? (width - radius * 2) / 2 : (height - radius * 2) / 2); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 画出不同状态的内容 drawables[mCurrentStatus].draw(canvas); } /** * 设置状态 */ public void setStatus(int status) { if (mCurrentStatus != status && !isAnim) { // 这里设置动画效果 changeStatus(mCurrentStatus, status); // 释放之前的动画 this.drawables[mCurrentStatus].release(); this.mCurrentStatus = status; this.drawables[mCurrentStatus].setIsAnim(true); } } /** * 状态改变的动画 */ private void changeStatus(int fromStatus, int toStatus) { isAnim = true; // 取出相关的动画信息 CircleProgressSuperInfo fromStatusInfo = drawables[fromStatus].getInfo(); CircleProgressSuperInfo toStatusInfo = drawables[toStatus].getInfo(); // 开始动画 AnimatorSet animatorSet = new AnimatorSet(); animatorSet.setDuration(mDuration); animatorSet.playTogether(AnimUtil.getColorAnim(fromStatusInfo.getBgColor(), toStatusInfo.getBgColor(), mDuration, colorUpdateListener), AnimUtil.getColorAnim(fromStatusInfo.getBorderColor(), toStatusInfo.getBorderColor(), mDuration, borderColorUpdateListener), AnimUtil.getRadiusAnim(fromStatusInfo.getRadius(), toStatusInfo.getRadius(), mDuration, radiusUpdateListener), AnimUtil.getShapeAnim(fromStatusInfo.getPadding(), toStatusInfo.getPadding(), mDuration, shapeUpdateListener) ); animatorSet.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animator) { } @Override public void onAnimationEnd(Animator animator) { isAnim = false; drawables[mCurrentStatus].setIsAnim(false); } @Override public void onAnimationCancel(Animator animator) { } @Override public void onAnimationRepeat(Animator animator) { } }); animatorSet.start(); } /** * color动画的回调 */ private ValueAnimator.AnimatorUpdateListener colorUpdateListener = new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { drawables[mCurrentStatus].setBgColor((Integer) valueAnimator.getAnimatedValue()); invalidate(); } }; /** * borderColor动画的回调 */ private ValueAnimator.AnimatorUpdateListener borderColorUpdateListener = new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { drawables[mCurrentStatus].setBorderColor((Integer) valueAnimator.getAnimatedValue()); } }; /** * radius动画的回调 */ private ValueAnimator.AnimatorUpdateListener radiusUpdateListener = new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { drawables[mCurrentStatus].setRadius((float) valueAnimator.getAnimatedValue()); } }; /** * shape动画的回调 */ private ValueAnimator.AnimatorUpdateListener shapeUpdateListener = new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { drawables[mCurrentStatus].setPadding((Float) valueAnimator.getAnimatedValue()); } }; /** * Invalidates the specified Drawable. * * @param drawable the drawable to invalidate */ @Override public void invalidateDrawable(@NonNull Drawable drawable) { invalidate(); } }

主要是找到各个状态的BaseStatusDrawable,然后取出信息,开始属性动画,但是有几个小知识点,你还记得吗?

1、在获取指定状态的图片,直接使用Status.xxx作为数组的索引,保存和取出的速度都很快,是不是想起之前我们聊过的哈希表了?

2、onMeasure方法里,记得使用getMeasuredXXX,因为getWidth和getHeight都是0,千万别忘了。

有些朋友发现了:怎么突然冒出来一个invalidateDrawable()方法?我这里直说了,大家想自己去踩坑的可以试试:

还记得之前说过的继承Drawable是为了重绘吗,如果是你只是调用了Drawable.invalidateSelf(),很遗憾的告诉你,是不可能重绘的,所以这里要重写这个方法,强制重绘。

直接看源码就知道原因了:

//Drawable的重绘方法,实际上是调用了callback,这样就和View解耦了
public void invalidateSelf() {
        final Callback callback = getCallback();
        if (callback != null) {
            callback.invalidateDrawable(this);
        }
    }

// view本身就实现了Callback
public class View implements Drawable.Callback{

    // 请注意里面的判断
    @Override
    public void invalidateDrawable(@NonNull Drawable drawable) {
      // 满足了这个条件,才会重绘,所以要看看判断条件是什么
        if (verifyDrawable(drawable)) {
            final Rect dirty = drawable.getDirtyBounds();
            final int scrollX = mScrollX;
            final int scrollY = mScrollY;

            invalidate(dirty.left + scrollX, dirty.top + scrollY,
                    dirty.right + scrollX, dirty.bottom + scrollY);
            rebuildOutline();
        }
    }

@CallSuper
    protected boolean verifyDrawable(@NonNull Drawable who) {
    // 这里就是判断,view要判断是否使用了这个Drawable,如果没有使用,就不去重绘了,这个理论都是可以理解的。
        return who == mBackground || (mForegroundInfo != null && mForegroundInfo.mDrawable == who);
    }
}

因为我们仅仅是canvas绘图,并没有设置背景或者是前景图片之类的东西,自然就无法重绘,也就是重写invalidateDrawable()方法的原因。

还有两个类,没有贴出来:Status(Drawable的状态),AnimUtil(动画工具类),因为感觉今天的内容已经很长了,所以就省略了把,大家可以在demo中去查看。

总结

看的说的挺溜,其实在写的时候还是出现了很多问题的,而且现在也还存在一些小问题,如果你发现博客中的代码和demo中有一点点区别,那就是我后来又修改了,但是主要思想是不会变了,大家可以自己去设置自定义属性,这样我们的完成度就更完美了。

github地址

你可能感兴趣的:(Android,自定义View系列)