转载注明出处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();
}
然后是画笔初始化,接着调用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();
}
});
}
一个是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,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);
}
}
}