LoadingDrawable

LoadingDrawable

LoadingDrawable是github上挺火的一个项目, 通过自定义Drawable来实现各式各样的Loading动画. 现已经有多种有意思的动画效果, 可以直接用在自己项目中, 或者仿照他的做法实现自己的loading动画.

LoadingDrawable使用

凡是好用的东西一般都使用非常简单, 当然这个也不例外, 只要有一个ImageView, 在代码中创建一个需要的LoadingDrawable, 再将drawable设给ImageView就可以了.


mIvMaterial= (ImageView) findViewById(R.id.material_view);

//使用自己需要的LoadingRenderer

mMaterialDrawable=newLoadingDrawable(newMaterialLoadingRenderer(this));

mIvMaterial.setImageDrawable(mMaterialDrawable);

LoadingDrawable分析

概述

LoadingDrawable通过自定义一个Drawable将不同的动画画出来, 其中的对于不同的动画对应不同的LoadingRender, 他们都是LoadingRender的子类, 分别重写了不同的计算和绘制的方法以实现不同的效果. 下面先看看他所涉及的类:

LoadingDrawable包结构

真的是很简捷清晰, 高亮是Drawable的子类, render包下是对不同动画的渲染器, 其中的LoadingRender是基类, 实现了基本的逻辑和定义绘制计算接口. 上面的几个包就是具体的动画实现.

LoadingDrawable

直接来看LoadingDrawable的实现:

//LoadingDrawable继承自Drawable, 可以自定义不用交互的可见控件
//实现Animatable接口, 他就成为一个动画, 可以在合适的时机显示或者停止
public class LoadingDrawable extends Drawable implements Animatable {
  private LoadingRenderer mLoadingRender;
  //定义一个Callback, 并将其传递给Render, 负责更新当前视图, 将其传递给Render可以避免Render去持有过多的引用
  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);
    }
  };

  //构造方法, 将Render保存在Drawable中, 方便后面将所有的计算绘制任务交给他
  public LoadingDrawable(LoadingRenderer loadingRender) {
    this.mLoadingRender = loadingRender;
    this.mLoadingRender.setCallback(mCallback);
  }

  //直接将绘制的任务交给Render去做, 后面的好多方法也是类似的, 直接给Render去处理
  @Override
  public void draw(Canvas canvas) {
    mLoadingRender.draw(canvas, getBounds());
  }我是我
  /*...省略部分代码...*/
}

代码量不大, 主要是将任务交给Render处理, 其中使用Callback的思想要学习一下.

LoadingRenderer

最核心的部分, 连接LoadingDrawable与各个具体动画, 规范各种动画接口的类, 就是LoadingRenderer, 下面我们看看他都做了什么.

public abstract class LoadingRenderer {

  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();

  //这里的start其实就是start渲染动画
  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;
  }

  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;
  }

  private void setupAnimators() {
    mRenderAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
    mRenderAnimator.setRepeatCount(Animation.INFINITE);
    mRenderAnimator.setRepeatMode(Animation.RESTART);
    //fuck you! the default interpolator is AccelerateDecelerateInterpolator
    mRenderAnimator.setInterpolator(new LinearInterpolator());
    mRenderAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
      @Override
      public void onAnimationUpdate(ValueAnimator animation) {
        //就是从这里把计算的任务与绘制的任务分开, 动画这部分只负责计算
        //真正的画出来是在draw里面
        computeRender((float) animation.getAnimatedValue());
        //通过CallBack通知Drable更新
        invalidateSelf();
      }
    });
  }
  
  /*....省略部分代码.....*/

  public void setDuration(long duration) {
    this.mDuration = duration;
    mRenderAnimator.setDuration(mDuration);
  }
}

LoadingRender对各种不同的动画进行了抽象, 将动画的计算和绘制拆分出来, 十分有利于后面不同动画的实现, 另外还对一些默认参数进行设置. 真正的计算和绘制都在下面的子类中, 我们分析一个MaterialLoadingRenderer.

MaterialLoadingRenderer

MaterialLoadingRenderer是右上方的效果, 三种颜色交替过渡出现, 圆先变大半圆再变小半圆, 然后整体还在转动,,初分析感觉好难, 下面细细看其代码, 不得不说以前自己写的动画都是什么玩意儿啊..下面看源码

public void computeRender(float renderProgress) {
    updateRingColor(renderProgress);

    // Moving the start trim only occurs in the first 50% of a
    // single ring animation
    if (renderProgress <= START_TRIM_DURATION_OFFSET) {
        //除定前半程比例, 这里相当于把一个动画分成了两个动画, 前半段是只移动头, 后半段只移动尾巴
        //这里把原来一共的比例换算到前半段的时间上来
        float startTrimProgress = renderProgress / START_TRIM_DURATION_OFFSET;
        //要开始的角度. 原始角度加已经过了的角度
        //向前伸出去的那个头,加原始角度(在一次动画中他是不变的, 等于上一次动画结束的地方)
        //再加最大的多半圈乘扫过的比例
        mStartDegrees = mOriginStartDegrees + MAX_SWIPE_DEGREES * MATERIAL_INTERPOLATOR.getInterpolation(startTrimProgress);
    }

    // Moving the end trim starts after 50% of a single ring
    // animation completes
    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) > MIN_SWIPE_DEGREE) {
        mSwipeDegrees = mEndDegrees - mStartDegrees;
    }

    //整个过程中画布一一直慢慢的转动
    mGroupRotation = ((FULL_GROUP_ROTATION / NUM_POINTS) * renderProgress) + (FULL_GROUP_ROTATION * (mRotationCount / NUM_POINTS));
    //不知道干啥的....
    mRotationIncrement = mOriginRotationIncrement + (MAX_ROTATION_INCREMENT * renderProgress);
}

过程中设了画笔的颜色, 颜色是动画前80%使用一个颜色, 后20%的时候使用

return ((startA + (int) (fraction * (endA - startA))) << 24)
        | ((startR + (int) (fraction * (endR - startR))) << 16)
        | ((startG + (int) (fraction * (endG - startG))) << 8)
        | ((startB + (int) (fraction * (endB - startB))));

计算两个颜色的过渡色, 就会产生过渡的颜色变化.
后面的计算基本如注释描述. 直接看draw方法.

public void draw(Canvas canvas, Rect bounds) {
    int saveCount = canvas.save();
    //对Canvas进行转动, 产生画的过程中首尾都在转动的效果
    canvas.rotate(mGroupRotation, bounds.exactCenterX(), bounds.exactCenterY());

    RectF arcBounds = mTempBounds;
    arcBounds.set(bounds);
    arcBounds.inset(mStrokeInset, mStrokeInset);

    mPaint.setColor(mCurrentColor);
    //绘制弧线, 就是Loading的主体
    canvas.drawArc(arcBounds, mStartDegrees, mSwipeDegrees, false, mPaint);

    canvas.restoreToCount(saveCount);
}

基本上是拿一到计算的数据进行绘制就可以了, 记得每次都要将canvas进行restore. 别的方法就是与动画相关的: 开始, 停止之类, 不再分析.
学习一个简单的Loading图就是这样, 后面会再分析一个使用图片的Loading图, 就可以根据自己的需求进行自定义各种动画了.

学习这个开源代码最大的收获就是感觉结果清晰, 每一个类, 每一个方法的责任都十分的明确, 十分值得我们学习.

推荐阅读更多精彩内容