循序渐进学 LoadingDrawable
来源:互联网 发布:上海中科软件 编辑:程序博客网 时间:2024/06/01 09:19
源码地址
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
- 循序渐进学 LoadingDrawable
- 循序渐进学WinPcap
- 循序渐进学WinPcap
- 循序渐进学J2Me,第一步
- 循序渐进学编程
- 循序渐进学编程
- 循序渐进学编程
- 循序渐进学编程
- 循序渐进学编程
- 循序渐进学HTTPClient
- 循序渐进学J2Me,第二步
- 循序渐进学J2Me,第三步
- 循序渐进学J2Me,第四步
- 循序渐进学J2Me,第五步
- Implements Developer 循序渐进学编程
- 循序渐进,碰到什么学什么
- IOS开发-循序渐进学开发
- 《循序渐进学docker》reading note
- Mysql 关系
- Java与JavaScript的区别你明白吗?
- 高薪面试题二
- Redis常用命令
- SSIS SSAS 的优化手段总结
- 循序渐进学 LoadingDrawable
- [DP 容斥原理] HDU 5519 Kykneion asma
- 范式
- ubuntu16.0.4 设置固定ip地址
- fiddler界面介绍及使用
- Ubuntu挂载远程共享目录到本地
- 实现微信内置浏览器全屏播放模式下html元素漂浮
- 四大图片加载框架之最 old ImageLoader , 但是有些老项目还在用的!加载 https
- 连接查询