安卓开发博客Android开发Android 开发技术分享

LoadingDrawable源码分析

2016-04-24  本文已影响5416人  SkyKai

项目地址:LoadingDrawable,本文分析版本: 979291e

1.简介

LoadingDrawable是一个使用Drawable来绘制Loading动画的项目,由于使用Drawable的原因可以结合任何View使用,并且替换方便。目前已经实现了8种动画,而且项目作者dinuscxj表示后期会维护至20多种动画。对于动画项目,我们之前有分析过:HTextViewJJSearchViewAnim。此类项目的结构大家应该比较熟悉了。那么我们就来一起看看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.类关系图

LoadingDrawable.png

类关系图很清晰,就不再多说了,我们直接来看源码:

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()方法很显然应该是控制动画的开始和停止。
对于自定义的Drawabledraw()方法应该是最重要的了。这里可以看出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种不同的动画,具体在LoadingRenderergithub主页上都可以看到。大家也可以选自己喜欢的动画去分析。(由于我的数学很渣。。)我们这次就分析一个较为简单的动画WhorlLoadingRenderer。就是下图左上角的这个动画:

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设置成了mDrawableCallback,所以当调用LoadingDrawablemCallbackinvalidateSelf();方法时其实是调用了ImageViewinvalidateDrawable()方法从而更新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或者ImageViewScaleTypeScaleType.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重新设置。所以如果把ImageViewScaleType设置成ScaleType.FIT_XY那么结果就会和setBackground()一样。接下来我们看看setBackground()是怎么实现的:

2.ViewsetBackground()方法实现


    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是要被绘制出来的。我们去Viewdraw()方法看看:


    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()方法会绘制出不一样的效果。所以这里推荐LoadingDrawableImageView配合使用。如果配合View使用可能还需要自己去手动调整一些参数。

6.个人评价

LoadingDrawable实现了多种实用的Loading动画,并且在一些特定的业务场景下,Drawable使用起来更加方便。除了需要注意上一条的注意事项之外。LoadingDrawable非常适合在项目中使用。而且LoadingDrawable的代码相当规范。如果你的项目里有类似动画的需求,结合LoadingDrawable一定能让你事半功倍!

我每周会写一篇源代码分析的文章,以后也可能会有其他主题.
如果你喜欢我写的文章的话,欢迎关注我的新浪微博@达达达达sky
地址: http://weibo.com/u/2030683111
每周我会第一时间在微博分享我写的文章,也会积极转发更多有用的知识给大家.

上一篇 下一篇

猜你喜欢

热点阅读