项目地址:LoadingDrawable,本文分析版本: 979291e
1.简介
LoadingDrawable是一个使用Drawable
来绘制Loading
动画的项目,由于使用Drawable
的原因可以结合任何View
使用,并且替换方便。目前已经实现了8种动画,而且项目作者dinuscxj表示后期会维护至20多种动画。对于动画项目,我们之前有分析过:HTextView、JJSearchViewAnim。此类项目的结构大家应该比较熟悉了。那么我们就来一起看看LoadingDrawable
是如何使用与实现的。
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
对象并在构造方法中初始化对应的动画实现类就可以了。另外开启动画与停止动画分别对应LoadingDrawable
中的start()
和stop()
方法即可。
3.类关系图
类关系图很清晰,就不再多说了,我们直接来看源码:
4.源码分析
1.LoadingDrawable的实现:
首先我们来看看LoadingDrawable
是如何实现的。代码如下:
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);
}
};
//构造方法
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
的并且实现了Animatable
接口,Drawable
简单的来说就是可以通过Canvas
来绘制出图形或者图像的类。通俗的抽象就是:一些能被画出来的东西。想必大家也都很熟悉了。对于Animatable
来说,其实就是Android
提供给需要实现动画的Drawable
需要实现的接口,它分别有start()
、stop()
和isRunning()
方法很显然应该是控制动画的开始和停止。
对于自定义的Drawable
,draw()
方法应该是最重要的了。这里可以看出draw()
方法中直接交给了mLoadingRender
来处理。看过我们以前分析的几篇动画库的同学应该知道,mLoadingRender
肯定就是所有动画的父类了。然后根据父类的抽象方法来分别做具体的实现,从而实现不同的动画。所以我们接着来看LoadingRenderer
的实现:
2.LoadingRenderer的实现:
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();
}
//抽象方法交给子类去实现
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);
}
//设置宽度,高度,线条宽度以及圆的半径等默认参数
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;
}
//设置ValueAnimator的参数
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
是一个抽象类,首先在构造方法里定义了一些参数的默认值,例如:宽高,描边宽度,圆的默认半径。以及初始化了一个ValueAnimator
,并在onAnimationUpdate()
方法里调用了computeRender()
以及invalidateSelf()
方法。
其中computeRender(float renderProgress)
方法里的renderProgress
就是0到1
不断变化的值,这个方法是用来计算当前动画绘制需要的参数,在实现动画的时候我们通常会使用这种方法根据当前的renderProgress
的值来计算当前需要绘制图像的参数,从而完成绘制。一般情况下我们都会在draw()
里直接根据renderProgress
来做计算然后直接进行绘制,但是这样写出的代码的可读性就不太好了,因为计算和绘制都写在了一起。LoadingDrawable
在这一点上就做的非常好。它通过computeRender(float renderProgress);
方法来计算好能直接被draw();
方法使用的参数。同时draw()
方法里就只负责绘制的逻辑。在计算比较复杂的场景这样做能极大的提高代码的可读性。这一点非常值得我们学习。
看完了LoadingRenderer
的实现,接下来我们就来看看其中一个具体的实现类到底是如何实现的。LoadingRenderer
目前实现了8种不同的动画,具体在LoadingRenderer
的github
主页上都可以看到。大家也可以选自己喜欢的动画去分析。(由于我的数学很渣。。)我们这次就分析一个较为简单的动画WhorlLoadingRenderer
。就是下图左上角的这个动画:
3.WhorlLoadingRenderer
的实现:
首先我们先来分解一下这个动画:
- 首先是不断旋转的。
- 先从初始点绘制出一个弧形,然后弧形再不断的缩短直至重新变为一个点,最终不断循环。
其中还有一些细节比如三条弧线的位置以及线条宽度等等,我们来看看WhorlLoadingRenderer
是怎样实现的:
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);
//设置Paint的参数
setupPaint();
//添加动画监听
addRenderListener(mAnimatorListener);
}
private void setupPaint() {
mColors = DEFAULT_COLORS;
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(getStrokeWidth());
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeCap(Paint.Cap.ROUND);
//设置内边距
setInsets((int) getWidth(), (int) getHeight());
}
@Override
public void draw(Canvas canvas, Rect bounds) {
//保存当前画布状态
int saveCount = canvas.save();
//旋转画布
canvas.rotate(mGroupRotation, bounds.exactCenterX(), bounds.exactCenterY());
//设置绘制的RectF
RectF arcBounds = mTempBounds;
arcBounds.set(bounds);
//设置内边距
arcBounds.inset(mStrokeInset, mStrokeInset);
if (mStartTrim == mEndTrim) {
mStartTrim = mEndTrim + getMinProgressArc();
}
//开始的角度
float startAngle = (mStartTrim + mRotation) * DEGREE_360;
//结束的角度
float endAngle = (mEndTrim + mRotation) * DEGREE_360;
//角度范围
float sweepAngle = endAngle - startAngle;
//根据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);
}
//恢复画布状态
canvas.restoreToCount(saveCount);
}
// 根据需要绘制弧线的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));
}
//计算画布整体的旋转角度
mGroupRotation = ((FULL_ROTATION / NUM_POINTS) * renderProgress) + (FULL_ROTATION * (mRotationCount / NUM_POINTS));
//计算弧线点的旋转
mRotation = originRotation + (ROTATION_FACTOR * renderProgress);
invalidateSelf();
}
@Override
public void reset() {
resetOriginals();
}
//设置内边距,为了使动画的绘制居中
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;
}
//重置初始参数
private void resetOriginals() {
mOriginStartTrim = 0;
mOriginEndTrim = 0;
mOriginRotation = 0;
setStartTrim(0);
setEndTrim(0);
setRotation(0);
}
//获得最小弧度,
private float getMinProgressArc() {
//Math.toRadians将角度转换为弧度,这里的角度是描边的宽度 / 周长。所以绘制出的初始是一个点
return (float) Math.toRadians(getStrokeWidth() / (2 * Math.PI * getCenterRadius()));
}
}
从WhorlLoadingRenderer
的代码中可以看出,首先在构造方法里设置了Paint
的参数以及添加了动画监听mAnimatorListener
。然后当start()
方法触发时,会不断的调用computeRender()
和draw()
方法。其中computeRender()
负责计算mStartTrim
、mEndTrim
、mGroupRotation
等参数。draw()
方法根据计算得到的值来绘制出对应的图形。从而最终形成动画。
5.注意事项
在使用LoadingDrawable
过程中我们会发现使用ImageView
的setImageDrawable()
方法与使用View
的setBackground()
效果并不一样:
使用ImageView
的setImageDrawable()
方法效果如下:
而使用View
的setBackground()
方法效果如下:
同样的实现代码,两种调用方法的效果竟然不一样。setBackground()
中的圆环变大了,而线条变细了,那这到底是因为什么呢?我们只能从这两个方法的源码中找答案了。我们来看看setImageDrawable()
和setBackground()
是如何实现的(源码对应Android API 23
):
1.ImageView的setImageDrawable()
方法的实现:
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
是否为空,然后赋值了宽高,紧接着调用了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;
}
}
设置Callback
以及一些参数,这里是把ImageView
设置成了mDrawable
的Callback
,所以当调用LoadingDrawable
中mCallback
的invalidateSelf();
方法时其实是调用了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
的宽高设置给drawable
。如果drawable
有宽高的话,那么ImageView
则会自身缩放来适应drawable
。具体的缩放是通过Matrix
来做的。有兴趣的同学可以自行研究。其实这里设置了mDrawable
的宽高,所以在LoadingDrawable
类里的draw()
方法:
@Override
public void draw(Canvas canvas) {
//直接交给mLoadingRender的draw()方法
mLoadingRender.draw(canvas, getBounds());
}
中的getBounds()
有可能会被ImageView
重新设置。所以如果把ImageView
的ScaleType
设置成ScaleType.FIT_XY
那么结果就会和setBackground()
一样。接下来我们看看setBackground()
是怎么实现的:
2.View
的setBackground()
方法实现
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);
}
我们发现方法里就只把drawable
赋值给了mBackground
并没有操作drawable
的大小。省略的部分代码也没有相关逻辑。但是我们知道最终mBackground
是要被绘制出来的。我们去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();
}
}
省略部分绘制背景的代码,最终我们发现会将View
的宽高设置给mBackground
。所以我们就找出了为什么setBackground()
方法会绘制出不一样的效果。所以这里推荐LoadingDrawable
和ImageView
配合使用。如果配合View
使用可能还需要自己去手动调整一些参数。
6.个人评价
LoadingDrawable
实现了多种实用的Loading
动画,并且在一些特定的业务场景下,Drawable
使用起来更加方便。除了需要注意上一条的注意事项之外。LoadingDrawable
非常适合在项目中使用。而且LoadingDrawable
的代码相当规范。如果你的项目里有类似动画的需求,结合LoadingDrawable
一定能让你事半功倍!
我每周会写一篇源代码分析的文章,以后也可能会有其他主题.
如果你喜欢我写的文章的话,欢迎关注我的新浪微博@达达达达sky
地址: http://weibo.com/u/2030683111
每周我会第一时间在微博分享我写的文章,也会积极转发更多有用的知识给大家.