dmytrodanylyk/circular-progress-button源碼解析(二),circularprogress
源碼下載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() {@Overridepublic 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); } } }