循序渐进学 LoadingDrawable

源码地址
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
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值