循序渐进学 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

1 0
原创粉丝点击