dmytrodanylyk/circular-progress-button源码解析(二)

郎曜文
2023-12-01

转载注明出处http://blog.csdn.net/crazy__chen/article/details/46280279

源码下载http://download.csdn.net/detail/kangaroo835127729/8755815

在上篇文章http://blog.csdn.net/crazy__chen/article/details/46278423中,我主要讲述了circular-progress-button状态切换的动画过程,接下来我们看一个最特殊的状态,就是加载状态,这个状态会显示一个圆环来表示当前加载的进度,但是其实circular-progress-button提供给了我们两个选择,一个是圆环弧度代表进度(例如下载文件),一个是在不知道进度(例如json数据请求)的情况下,有一个特别的旋转样式(这个样式的实现比较复杂,这篇文章主要也是为了讲它)。

上述样式,由一个属性mIndeterminateProgressMode来定义(有提供set方法)。当mIndeterminateProgressMode为false(出现进度样式),为true,出现旋转样式

下面看一下这两种样式(还不会截动图,过几天上传)

对于这两个两个样式,我们先从circular-progress-button的ondraw()方法说起

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        if (mProgress > 0 && mState == State.PROGRESS && !mMorphingInProgress) {
            if (mIndeterminateProgressMode) {
                drawIndeterminateProgress(canvas);
            } else {
                drawProgress(canvas);
            }
        }
    }
从ondraw()方法可以看出,当mState为mState == State.PROGRESS,也就是为加载状态时,才调用函数来进行自定义绘制(绘制圆环),否则自己绘制按钮样式就可以了。

我先来看drawProgress(canvas)方法,也就是根据进度绘圆环,不旋转

/**
     * 进度环形背景
     * @param canvas
     */
    private void drawProgress(Canvas canvas) {
        if (mProgressDrawable == null) {
            //偏移,因为要求环形在原按钮的正中间
            int offset = (getWidth() - getHeight()) / 2;
            //环形高度           
            int size = getHeight() - mPaddingProgress * 2;
            mProgressDrawable = new CircularProgressDrawable(size, mStrokeWidth, mColorIndicator);
            int left = offset + mPaddingProgress;
            mProgressDrawable.setBounds(left, mPaddingProgress, left, mPaddingProgress);
        }
        float sweepAngle = (360f / mMaxProgress) * mProgress;
        mProgressDrawable.setSweepAngle(sweepAngle);
        mProgressDrawable.draw(canvas);
    }
这里关键是我们定义了一个CircularProgressDrawable,设置了它的位置,范围,和颜色等

class CircularProgressDrawable extends Drawable {

    private float mSweepAngle;
    private float mStartAngle;
    private int mSize;
    private int mStrokeWidth;
    private int mStrokeColor;

    public CircularProgressDrawable(int size, int strokeWidth, int strokeColor) {
        mSize = size;
        mStrokeWidth = strokeWidth;
        mStrokeColor = strokeColor;
        mStartAngle = -90;
        mSweepAngle = 0;
    }

    public void setSweepAngle(float sweepAngle) {
        mSweepAngle = sweepAngle;
    }

    public int getSize() {
        return mSize;
    }

    @Override
    public void draw(Canvas canvas) {
        final Rect bounds = getBounds();

        if (mPath == null) {
            mPath = new Path();
        }
        mPath.reset();
        mPath.addArc(getRect(), mStartAngle, mSweepAngle);
        mPath.offset(bounds.left, bounds.top);
        canvas.drawPath(mPath, createPaint());
    }

    @Override
    public void setAlpha(int alpha) {

    }

    @Override
    public void setColorFilter(ColorFilter cf) {
    }

    @Override
    public int getOpacity() {
        return 1;
    }

    private RectF mRectF;
    private Paint mPaint;
    private Path mPath;

    private RectF getRect() {
        if (mRectF == null) {
            int index = mStrokeWidth / 2;
            mRectF = new RectF(index, index, getSize() - index, getSize() - index);
        }
        return mRectF;
    }

    private Paint createPaint() {
        if (mPaint == null) {
            mPaint = new Paint();
            mPaint.setAntiAlias(true);
            mPaint.setStyle(Paint.Style.STROKE);
            mPaint.setStrokeWidth(mStrokeWidth);
            mPaint.setColor(mStrokeColor);
        }

        return mPaint;
    }
}
关键是里面的draw方法,mpath用于设置绘制圆环的路径,我们知道调用mPath.addArc()就可以设置绘制圆环,那么RectF是什么呢,看getRect()方法注意到,我们创建一个RectF,这个RectF的left是笔触的一半,为什么是一半呢?因为我沿着一半画,笔触宽度刚好被这一半平分。(如果不是一半,就画的时候就会越界,因为笔触也是有宽度的)
int index = mStrokeWidth / 2;
            mRectF = new RectF(index, index, getSize() - index, getSize() - index);
然后是,使之移动到正中间
mPath.offset(bounds.left, bounds.top);
最后,mSweepAngle觉得了扫过的弧度,而这个弧度,是根据进度process除以100*360计算出来的
mPath.addArc(getRect(), mStartAngle, mSweepAngle);

OK,就是这么简单,我们绘制出了圆弧。


接下来我们看另外一个方法drawIndeterminateProgress()

/**
     * 环形循环式背景
     * @param canvas
     */
    private void drawIndeterminateProgress(Canvas canvas) {
        if (mAnimatedDrawable == null) {
            int offset = (getWidth() - getHeight()) / 2;
            mAnimatedDrawable = new CircularAnimatedDrawable(mColorIndicator, mStrokeWidth);
            //使之在正中间
            int left = offset + mPaddingProgress;
            int right = getWidth() - offset - mPaddingProgress;
            int bottom = getHeight() - mPaddingProgress;
            int top = mPaddingProgress;
            mAnimatedDrawable.setBounds(left, top, right, bottom);
            mAnimatedDrawable.setCallback(this);
            mAnimatedDrawable.start();
        } else {
            mAnimatedDrawable.draw(canvas);
        }
    }
这个方法与上面的其实类似,只是创建的对象不同,我们这里创建了一个CircularAnimatedDrawable对象(从这里可以看出,每种不同的样式,其实就是不同的Drawable对象)

这个对象比较复杂,我们慢慢来看,首先是一些基本属性和构造方法

class CircularAnimatedDrawable extends Drawable implements Animatable {
	/**
	 * 线性时间插入器
	 */
    private static final Interpolator ANGLE_INTERPOLATOR = new LinearInterpolator();
    /**
	 * 时间插入器,先快后慢
	 */
    private static final Interpolator SWEEP_INTERPOLATOR = new DecelerateInterpolator();    
    private static final int ANGLE_ANIMATOR_DURATION = 2000;
    private static final int SWEEP_ANIMATOR_DURATION = 600;
    public static final int MIN_SWEEP_ANGLE = 30;
    private final RectF fBounds = new RectF();

    private ObjectAnimator mObjectAnimatorSweep;
    private ObjectAnimator mObjectAnimatorAngle;
    /**
     * 是否改变头尾
     */
    private boolean mModeAppearing;
    private Paint mPaint;
    /**
     * 循环从0到360,每次增加2*MIN_SWEEP_ANGLE(60)
     */
    private float mCurrentGlobalAngleOffset;
    /**
     * 当前角度,线性增长
     */
    private float mCurrentGlobalAngle;
    /**
     * 当前扫过角度,先快后慢增长
     */
    private float mCurrentSweepAngle;
    /**
     * 边框宽度
     */
    private float mBorderWidth;
    /**
     * 是否正在运行动画
     */
    private boolean mRunning;

构造方法

public CircularAnimatedDrawable(int color, float borderWidth) {
        mBorderWidth = borderWidth;
        //初始化画笔
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.STROKE);
        //设置画笔宽度
        mPaint.setStrokeWidth(borderWidth);
        mPaint.setColor(color);

        setupAnimations();
    }

color是圆环颜色,borderwidth是笔触大小

然后是画笔初始化,接着调用setupAnimations()方法初始化动画

/**
     * 装载动画
     */
    private void setupAnimations() {
    	//角度动画
    	/**
    	 * target 	The object whose property is to be animated.被设置动画的对象
		 * property 	The property being animated. 被设置动画的属性
		 * values 	A set of values that the animation will animate between over time. 设置的值
		 * 这个构造函数说明,对CircularAnimatedDrawable类对象,设置关于mCurrentGlobalAngle属性的动画
		 * 也就是线性的给mCurrentGlobalAngle从0到360f赋值
    	 */
        mObjectAnimatorAngle = ObjectAnimator.ofFloat(this, mAngleProperty, 360f);
        //设置插入器
        mObjectAnimatorAngle.setInterpolator(ANGLE_INTERPOLATOR);
        //设置动画时长
        mObjectAnimatorAngle.setDuration(ANGLE_ANIMATOR_DURATION);
        //设置循环模式
        mObjectAnimatorAngle.setRepeatMode(ValueAnimator.RESTART);
        //设置循环次数
        mObjectAnimatorAngle.setRepeatCount(ValueAnimator.INFINITE);

        //扫过动画
        //从快到慢地给mCurrentSweepAngle从0到(360f -MIN_SWEEP_ANGLE * 2)(也就是300)赋值
        mObjectAnimatorSweep = ObjectAnimator.ofFloat(this, mSweepProperty, 360f - MIN_SWEEP_ANGLE * 2);
        mObjectAnimatorSweep.setInterpolator(SWEEP_INTERPOLATOR);
        mObjectAnimatorSweep.setDuration(SWEEP_ANIMATOR_DURATION);
        mObjectAnimatorSweep.setRepeatMode(ValueAnimator.RESTART);
        mObjectAnimatorSweep.setRepeatCount(ValueAnimator.INFINITE);
        mObjectAnimatorSweep.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {

            }

            @Override
            public void onAnimationEnd(Animator animation) {

            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {
            	//当尾追上头,改变头尾
                toggleAppearingMode();
            }
        });
    }

这里实质上创建了两种动画,都是ObjectAnimator动画,而ObjectAnimator继承自ValueAnimator,所以说这两个动画,都是做提供当前值的作用,本身不改变控件样式

一个是mObjectAnimatorAngle,线性增长,用于计算起始角度

一个是mObjectAnimatorSweep,先快后慢增长,用于计算扫过的角度

我们要仔细观察动画的过程(可能很快,大家在测试时,可以加大动画默认播放时间,便于观察),注意到过程是这样的,首先是快的追慢的,使圆弧不断缩小(同时整个圆弧在移动,因为快慢两头都在动),追上以后(相距MIN_SWEEP_ANGLE = 30称为追上),快的和慢的交换(也就是快的变成慢,慢的变成快),之后快的继续追慢的,使圆弧不断增大,追上以后,再次交换,重复上述过程。


设置好上面的动画以后,我们来看ondraw()方法

 @Override
    public void draw(Canvas canvas) {    	
        float startAngle = mCurrentGlobalAngle - mCurrentGlobalAngleOffset;
        float sweepAngle = mCurrentSweepAngle;
        if (!mModeAppearing) {
            startAngle = startAngle + sweepAngle;
            sweepAngle = 360 - sweepAngle - MIN_SWEEP_ANGLE;       
        } else {
            sweepAngle += MIN_SWEEP_ANGLE;           
        }
        
        /**
         * 绘制圆弧       
         * oval :指定圆弧的外轮廓矩形区域。
         * startAngle: 圆弧起始角度,单位为度。
         * sweepAngle: 圆弧扫过的角度,顺时针方向,单位为度。
         * useCenter: 如果为True时,在绘制圆弧时将圆心包括在内,通常用来绘制扇形。
    	 * paint: 绘制圆弧的画板属性,如颜色,是否填充等。
         */
        canvas.drawArc(fBounds, startAngle, sweepAngle, false, mPaint);
    }

首先是mModeAppearing这个属性,初始值为false,然后我们注意到,在每次mObjectAnimatorSweep增长到300度时,就会对这个值取反,为什么要这样,继续看。

我们模拟一下最开始的过程,首先这时候mModeAppearing为false,mCurrentGlobalAngleOffset为0,

那么startAngle(起始点) = mCurrentGlobalAngle维持线性增长,sweepAngle = mCurrentSweepAngle为非线性增长

然后到条件语句,会调用

startAngle = startAngle + sweepAngle;
sweepAngle = 360 - sweepAngle - MIN_SWEEP_ANGLE;

这时startAngle为线性增长和非线性增长的和,所以本质为非线性增长(先快后慢)

而我们来考虑圆弧的终止点,等于startAngle+sweepAngle=360 + startAngle- MIN_SWEEP_ANGLE(注意前后startAngle意义不同,其实就是上面两式相加)

可以看出终止点是线性增长的。

所以显然,起始点会追上终止点,所以在第一次绘制中,运动地快的那个,其实是起始点,慢的是终止点

由于两者速度差异,而我们绘制的是起始点到终止点的圆弧,自然就会逐渐缩小了。


OK,当起始点追上终止点(相距MIN_SWEEP_ANGLE = 30称为追上)时,我们要交换两者的增长方式,所以mModeAppearing被设置为true

我再来看true的时候,做了些什么

float startAngle = mCurrentGlobalAngle - mCurrentGlobalAngleOffset;
起始点为线性增长

float sweepAngle = mCurrentSweepAngle;
扫过弧度为非线性,导致终止点为非线性

else {
            sweepAngle += MIN_SWEEP_ANGLE;           
        }
还要加上MIN_SWEEP_ANGLE,因为交换完之后,mCurrentSweepAngle被置零了,而我们还要保持原来相差30度的样式,所以要加上一个30度

另外注意到float startAngle = mCurrentGlobalAngle - mCurrentGlobalAngleOffset,其中mCurrentGlobalAngleOffset又这个函数决定

/**
     * 转换显示模式
     */
    private void toggleAppearingMode() {
    	//转换模式
        mModeAppearing = !mModeAppearing;
        if (mModeAppearing) {
            mCurrentGlobalAngleOffset = (mCurrentGlobalAngleOffset + MIN_SWEEP_ANGLE * 2) % 360;
        }
    }
也就是mCurrentGlobalAngleOffset在每次的基础上加上60,为什么呢、因为当第一次追上时,startAngle = mCurrentGlobalAngle+300

由于一个周期360,mCurrentGlobalAngle+300所在位就等于mCurrentGlobalAngle-60

而且每次循环就加60,所以mCurrentGlobalAngleOffset也要对应增加


就这样,通过起始点和终止点计算方式的反复交换,就可以生成图上的效果。

OK,circular-progress-button到这里就讲解完毕了,下面贴一段对circular-progress-button进行使用的代码,不进行过多说明,大家简单看看

public class MainActivity extends Activity {		
	
    private CircularProgressButton circularProgressButton;
    private CircularProgressButton circularProgressButton2;

	@Override
    public void onCreate(Bundle savedInstanceState) {
    	super.onCreate(savedInstanceState);    	
    	setContentView(R.layout.main);
    	circularProgressButton = (CircularProgressButton) findViewById(R.id.mp);
    	circularProgressButton2 = (CircularProgressButton) findViewById(R.id.mp2);
    	//设置为旋转样式
    	circularProgressButton2.setIndeterminateProgressMode(true);
    	circularProgressButton2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (circularProgressButton2.getProgress() == 0) {
                	circularProgressButton2.setProgress(50);
                } else if (circularProgressButton2.getProgress() == -1) {
                	circularProgressButton2.setProgress(0);
                } else {
                	circularProgressButton2.setProgress(-1);
                }
            }
        });
    	
    	circularProgressButton.setOnClickListener(new OnClickListener() {			
			@Override
			public void onClick(View v) {				
				new GetDataTask().execute();
			}
		});
		
    }    
	
	private class GetDataTask extends AsyncTask<Void, Integer, Void> {
		int i = 0;             
        @Override
        protected Void doInBackground(Void... params) {      
        	while(i<=100){
        		try {
    				Thread.sleep(20);
    			} catch (InterruptedException e) {				
    				e.printStackTrace();
    			}
            	publishProgress(i++);  
        	}        	
			return null;          
        }        
        
        @Override
        protected void onProgressUpdate(Integer... values) {
        	circularProgressButton.setProgress(values[0]);	        	
        	super.onProgressUpdate(values);
        }
        
        @Override
        protected void onPostExecute(Void result) {    
        	circularProgressButton.setProgress(100);	        	
        	super.onPostExecute(result);
        }
    }
    
}

 类似资料: