源码地址
https://github.com/dinuscxj/LoadingDrawable相关资料
http://www.jianshu.com/p/6e0ac5af4e8b
http://www.jianshu.com/p/1c3c6fc1b7ff
前言
LoadingDrawable是一个使用Drawable来绘制Loading动画的项目,由于使用Drawable的原因可以结合任何View使用,并且替换方便。比如一个简单的ImageView又或者一个自定义View或ViewGroup的背景,它都可以做到。我们先看一下效果图。
以上这些效果图,都是通过Drawable实现的,其中包括简单的转圈圈,复杂的水珠跳动,再或者是作者命名为怪物眼睛的最后一个动画(这确定是怪物眼睛?而不是怪物的其他部位?)这里我要说一下,这些动画基础实现是LoadingDrawable,但是核心还是数学,怎么计算出这些的位置才是关键。现在,就让我为大家从源码层次上分析LoadingDrawable。
LoadingDrawable
LoadingDrawable毫无疑问首先需要继承Drawable,其次需要实现Animatable。Drawable我们都很熟悉,但是Animatable是什么呢?我们来看官方文档的定义。
Interface that drawables supporting animations should implement.
Animatable其实就是Drawable支持动画的接口,其中只包括了如下的方法。
boolean isRunning ():返回动画是否运行
void start ():动画开始
void stop ():动画结束
我们了解了这些,那就可以开始看源码了,直接贴出源码。
public class LoadingDrawable extends Drawable implements Animatable {
private final LoadingRenderer mLoadingRender;
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
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
this.mLoadingRender.setBounds(bounds);
}
@Override
public void draw(Canvas canvas) {
if (!getBounds().isEmpty()) {
this.mLoadingRender.draw(canvas);
}
}
@Override
public void setAlpha(int alpha) {
this.mLoadingRender.setAlpha(alpha);
}
@Override
public void setColorFilter(ColorFilter cf) {
this.mLoadingRender.setColorFilter(cf);
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
@Override
public void start() {
this.mLoadingRender.start();
}
@Override
public void stop() {
this.mLoadingRender.stop();
}
@Override
public boolean isRunning() {
return this.mLoadingRender.isRunning();
}
@Override
public int getIntrinsicHeight() {
return (int) this.mLoadingRender.mHeight;
}
@Override
public int getIntrinsicWidth() {
return (int) this.mLoadingRender.mWidth;
}
}
首先可以看到声明了一个LoadingRenderer,然后声明了一个Callback。而该类的初始化就是传入LoadingRenderer并且赋值设置Callback。而其余方法都是通过LoadingRenderer的实例实现的。由于LoadingRenderer比较关键,我们先放下它,看Callback。
在Drawable源码中有这样一段话
A Drawable can perform animations by calling back to its client through the {Callback} interface. All clients should support this interface (via {#setCallback}) so that animations will work. A simple way to do this is through the system facilities such as={ android.view.View#setBackground(Drawable)} and={android.widget.ImageView}.
也就是说每一个想通过Drawable执行动画的View都应该实现Callback。但LoadingDrawable中的Callback并不是传给View,它只是传入LoadingRenderer中,让LoadingRenderer执行Callback的方法,比如invalidateDrawable()。而方法内部实际调用的是Drawable的方法invalidateSelf()。
其余方法中,getOpacity()返回的是透明,这样处理不会覆盖下一层View。onBoundsChange(Rect bounds)是每次绘制时区域发生变化调用的方法。getIntrinsicHeight/Width()是告诉设置Drawable的View,它的高和宽。
LoadingRenderer
刚刚讲完了LoadingDrawable,发现其中都是通过LoadingRenderer实现的,我们现在就来看它的源码
public abstract class LoadingRenderer {
private static final long ANIMATION_DURATION = 1333;
private static final float DEFAULT_SIZE = 56.0f;
private final ValueAnimator.AnimatorUpdateListener mAnimatorUpdateListener
= new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
computeRender((float) animation.getAnimatedValue());
invalidateSelf();
}
};
protected final Rect mBounds = new Rect();
private Drawable.Callback mCallback;
private ValueAnimator mRenderAnimator;
protected long mDuration;
protected float mWidth;
protected float mHeight;
public LoadingRenderer(Context context) {
initParams(context);
setupAnimators();
}
@Deprecated
protected void draw(Canvas canvas, Rect bounds) {
}
protected void draw(Canvas canvas) {
draw(canvas, mBounds);
}
protected abstract void computeRender(float renderProgress);
protected abstract void setAlpha(int alpha);
protected abstract void setColorFilter(ColorFilter cf);
protected abstract void reset();
protected void addRenderListener(Animator.AnimatorListener animatorListener) {
mRenderAnimator.addListener(animatorListener);
}
void start() {
reset();
mRenderAnimator.addUpdateListener(mAnimatorUpdateListener);
mRenderAnimator.setRepeatCount(ValueAnimator.INFINITE);
mRenderAnimator.setDuration(mDuration);
mRenderAnimator.start();
}
void stop() {
mRenderAnimator.removeUpdateListener(mAnimatorUpdateListener);
mRenderAnimator.setRepeatCount(0);
mRenderAnimator.setDuration(0);
mRenderAnimator.end();
}
boolean isRunning() {
return mRenderAnimator.isRunning();
}
void setCallback(Drawable.Callback callback) {
this.mCallback = callback;
}
void setBounds(Rect bounds) {
mBounds.set(bounds);
}
private float dip2px(Context context, float dpValue) {
float scale = context.getResources().getDisplayMetrics().density;
return dpValue * scale;
}
private void initParams(Context context) {
mWidth = dip2px(context, DEFAULT_SIZE);
mHeight = dip2px(context, DEFAULT_SIZE);
mDuration = ANIMATION_DURATION;
}
private void setupAnimators() {
mRenderAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
mRenderAnimator.setRepeatCount(Animation.INFINITE);
mRenderAnimator.setRepeatMode(ValueAnimator.RESTART);
mRenderAnimator.setDuration(mDuration);
mRenderAnimator.setInterpolator(new LinearInterpolator());
mRenderAnimator.addUpdateListener(mAnimatorUpdateListener);
}
private void invalidateSelf() {
mCallback.invalidateDrawable(null);
}
}
首先是默认的持续时间和宽高,然后则是一个ValueAnimator.AnimatorUpdateListener。学过属性动画的应该很熟悉,没学过的我推荐郭霖大大的博客,保证一学就会。
Android属性动画完全解析(上),初识属性动画的基本用法 :http://blog.csdn.net/guolin_blog/article/details/43536355
Android属性动画完全解析(中),ValueAnimator和ObjectAnimator的高级用法:http://blog.csdn.net/guolin_blog/article/details/43536355
Android属性动画完全解析(下),Interpolator和ViewPropertyAnimator的用法:http://blog.csdn.net/guolin_blog/article/details/44171115
其中最关键的就是computeRender((float) animation.getAnimatedValue())通过当前进度计算当前绘制的图案。我们发现这个方法是一个抽象方法,需要我们具体实现。
剩下的方法中initParams()为初始化宽高和时间,setupAnimators()为初始化动画,其中设置为float类型从0到1变化,Interpolator为正常线性增长,等。
这里我们需要注意的是mBounds的变化。在LoadingDrawable中发现是所属的View区域变化时进行赋值。但mBounds真正初始化应该发生在所属View的方法中,这里面就存在ImageView.setImageDrawable和View.setBackground设置方法不同的一些问题了。比如下面
这两幅图就是差别,前者为ImageView.setImageDrawable,后者为View.setBackground。
ImageView.setImageDrawable
我们先来看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();
}
}
其中判断是否发生变化,然后更新Drawable。我们直接看updateDrawable方法。由于代码略长,这里只留下我们需要的代码。
private void updateDrawable(Drawable d) {
...
mDrawable = d;
if (d != null) {
d.setCallback(this);
d.setLayoutDirection(getLayoutDirection());
...
mDrawableWidth = d.getIntrinsicWidth();
mDrawableHeight = d.getIntrinsicHeight();
...
configureBounds();
} else {
mDrawableWidth = mDrawableHeight = -1;
}
}
首先看是否为空,如果不为空,直接把ImageView中的Callback交给了Drawable,然后就是获取Drawable的宽高,接着就是最关键的configureBounds方法。我们也只留下我们需要的代码。
private void configureBounds() {
if (mDrawable == null || !mHaveFrame) {
return;
}
final int dwidth = mDrawableWidth;
final int dheight = mDrawableHeight;
final int vwidth = getWidth() - mPaddingLeft - mPaddingRight;
final int vheight = getHeight() - mPaddingTop - mPaddingBottom;
final 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.
mDrawable.setBounds(0, 0, dwidth, dheight);
if (ScaleType.MATRIX == mScaleType) {
// Use the specified matrix as-is.
if (mMatrix.isIdentity()) {
mDrawMatrix = null;
} else {
mDrawMatrix = mMatrix;
}
}
...
else {
// Generate the required transform.
mTempSrc.set(0, 0, dwidth, dheight);
mTempDst.set(0, 0, vwidth, vheight);
mDrawMatrix = mMatrix;
mDrawMatrix.setRectToRect(mTempSrc, mTempDst, scaleTypeToScaleToFit(mScaleType));
}
}
}
其中会判断Drawable宽高是否大于0,否则将ImageView的宽高交给Drawable。然后就是判断ImageView的填充类型。注意这里,类型默认是中心填满,也就是说即使你设置了Drawable大小,但是ImageView没设置类型,还是会缩放到ImageView的区域。
View.setBackground
我们接着来看View.setBackground方法。
public void setBackground(Drawable background) {
//noinspection deprecation
setBackgroundDrawable(background);
}
/**
* @deprecated use {@link #setBackground(Drawable)} instead
*/
@Deprecated
public void setBackgroundDrawable(Drawable background) {
...
if (background == mBackground) {
return;
}
boolean requestLayout = false;
mBackgroundResource = 0;
if (mBackground != null) {
...
mBackground.setCallback(null);
unscheduleDrawable(mBackground);
}
if (background != null) {
......
mBackground = background;
......
}
......
if (requestLayout) {
requestLayout();
}
mBackgroundSizeChanged = true;
invalidate(true);
}
这里直接是将Drawable交给了mBackground,并不会像ImageView会有具体操作。而具体反映在界面的实际上是drawBackground()方法。
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,同样没有进行操作。所以也就会导致了一个问题,用这种方法设置的区域一直是背景区域。而具体的解决方式在具体LoadingRenderer实例中,接下来我们来看一个实例,动画效果的第一个例子,WhorlLoadingRenderer。
WhorlLoadingRenderer
我们先一步步来,看这个动画,想是怎么设计的。
很明显能看出来,里面的两个动画生成其实就是缩小版的最外层动画,那么我们先来想最外层动画如何实现就可以了。
首先是一个点,然后开始画移动的弧。一个移动的弧首先肯定有先动的地方,然后再有后动的地方,分别是起点终点。我们仔细观察一下可以发现起点画的速度先快,然后再慢,而终点相反,最后追上起点。也就是说两者有一个速度差。那么我们可不可以这样,起点终点同时减去一次绘制中最慢的速度,也就是终点开始画的慢速度。这样让终点在起点画的时候不动,然后等到终点画的时候,起点再动。
这其实就是这个动画的一个雏形。随着进度增长,起点先动,终点不动,到达一个值后,起点停止,终点开始动。最后把整体加上一个统一的速度,而这个速度的实现,我们通过整体画布的旋转,也就是说,你感觉是起点终点在动,实际是画布旋转了。最后,为了让起点或者终点在移动的时候不是那么无趣,让他不匀速移动,产生一个先慢速然后快速最后再慢速的一个变速运动,这个变速值就是FastOutSlowInInterpolator产生的。
上面我叙述的这个移动的过程就是computeRender方法所做的。也是一个动画的核心。
接着就是绘制了。首先动画一般都是居中的吧,就要计算并扣除内边距。我们看到的动画是三个点,而且每个点绘制的颜色不一样,那么我们就需要做一个循环来判断每个点的情况,并针对每个点进行赋值。但是呢,这里有一个问题,那就是怎么做到一圈比一圈小的呢?作者很机智的通过了缩小画布的方式,也就是说,你画布小了,一个完整的弧,总不会画出去吧。这样就产生了一圈比一圈小的样子。接着就是旋转画布,也就是让他有个初速度。这样我们就画完了。
讲完设计,下面我们直接看源码,基本我都注释过啦
public class WhorlLoadingRenderer extends LoadingRenderer {
//贝塞尔变化的Interpolator,规律是慢快慢
private static final Interpolator MATERIAL_INTERPOLATOR = new FastOutSlowInInterpolator();
private static final int DEGREE_180 = 180;
private static final int DEGREE_360 = 360;
//循环次数
private static final int NUM_POINTS = 5;
//单次绘制画弧所占角度
private static final float MAX_SWIPE_DEGREES = 0.6f * DEGREE_360;
//一次循环所占角度
private static final float FULL_GROUP_ROTATION = 3.0f * DEGREE_360;
//起点绘制结束时进度
private static final float START_TRIM_DURATION_OFFSET = 0.5f;
//终点绘制结束时进度
private static final float END_TRIM_DURATION_OFFSET = 1.0f;
//圆半径
private static final float DEFAULT_CENTER_RADIUS = 12.5f;
//所画线宽度
private static final float DEFAULT_STROKE_WIDTH = 2.5f;
//颜色
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 RectF mTempArcBounds = new RectF();
//当前循环位置
private float mRotationCount;
private final Animator.AnimatorListener mAnimatorListener = new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
//动画开始,循环位置为0
mRotationCount = 0;
}
//一次绘制结束,进行下一次
@Override
public void onAnimationRepeat(Animator animator) {
super.onAnimationRepeat(animator);
//保存上一次位置
storeOriginals();
//下一次起点为上一次终点
mStartDegrees = mEndDegrees;
//当前循环位置
mRotationCount = (mRotationCount + 1) % (NUM_POINTS);
}
};
private int[] mColors;
//内边距
private float mStrokeInset;
//画布旋转角度
private float mGroupRotation;
//终点角度
private float mEndDegrees;
//起点角度
private float mStartDegrees;
//扫过角度
private float mSwipeDegrees;
//上一次终点角度
private float mOriginEndDegrees;
//上一次起点角度
private float mOriginStartDegrees;
//所画线宽度
private float mStrokeWidth;
//圆半径
private float mCenterRadius;
private WhorlLoadingRenderer(Context context) {
super(context);
init(context);
setupPaint();
addRenderListener(mAnimatorListener);
}
private void init(Context context) {
mColors = DEFAULT_COLORS;
mStrokeWidth = dip2px(context, DEFAULT_STROKE_WIDTH);
mCenterRadius = dip2px(context, DEFAULT_CENTER_RADIUS);
initStrokeInset(mWidth, mHeight);
}
private float dip2px(Context context, float dpValue) {
float scale = context.getResources().getDisplayMetrics().density;
return dpValue * scale;
}
//计算内边距,使动画居中
private void initStrokeInset(float width, float height) {
float minSize = Math.min(width, height);
float strokeInset = minSize / 2.0f - mCenterRadius;
float minStrokeInset = (float) Math.ceil(mStrokeWidth / 2.0f);
mStrokeInset = strokeInset < minStrokeInset ? minStrokeInset : strokeInset;
}
private void setupPaint() {
//平滑画边
mPaint.setAntiAlias(true);
//设置画笔粗度
mPaint.setStrokeWidth(mStrokeWidth);
//用划的方式进行画
mPaint.setStyle(Paint.Style.STROKE);
//设置画笔头类型,半圆
mPaint.setStrokeCap(Paint.Cap.ROUND);
}
@Override
protected void draw(Canvas canvas) {
int saveCount = canvas.save();
//设置缓存区域及内边距
mTempBounds.set(mBounds);
mTempBounds.inset(mStrokeInset, mStrokeInset);
//以画布中心为中心进行画布旋转
canvas.rotate(mGroupRotation, mTempBounds.centerX(), mTempBounds.centerY());
if (mSwipeDegrees != 0) {
//针对每一个线
for (int i = 0; i < mColors.length; i++) {
//序号越大,线越细,也就是说最外部是序号0
mPaint.setStrokeWidth(mStrokeWidth / (i + 1));
mPaint.setColor(mColors[i]);
//画弧,其中第四个参数为是否画出半径
canvas.drawArc(createArcBounds(mTempBounds, i), mStartDegrees + DEGREE_180 * (i % 2),
mSwipeDegrees, false, mPaint);
}
}
canvas.restoreToCount(saveCount);
}
//针对每个线确定画布区域
private RectF createArcBounds(RectF sourceArcBounds, int index) {
int intervalWidth = 0;
for (int i = 0; i < index; i++) {
//两条线间区域差为1.5倍上一个线宽度
intervalWidth += mStrokeWidth / (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);
mTempArcBounds.set(arcBoundsLeft, arcBoundsTop, arcBoundsRight, arcBoundsBottom);
return mTempArcBounds;
}
@Override
protected void computeRender(float renderProgress) {
//当目前变化进度小于起点结束进度
if (renderProgress <= START_TRIM_DURATION_OFFSET) {
//起点移动进度=当前变化进度/起点变化进度
float startTrimProgress = (renderProgress) / (1.0f - START_TRIM_DURATION_OFFSET);
//起点应该移动后的新角度=原角度+一次绘制角度*转换后起点应该移动的进度
mStartDegrees = mOriginStartDegrees + MAX_SWIPE_DEGREES * MATERIAL_INTERPOLATOR.getInterpolation(startTrimProgress);
}
//当目前变化进度大于起点结束进度
if (renderProgress > START_TRIM_DURATION_OFFSET) {
//终点移动进度=当前变化进度/终点变化进度
float endTrimProgress = (renderProgress - START_TRIM_DURATION_OFFSET) / (END_TRIM_DURATION_OFFSET - START_TRIM_DURATION_OFFSET);
//终点应该移动后的新角度=原角度+一次绘制角度*转换后终点应该移动的进度
mEndDegrees = mOriginEndDegrees + MAX_SWIPE_DEGREES * MATERIAL_INTERPOLATOR.getInterpolation(endTrimProgress);
}
//生成变化角度
if (Math.abs(mEndDegrees - mStartDegrees) > 0) {
mSwipeDegrees = mEndDegrees - mStartDegrees;
}
//生成画布旋转角度
mGroupRotation = ((FULL_GROUP_ROTATION / NUM_POINTS) * renderProgress) + (FULL_GROUP_ROTATION * (mRotationCount / NUM_POINTS));
}
@Override
protected void setAlpha(int alpha) {
mPaint.setAlpha(alpha);
}
@Override
protected void setColorFilter(ColorFilter cf) {
mPaint.setColorFilter(cf);
}
@Override
protected void reset() {
resetOriginals();
}
private void storeOriginals() {
mOriginEndDegrees = mEndDegrees;
mOriginStartDegrees = mEndDegrees;
}
private void resetOriginals() {
mOriginEndDegrees = 0;
mOriginStartDegrees = 0;
mEndDegrees = 0;
mStartDegrees = 0;
mSwipeDegrees = 0;
}
//应用变化
private void apply(Builder builder) {
this.mWidth = builder.mWidth > 0 ? builder.mWidth : this.mWidth;
this.mHeight = builder.mHeight > 0 ? builder.mHeight : this.mHeight;
this.mStrokeWidth = builder.mStrokeWidth > 0 ? builder.mStrokeWidth : this.mStrokeWidth;
this.mCenterRadius = builder.mCenterRadius > 0 ? builder.mCenterRadius : this.mCenterRadius;
this.mDuration = builder.mDuration > 0 ? builder.mDuration : this.mDuration;
this.mColors = builder.mColors != null && builder.mColors.length > 0 ? builder.mColors : this.mColors;
setupPaint();
initStrokeInset(this.mWidth, this.mHeight);
}
public static class Builder {
private Context mContext;
private int mWidth;
private int mHeight;
private int mStrokeWidth;
private int mCenterRadius;
private int mDuration;
private int[] mColors;
public Builder(Context mContext) {
this.mContext = mContext;
}
public Builder setWidth(int width) {
this.mWidth = width;
return this;
}
public Builder setHeight(int height) {
this.mHeight = height;
return this;
}
public Builder setStrokeWidth(int strokeWidth) {
this.mStrokeWidth = strokeWidth;
return this;
}
public Builder setCenterRadius(int centerRadius) {
this.mCenterRadius = centerRadius;
return this;
}
public Builder setDuration(int duration) {
this.mDuration = duration;
return this;
}
public Builder setColors(int[] colors) {
this.mColors = colors;
return this;
}
public WhorlLoadingRenderer build() {
WhorlLoadingRenderer loadingRenderer = new WhorlLoadingRenderer(mContext);
loadingRenderer.apply(this);
return loadingRenderer;
}
}
}
看源码,我们注意到最后有一个Builder类,这其实就是建造者模式,也就是我们在使用Android中Dialog时的样子,通过这个Builder为该类赋值,最后应用。
我们直接来看如何用的
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.rl_tset);
//获取屏幕宽高
Point point = new Point();
WindowManager windowManager = getWindowManager();
windowManager.getDefaultDisplay().getSize(point);
int scrWidth = point.x;
int scrHeight = point.y;
LoadingViewGroup viewGroup = (LoadingViewGroup) findViewById(R.id.lvg_test);
ImageView imageView = (ImageView) findViewById(R.id.iv_test);
LoadingDrawable loadingDrawable = new LoadingDrawable(
new WhorlLoadingRenderer.Builder(this)
.setWidth(scrWidth)
.setHeight(scrWidth)
.setCenterRadius(scrWidth / 4)
.setStrokeWidth(scrWidth / 16)
.build());
//通过View.setBackground方式
viewGroup.setBackground(loadingDrawable);
//通过ImageView.setImageDrawable方式
//imageView.setImageDrawable(loadingDrawable);
//开始动画
loadingDrawable.start();
}
其中LoadingViewGroup为一个自定义ViewGroup。
最后一个问题
还记得我之前说过View.setBackground这种方式使用会有一些问题么?就是设置参数以后无效。因为我们用这种方式设置后,绘制区域一直是整个背景,而无法改变。下面说说我的解决方式。
private void reAdjustBound() {
float boundHeight = mBounds.bottom - mBounds.top;
float boundWidth = mBounds.right - mBounds.left;
if (boundHeight < mHeight || boundWidth < mWidth) {
mHeight = boundHeight;
mWidth = boundWidth;
initStrokeInset(mHeight, mWidth);
}
if (boundHeight > mHeight || boundWidth > mWidth) {
Rect rect = new Rect(
(int) ((boundWidth - mWidth) / 2 + mBounds.left),
(int) ((boundHeight - mHeight) / 2 + mBounds.top),
(int) ((boundWidth + mWidth) / 2 + mBounds.left),
(int) ((boundHeight + mHeight) / 2 + mBounds.top));
mBounds.set(rect);
}
}
就是上面的函数,通过判断当前区域宽高与设置的宽高比较,如果区域大于设置,直接将区域宽高换成设置宽高,如果小于,那么设置宽高换成区域宽高,这样就解决啦,然后再绘制前调用一下就可以啦。
修改后的代码
@Override
protected void draw(Canvas canvas) {
int saveCount = canvas.save();
reAdjustBound();
//设置缓存区域及内边距
mTempBounds.set(mBounds);
mTempBounds.inset(mStrokeInset, mStrokeInset);
//以画布中心为中心进行画布旋转
canvas.rotate(mGroupRotation, mTempBounds.centerX(), mTempBounds.centerY());
if (mSwipeDegrees != 0) {
//针对每一个线
for (int i = 0; i < mColors.length; i++) {
//序号越大,线越细,也就是说最外部是序号0
mPaint.setStrokeWidth(mStrokeWidth / (i + 1));
mPaint.setColor(mColors[i]);
//画弧,其中第四个参数为是否画出半径
canvas.drawArc(createArcBounds(mTempBounds, i), mStartDegrees + DEGREE_180 * (i % 2),
mSwipeDegrees, false, mPaint);
}
}
canvas.restoreToCount(saveCount);
}
好啦,大功告成!
已经把这个测试的源码上传到Github上啦,欢迎下载学习。
https://github.com/CMonoceros/LoadingDrawable