源码下载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度
就这样,通过起始点和终止点计算方式的反复交换,就可以生成图上的效果。
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); } } }