項目地址:LoadingDrawable糯笙,本文分析版本: 979291e
1.簡介
LoadingDrawable是一個使用Drawable
來繪制Loading
動畫的項目稽屏,由于使用Drawable
的原因可以結(jié)合任何View
使用欠动,并且替換方便。目前已經(jīng)實現(xiàn)了8種動畫,而且項目作者dinuscxj表示后期會維護至20多種動畫权均。對于動畫項目,我們之前有分析過:HTextView锅锨、JJSearchViewAnim叽赊。此類項目的結(jié)構(gòu)大家應該比較熟悉了。那么我們就來一起看看LoadingDrawable
是如何使用與實現(xiàn)的必搞。
2.使用方法
如果在ImageView
上使用:
ImageView mIvGear = (ImageView) findViewById(R.id.gear_view);
LoadingDrawable mGearDrawable = new LoadingDrawable(new GearLoadingRenderer(this));
mIvGear.setImageDrawable(mGearDrawable);
mGearDrawable.start();
mGearDrawable.stop();
如果在View
上使用:
View mIvGear = findViewById(R.id.gear_view);
LoadingDrawable mGearDrawable = new LoadingDrawable(new GearLoadingRenderer(this));
mIvGear.setBackground(mGearDrawable);
mGearDrawable.start();
mGearDrawable.stop();
LoadingDrawable
的使用方法非常簡單必指,我們只需要在setImageDrawable()
方法或者setBackground()
傳入LoadingDrawable
對象并在構(gòu)造方法中初始化對應的動畫實現(xiàn)類就可以了。另外開啟動畫與停止動畫分別對應LoadingDrawable
中的start()
和stop()
方法即可恕洲。
3.類關(guān)系圖
類關(guān)系圖很清晰塔橡,就不再多說了,我們直接來看源碼:
4.源碼分析
1.LoadingDrawable的實現(xiàn):
首先我們來看看LoadingDrawable
是如何實現(xiàn)的研侣。代碼如下:
public class LoadingDrawable extends Drawable implements Animatable {
//LoadingRenderer負責具體動畫的繪制
private LoadingRenderer mLoadingRender;
//Drawable.CallBack這里是負責更新Drawable谱邪。
private final Callback mCallback = new Callback() {
@Override
public void invalidateDrawable(Drawable d) {
invalidateSelf();
}
@Override
public void scheduleDrawable(Drawable d, Runnable what, long when) {
scheduleSelf(what, when);
}
@Override
public void unscheduleDrawable(Drawable d, Runnable what) {
unscheduleSelf(what);
}
};
//構(gòu)造方法
public LoadingDrawable(LoadingRenderer loadingRender) {
this.mLoadingRender = loadingRender;
this.mLoadingRender.setCallback(mCallback);
}
@Override
public void draw(Canvas canvas) {
//直接交給mLoadingRender的draw()方法
mLoadingRender.draw(canvas, getBounds());
}
@Override
public void setAlpha(int alpha) {
mLoadingRender.setAlpha(alpha);
}
@Override
public void setColorFilter(ColorFilter cf) {
mLoadingRender.setColorFilter(cf);
}
@Override
public int getOpacity() {
//返回透明的像素格式
return PixelFormat.TRANSLUCENT;
}
@Override
public void start() {
mLoadingRender.start();
}
@Override
public void stop() {
mLoadingRender.stop();
}
@Override
public boolean isRunning() {
return mLoadingRender.isRunning();
}
@Override
public int getIntrinsicHeight() {
//返回Drawable的高度
return (int) (mLoadingRender.getHeight() + 1);
}
@Override
public int getIntrinsicWidth() {
//返回Drawable的寬度
return (int) (mLoadingRender.getWidth() + 1);
}
}
LoadingDrawable
是繼承自Drawable
的并且實現(xiàn)了Animatable
接口,Drawable
簡單的來說就是可以通過Canvas
來繪制出圖形或者圖像的類庶诡。通俗的抽象就是:一些能被畫出來的東西惦银。想必大家也都很熟悉了。對于Animatable
來說,其實就是Android
提供給需要實現(xiàn)動畫的Drawable
需要實現(xiàn)的接口扯俱,它分別有start()
书蚪、stop()
和isRunning()
方法很顯然應該是控制動畫的開始和停止。
對于自定義的Drawable
迅栅,draw()
方法應該是最重要的了殊校。這里可以看出draw()
方法中直接交給了mLoadingRender
來處理《链妫看過我們以前分析的幾篇動畫庫的同學應該知道为流,mLoadingRender
肯定就是所有動畫的父類了。然后根據(jù)父類的抽象方法來分別做具體的實現(xiàn)让簿,從而實現(xiàn)不同的動畫敬察。所以我們接著來看LoadingRenderer
的實現(xiàn):
2.LoadingRenderer的實現(xiàn):
LoadingRenderer
的主要代碼如下:
public abstract class LoadingRenderer {
protected float mWidth;
protected float mHeight;
protected float mStrokeWidth;
protected float mCenterRadius;
private long mDuration;
private Drawable.Callback mCallback;
private ValueAnimator mRenderAnimator;
public LoadingRenderer(Context context) {
setupDefaultParams(context);
setupAnimators();
}
//抽象方法交給子類去實現(xiàn)
public abstract void draw(Canvas canvas, Rect bounds);
public abstract void computeRender(float renderProgress);
public abstract void setAlpha(int alpha);
public abstract void setColorFilter(ColorFilter cf);
public abstract void reset();
public void start() {
reset();
setDuration(mDuration);
mRenderAnimator.start();
}
public void stop() {
mRenderAnimator.cancel();
}
public boolean isRunning() {
return mRenderAnimator.isRunning();
}
public void setCallback(Drawable.Callback callback) {
this.mCallback = callback;
}
//invalidate方法,重繪當前Drawable
protected void invalidateSelf() {
mCallback.invalidateDrawable(null);
}
//設(shè)置寬度,高度,線條寬度以及圓的半徑等默認參數(shù)
private void setupDefaultParams(Context context) {
final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
final float screenDensity = metrics.density;
mWidth = DEFAULT_SIZE * screenDensity;
mHeight = DEFAULT_SIZE * screenDensity;
mStrokeWidth = DEFAULT_STROKE_WIDTH * screenDensity;
mCenterRadius = DEFAULT_CENTER_RADIUS * screenDensity;
mDuration = ANIMATION_DURATION;
}
//設(shè)置ValueAnimator的參數(shù)
private void setupAnimators() {
mRenderAnimator = ValueAnimator.ofFloat(0, 1);
mRenderAnimator.setRepeatCount(Animation.INFINITE);
mRenderAnimator.setRepeatMode(Animation.RESTART);
mRenderAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
computeRender((float) animation.getAnimatedValue());
invalidateSelf();
}
});
}
}
果然和我們的猜想是一樣的LoadingRenderer
是一個抽象類尔当,首先在構(gòu)造方法里定義了一些參數(shù)的默認值莲祸,例如:寬高,描邊寬度椭迎,圓的默認半徑锐帜。以及初始化了一個ValueAnimator
,并在onAnimationUpdate()
方法里調(diào)用了computeRender()
以及invalidateSelf()
方法畜号。
其中computeRender(float renderProgress)
方法里的renderProgress
就是0到1
不斷變化的值缴阎,這個方法是用來計算當前動畫繪制需要的參數(shù),在實現(xiàn)動畫的時候我們通常會使用這種方法根據(jù)當前的renderProgress
的值來計算當前需要繪制圖像的參數(shù)简软,從而完成繪制药蜻。一般情況下我們都會在draw()
里直接根據(jù)renderProgress
來做計算然后直接進行繪制,但是這樣寫出的代碼的可讀性就不太好了替饿,因為計算和繪制都寫在了一起。LoadingDrawable
在這一點上就做的非常好贸典。它通過computeRender(float renderProgress);
方法來計算好能直接被draw();
方法使用的參數(shù)视卢。同時draw()
方法里就只負責繪制的邏輯。在計算比較復雜的場景這樣做能極大的提高代碼的可讀性廊驼。這一點非常值得我們學習据过。
看完了LoadingRenderer
的實現(xiàn),接下來我們就來看看其中一個具體的實現(xiàn)類到底是如何實現(xiàn)的妒挎。LoadingRenderer
目前實現(xiàn)了8種不同的動畫绳锅,具體在LoadingRenderer
的github
主頁上都可以看到。大家也可以選自己喜歡的動畫去分析酝掩。(由于我的數(shù)學很渣鳞芙。。)我們這次就分析一個較為簡單的動畫WhorlLoadingRenderer
。就是下圖左上角的這個動畫:
3.WhorlLoadingRenderer
的實現(xiàn):
首先我們先來分解一下這個動畫:
- 首先是不斷旋轉(zhuǎn)的原朝。
- 先從初始點繪制出一個弧形驯嘱,然后弧形再不斷的縮短直至重新變?yōu)橐粋€點,最終不斷循環(huán)喳坠。
其中還有一些細節(jié)比如三條弧線的位置以及線條寬度等等鞠评,我們來看看WhorlLoadingRenderer
是怎樣實現(xiàn)的:
public class WhorlLoadingRenderer extends LoadingRenderer {
private static final Interpolator MATERIAL_INTERPOLATOR = new FastOutSlowInInterpolator();
private static final float FULL_ROTATION = 1080.0f;
private static final float ROTATION_FACTOR = 0.25f;
private static final float MAX_PROGRESS_ARC = 0.6f;
private static final float START_TRIM_DURATION_OFFSET = 0.5f;
private static final float END_TRIM_DURATION_OFFSET = 1.0f;
private static final int DEGREE_180 = 180;
private static final int DEGREE_360 = 360;
private static final int NUM_POINTS = 5;
private int[] mColors;
private float mStrokeInset;
private float mEndTrim;
private float mRotation;
private float mStartTrim;
private float mRotationCount;
private float mGroupRotation;
private float mOriginEndTrim;
private float mOriginRotation;
private float mOriginStartTrim;
private static final int[] DEFAULT_COLORS = new int[] {
Color.RED, Color.GREEN, Color.BLUE
};
private final Paint mPaint = new Paint();
private final RectF mTempBounds = new RectF();
private final Animator.AnimatorListener mAnimatorListener = new AnimatorListenerAdapter() {
@Override
public void onAnimationRepeat(Animator animator) {
super.onAnimationRepeat(animator);
//儲存初始點
storeOriginals();
mStartTrim = mEndTrim;
mRotationCount = (mRotationCount + 1) % (NUM_POINTS);
}
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
mRotationCount = 0;
}
};
public WhorlLoadingRenderer(Context context) {
super(context);
//設(shè)置Paint的參數(shù)
setupPaint();
//添加動畫監(jiān)聽
addRenderListener(mAnimatorListener);
}
private void setupPaint() {
mColors = DEFAULT_COLORS;
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(getStrokeWidth());
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeCap(Paint.Cap.ROUND);
//設(shè)置內(nèi)邊距
setInsets((int) getWidth(), (int) getHeight());
}
@Override
public void draw(Canvas canvas, Rect bounds) {
//保存當前畫布狀態(tài)
int saveCount = canvas.save();
//旋轉(zhuǎn)畫布
canvas.rotate(mGroupRotation, bounds.exactCenterX(), bounds.exactCenterY());
//設(shè)置繪制的RectF
RectF arcBounds = mTempBounds;
arcBounds.set(bounds);
//設(shè)置內(nèi)邊距
arcBounds.inset(mStrokeInset, mStrokeInset);
if (mStartTrim == mEndTrim) {
mStartTrim = mEndTrim + getMinProgressArc();
}
//開始的角度
float startAngle = (mStartTrim + mRotation) * DEGREE_360;
//結(jié)束的角度
float endAngle = (mEndTrim + mRotation) * DEGREE_360;
//角度范圍
float sweepAngle = endAngle - startAngle;
//根據(jù)mColors.length來繪制曲線
for (int i = 0; i < mColors.length; i++) {
mPaint.setStrokeWidth(getStrokeWidth() / (i + 1));
mPaint.setColor(mColors[i]);
//繪制弧線
canvas.drawArc(createArcBounds(arcBounds, i), startAngle + DEGREE_180 * (i % 2), sweepAngle, false, mPaint);
}
//恢復畫布狀態(tài)
canvas.restoreToCount(saveCount);
}
// 根據(jù)需要繪制弧線的index 來計算不同的繪制范圍即arcBounds
private RectF createArcBounds(RectF sourceArcBounds, int index) {
RectF arcBounds = new RectF();
int intervalWidth = 0;
for (int i = 0; i < index; i++) {
intervalWidth += getStrokeWidth() / (i + 1.0f) * 1.5f;
}
int arcBoundsLeft = (int) (sourceArcBounds.left + intervalWidth);
int arcBoundsTop = (int) (sourceArcBounds.top + intervalWidth);
int arcBoundsRight = (int) (sourceArcBounds.right - intervalWidth);
int arcBoundsBottom = (int) (sourceArcBounds.bottom - intervalWidth);
arcBounds.set(arcBoundsLeft, arcBoundsTop, arcBoundsRight, arcBoundsBottom);
return arcBounds;
}
@Override
public void computeRender(float renderProgress) {
//獲得最小的弧度
final float minProgressArc = getMinProgressArc();
final float originEndTrim = mOriginEndTrim;
final float originStartTrim = mOriginStartTrim;
final float originRotation = mOriginRotation;
// Moving the start trim only occurs in the first 50% of a
// single ring animation
// 當renderProgress < 0.5時,不斷增加mStartTrim的值
if (renderProgress <= START_TRIM_DURATION_OFFSET) {
float startTrimProgress = (renderProgress) / (1.0f - START_TRIM_DURATION_OFFSET);
mStartTrim = originStartTrim + ((MAX_PROGRESS_ARC - minProgressArc) * MATERIAL_INTERPOLATOR.getInterpolation(startTrimProgress));
}
// Moving the end trim starts after 50% of a single ring
// animation completes
// 當renderProgress > 0.5時壕鹉,不斷增加mEndTrim的值
if (renderProgress > START_TRIM_DURATION_OFFSET) {
float endTrimProgress = (renderProgress - START_TRIM_DURATION_OFFSET) / (END_TRIM_DURATION_OFFSET - START_TRIM_DURATION_OFFSET);
mEndTrim = originEndTrim + ((MAX_PROGRESS_ARC - minProgressArc) * MATERIAL_INTERPOLATOR.getInterpolation(endTrimProgress));
}
//計算畫布整體的旋轉(zhuǎn)角度
mGroupRotation = ((FULL_ROTATION / NUM_POINTS) * renderProgress) + (FULL_ROTATION * (mRotationCount / NUM_POINTS));
//計算弧線點的旋轉(zhuǎn)
mRotation = originRotation + (ROTATION_FACTOR * renderProgress);
invalidateSelf();
}
@Override
public void reset() {
resetOriginals();
}
//設(shè)置內(nèi)邊距剃幌,為了使動畫的繪制居中
public void setInsets(int width, int height) {
final float minEdge = (float) Math.min(width, height);
float insets;
if (getCenterRadius() <= 0 || minEdge < 0) {
insets = (float) Math.ceil(getStrokeWidth() / 2.0f);
} else {
insets = minEdge / 2.0f - getCenterRadius();
}
mStrokeInset = insets;
}
private void storeOriginals() {
mOriginStartTrim = mStartTrim;
mOriginEndTrim = mEndTrim;
mOriginRotation = mRotation;
}
//重置初始參數(shù)
private void resetOriginals() {
mOriginStartTrim = 0;
mOriginEndTrim = 0;
mOriginRotation = 0;
setStartTrim(0);
setEndTrim(0);
setRotation(0);
}
//獲得最小弧度,
private float getMinProgressArc() {
//Math.toRadians將角度轉(zhuǎn)換為弧度,這里的角度是描邊的寬度 / 周長。所以繪制出的初始是一個點
return (float) Math.toRadians(getStrokeWidth() / (2 * Math.PI * getCenterRadius()));
}
}
從WhorlLoadingRenderer
的代碼中可以看出晾浴,首先在構(gòu)造方法里設(shè)置了Paint
的參數(shù)以及添加了動畫監(jiān)聽mAnimatorListener
负乡。然后當start()
方法觸發(fā)時,會不斷的調(diào)用computeRender()
和draw()
方法怠肋。其中computeRender()
負責計算mStartTrim
敬鬓、mEndTrim
、mGroupRotation
等參數(shù)笙各。draw()
方法根據(jù)計算得到的值來繪制出對應的圖形钉答。從而最終形成動畫。
5.注意事項
在使用LoadingDrawable
過程中我們會發(fā)現(xiàn)使用ImageView
的setImageDrawable()
方法與使用View
的setBackground()
效果并不一樣:
使用ImageView
的setImageDrawable()
方法效果如下:
而使用View
的setBackground()
方法效果如下:
同樣的實現(xiàn)代碼杈抢,兩種調(diào)用方法的效果竟然不一樣数尿。setBackground()
中的圓環(huán)變大了,而線條變細了惶楼,那這到底是因為什么呢右蹦?我們只能從這兩個方法的源碼中找答案了。我們來看看setImageDrawable()
和setBackground()
是如何實現(xiàn)的(源碼對應Android API 23
):
1.ImageView的setImageDrawable()
方法的實現(xiàn):
public void setImageDrawable(@Nullable Drawable drawable) {
if (mDrawable != drawable) {
mResource = 0;
mUri = null;
final int oldWidth = mDrawableWidth;
final int oldHeight = mDrawableHeight;
updateDrawable(drawable);
if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {
requestLayout();
}
invalidate();
}
}
首先判斷mDrawable
是否為空歼捐,然后賦值了寬高何陆,緊接著調(diào)用了updateDrawable(drawable)
:
private void updateDrawable(Drawable d) {
if (d != mRecycleableBitmapDrawable && mRecycleableBitmapDrawable != null) {
mRecycleableBitmapDrawable.setBitmap(null);
}
if (mDrawable != null) {
mDrawable.setCallback(null);
unscheduleDrawable(mDrawable);
}
mDrawable = d;
if (d != null) {
d.setCallback(this);
d.setLayoutDirection(getLayoutDirection());
if (d.isStateful()) {
d.setState(getDrawableState());
}
d.setVisible(getVisibility() == VISIBLE, true);
d.setLevel(mLevel);
mDrawableWidth = d.getIntrinsicWidth();
mDrawableHeight = d.getIntrinsicHeight();
applyImageTint();
applyColorMod();
configureBounds();
} else {
mDrawableWidth = mDrawableHeight = -1;
}
}
設(shè)置Callback
以及一些參數(shù),這里是把ImageView
設(shè)置成了mDrawable
的Callback
豹储,所以當調(diào)用LoadingDrawable
中mCallback
的invalidateSelf();
方法時其實是調(diào)用了ImageView
的invalidateDrawable()
方法從而更新drawable
贷盲。這里我們的重點是看configureBounds()
方法:
private void configureBounds() {
if (mDrawable == null || !mHaveFrame) {
return;
}
int dwidth = mDrawableWidth;
int dheight = mDrawableHeight;
int vwidth = getWidth() - mPaddingLeft - mPaddingRight;
int vheight = getHeight() - mPaddingTop - mPaddingBottom;
boolean fits = (dwidth < 0 || vwidth == dwidth) &&
(dheight < 0 || vheight == dheight);
if (dwidth <= 0 || dheight <= 0 || ScaleType.FIT_XY == mScaleType) {
/* If the drawable has no intrinsic size, or we're told to
scaletofit, then we just fill our entire view.
*/
mDrawable.setBounds(0, 0, vwidth, vheight);
mDrawMatrix = null;
} else {
// We need to do the scaling ourself, so have the drawable
// use its native size.
//我們需要自身縮放。所以drawable使用它已有的尺寸
mDrawable.setBounds(0, 0, dwidth, dheight);
......
}
}
省略了部分代碼剥扣,這里我們注意看注釋巩剖。當drawable
的寬高為0
或者ImageView
的ScaleType
是ScaleType.FIT_XY
時。直接把當前View
去除padding
的寬高設(shè)置給drawable
钠怯。如果drawable
有寬高的話佳魔,那么ImageView
則會自身縮放來適應drawable
。具體的縮放是通過Matrix
來做的晦炊。有興趣的同學可以自行研究鞠鲜。其實這里設(shè)置了mDrawable
的寬高宁脊,所以在LoadingDrawable
類里的draw()
方法:
@Override
public void draw(Canvas canvas) {
//直接交給mLoadingRender的draw()方法
mLoadingRender.draw(canvas, getBounds());
}
中的getBounds()
有可能會被ImageView
重新設(shè)置。所以如果把ImageView
的ScaleType
設(shè)置成ScaleType.FIT_XY
那么結(jié)果就會和setBackground()
一樣镊尺。接下來我們看看setBackground()
是怎么實現(xiàn)的:
2.View
的setBackground()
方法實現(xiàn)
public void setBackground(Drawable background) {
//noinspection deprecation
setBackgroundDrawable(background);
}
/**
* @deprecated use {@link #setBackground(Drawable)} instead
*/
@Deprecated
public void setBackgroundDrawable(Drawable background) {
computeOpaqueFlags();
if (background == mBackground) {
return;
}
boolean requestLayout = false;
mBackgroundResource = 0;
if (mBackground != null) {
mBackground.setCallback(null);
unscheduleDrawable(mBackground);
}
if (background != null) {
......
mBackground = background;
......
}
......
computeOpaqueFlags();
if (requestLayout) {
requestLayout();
}
mBackgroundSizeChanged = true;
invalidate(true);
}
我們發(fā)現(xiàn)方法里就只把drawable
賦值給了mBackground
并沒有操作drawable
的大小朦佩。省略的部分代碼也沒有相關(guān)邏輯。但是我們知道最終mBackground
是要被繪制出來的庐氮。我們?nèi)?code>View的draw()
方法看看:
public void draw(Canvas canvas) {
...
if (!dirtyOpaque) {
drawBackground(canvas);
}
...
}
果然有drawBackground(canvas)
:
private void drawBackground(Canvas canvas) {
final Drawable background = mBackground;
if (background == null) {
return;
}
setBackgroundBounds();
...
}
void setBackgroundBounds() {
if (mBackgroundSizeChanged && mBackground != null) {
mBackground.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
rebuildOutline();
}
}
省略部分繪制背景的代碼语稠,最終我們發(fā)現(xiàn)會將View
的寬高設(shè)置給mBackground
。所以我們就找出了為什么setBackground()
方法會繪制出不一樣的效果弄砍。所以這里推薦LoadingDrawable
和ImageView
配合使用仙畦。如果配合View
使用可能還需要自己去手動調(diào)整一些參數(shù)。
6.個人評價
LoadingDrawable
實現(xiàn)了多種實用的Loading
動畫音婶,并且在一些特定的業(yè)務(wù)場景下慨畸,Drawable
使用起來更加方便。除了需要注意上一條的注意事項之外衣式。LoadingDrawable
非常適合在項目中使用寸士。而且LoadingDrawable
的代碼相當規(guī)范。如果你的項目里有類似動畫的需求碴卧,結(jié)合LoadingDrawable
一定能讓你事半功倍!
我每周會寫一篇源代碼分析的文章,以后也可能會有其他主題.
如果你喜歡我寫的文章的話,歡迎關(guān)注我的新浪微博@達達達達sky
地址: http://weibo.com/u/2030683111
每周我會第一時間在微博分享我寫的文章,也會積極轉(zhuǎn)發(fā)更多有用的知識給大家.