Android动画 - AndroidFillableLoaders源码分析

AndroidFillableLoaders地址

先看一下使用AndroidFillableLoaders生成的动画效果:

这里写图片描述

动画效果很赞,AndroidFillableLoaders库让我们可以方便的实现相对复杂的动画。

AndroidFillableLoaders的github上,比较详细的说明了AndroidFillableLoaders的使用。知道了其怎么使用,就知道使用其时,我们首先需要使用FillableLoaderBuilder来对想要达到的动画效果进行设置。

下面我们就看一下FillableLoaderBuilder中主要代码:

FillableLoader构建器FillableLoaderBuilder

public class FillableLoaderBuilder {

  private ViewGroup parent;//待添加到的父视图
  private ViewGroup.LayoutParams params;//当前视图布局参数

  private int strokeColor = -1;//轮廓线颜色
  private int fillColor = -1;//填充颜色
  private int strokeWidth = -1;//轮廓线宽度
  private int originalWidth = -1;//原始svg的宽度
  private int originalHeight = -1;//原始svg的高度
  private int strokeDrawingDuration = -1;//轮廓绘制时间
  private int fillDuration = -1;//填充时间
  private boolean percentageEnabled;//启动填充百分比
  private float percentage;//填充百分比(0, 100)
  private ClippingTransform clippingTransform;//裁剪转换,用于设置填充样式
  private String svgPath;//绘制路径

  public FillableLoader build() {
    return new FillableLoader(parent, params, strokeColor, fillColor, strokeWidth, originalWidth,
        originalHeight, strokeDrawingDuration, fillDuration, clippingTransform, svgPath,
        percentageEnabled, percentage);
  }
}

其内部实现就是存储绘制的相关属性,并在build()方法时,通过这些设置的参数,构建FillableLoader。

下面我们看一下FillableLoader内部的实现。

FillableLoader属性及构造方法

public class FillableLoader extends View {

  //该部分参数说明同FillableLoaderBuilder中的
  private int strokeColor, fillColor, strokeWidth;
  private int originalWidth, originalHeight;
  private int strokeDrawingDuration, fillDuration;
  private ClippingTransform clippingTransform;
  private boolean percentageEnabled;
  private float percentage;
  private String svgPath;

  private PathData pathData;//svg解析后的路径数据
  private Paint dashPaint;//轮廓绘制画笔
  private Paint fillPaint;//填充时使用的画笔
  private int drawingState;//绘制状态
  private long initialTime;//绘制启动时间

  private int viewWidth;//当前布局宽
  private int viewHeight;//当前布局高

  private Interpolator animInterpolator;//轮廓绘制插值器
  private OnStateChangeListener stateChangeListener;//状态改变回调

  private float previousFramePercentage;
  private long previousFramePercentageTime;

  FillableLoader(ViewGroup parent, ViewGroup.LayoutParams params, int strokeColor, int fillColor,
      int strokeWidth, int originalWidth, int originalHeight, int strokeDrawingDuration,
      int fillDuration, ClippingTransform transform, String svgPath, boolean percentageEnabled,
      float fillPercentage) {

    super(parent.getContext());

    this.strokeColor = strokeColor;
    this.fillColor = fillColor;
    this.strokeWidth = strokeWidth;
    this.strokeDrawingDuration = strokeDrawingDuration;
    this.fillDuration = fillDuration;
    this.clippingTransform = transform;
    this.originalWidth = originalWidth;
    this.originalHeight = originalHeight;
    this.svgPath = svgPath;
    this.percentageEnabled = percentageEnabled;
    this.percentage = fillPercentage;

    init();
    parent.addView(this, params);
  }
}

FillableLoader继承自View,所以FillableLoader本身是一个可以用于界面展示的视图。

FillableLoader的构造器内主要设置了我们上面通过构建器设置的各种绘制属性。并将FillableLoader视图添加到父视图中。其中有一个init()方法,下面我们看一下init()方法的实现:

FillableLoader的init()方法

private void init() {
    drawingState = State.NOT_STARTED;

    initDashPaint();
    initFillPaint();

    animInterpolator = new DecelerateInterpolator();
    setLayerType(LAYER_TYPE_SOFTWARE, null);
  }

我们看见init()方法中,开始设置了绘制状态为“未开始状态”,然后初始化了轮廓绘制画笔和填充绘制画笔,并将轮廓绘制插值器设置为减速插值器,最后还设置了图层使用软件渲染。

我们先看一下绘制状态的定义:

绘制状态State

public class State {
  public static final int NOT_STARTED = 0;//未开始
  public static final int STROKE_STARTED = 1;//轮廓绘制开始
  public static final int FILL_STARTED = 2;//填充开始
  public static final int FINISHED = 3;//绘制完成
}

再看一下两种画笔的具体初始化:

FillableLoader的initDashPaint()和initFillPaint()方法

private void initDashPaint() {
    dashPaint = new Paint();
    dashPaint.setStyle(Paint.Style.STROKE);
    dashPaint.setAntiAlias(true);
    dashPaint.setStrokeWidth(strokeWidth);
    dashPaint.setColor(strokeColor);
}

private void initFillPaint() {
    fillPaint = new Paint();
    fillPaint.setAntiAlias(true);
    fillPaint.setStyle(Paint.Style.FILL);
    fillPaint.setColor(fillColor);
}

画笔的初始化,只是简单的根据设置的参数进行了设置。

我们查看FillableLoader代码,发现它重写了view的onSizeChanged(int w, int h, int oldw, int oldh)方法,我们知道该方法会在View大小改变时调用,view被加入到父视图时会被调用。所以其也会在具体绘制之前调用。下面我们看下其具体实现:

FillableLoader重写View的onSizeChanged(int w, int h, int oldw, int oldh)方法

super.onSizeChanged(w, h, oldw, oldh);
    viewWidth = w;
    viewHeight = h;
    buildPathData();

方法很简单,除了设置了视图的宽、高。只调用了buildPathData()方法。下面我们重点看一下buildPathData()方法的具体实现:

FillableLoader中的buildPathData方法

private void buildPathData() {
    SvgPathParser parser = getPathParser();
    pathData = new PathData();
    try {
      pathData.path = parser.parsePath(svgPath);
    } catch (ParseException e) {
      pathData.path = new Path();
    }

    PathMeasure pm = new PathMeasure(pathData.path, true);
    while (true) {
      pathData.length = Math.max(pathData.length, pm.getLength());
      if (!pm.nextContour()) {
        break;
      }
    }
  }

要理解上面这段代码的意思,我们需要看下SvgPathParser类和PathData数据结构的实现。

class PathData {
  Path path;//android中的路径
  float length;//路径长度
}

PathData很简单,只是有一个路径和一个路径长度的存储。

接下来我们重点看看SvgPathParser。其将Android不能识别的SVG转换成了Android绘图能够使用的Path。

SvgPathParser:将SVG转换成Android能够识别的Path

public class SvgPathParser {
  private static final int TOKEN_ABSOLUTE_COMMAND = 1;
  private static final int TOKEN_RELATIVE_COMMAND = 2;
  private static final int TOKEN_VALUE = 3;
  private static final int TOKEN_EOF = 4;

  private int mCurrentToken;
  private PointF mCurrentPoint = new PointF();
  private int mLength;
  private int mIndex;
  private String mPathString;

  protected float transformX(float x) {
    return x;
  }

  protected float transformY(float y) {
    return y;
  }

  public Path parsePath(String s) throws ParseException {
    mCurrentPoint.set(Float.NaN, Float.NaN);
    mPathString = s;
    mIndex = 0;
    mLength = mPathString.length();

    PointF tempPoint1 = new PointF();
    PointF tempPoint2 = new PointF();
    PointF tempPoint3 = new PointF();

    Path p = new Path();
    p.setFillType(Path.FillType.WINDING);

    boolean firstMove = true;
    while (mIndex < mLength) {
      char command = consumeCommand();
      boolean relative = (mCurrentToken == TOKEN_RELATIVE_COMMAND);
      switch (command) {
        case 'M':
        case 'm': {
          // move command
          boolean firstPoint = true;
          while (advanceToNextToken() == TOKEN_VALUE) {
            consumeAndTransformPoint(tempPoint1, relative && mCurrentPoint.x != Float.NaN);
            if (firstPoint) {
              p.moveTo(tempPoint1.x, tempPoint1.y);
              firstPoint = false;
              if (firstMove) {
                mCurrentPoint.set(tempPoint1);
                firstMove = false;
              }
            } else {
              p.lineTo(tempPoint1.x, tempPoint1.y);
            }
          }
          mCurrentPoint.set(tempPoint1);
          break;
        }

        case 'C':
        case 'c': {
          // curve command
          if (mCurrentPoint.x == Float.NaN) {
            throw new ParseException("Relative commands require current point", mIndex);
          }

          while (advanceToNextToken() == TOKEN_VALUE) {
            consumeAndTransformPoint(tempPoint1, relative);
            consumeAndTransformPoint(tempPoint2, relative);
            consumeAndTransformPoint(tempPoint3, relative);
            p.cubicTo(tempPoint1.x, tempPoint1.y, tempPoint2.x, tempPoint2.y, tempPoint3.x,
                tempPoint3.y);
          }
          mCurrentPoint.set(tempPoint3);
          break;
        }

        case 'L':
        case 'l': {
          // line command
          if (mCurrentPoint.x == Float.NaN) {
            throw new ParseException("Relative commands require current point", mIndex);
          }

          while (advanceToNextToken() == TOKEN_VALUE) {
            consumeAndTransformPoint(tempPoint1, relative);
            p.lineTo(tempPoint1.x, tempPoint1.y);
          }
          mCurrentPoint.set(tempPoint1);
          break;
        }

        case 'H':
        case 'h': {
          // horizontal line command
          if (mCurrentPoint.x == Float.NaN) {
            throw new ParseException("Relative commands require current point", mIndex);
          }

          while (advanceToNextToken() == TOKEN_VALUE) {
            float x = transformX(consumeValue());
            if (relative) {
              x += mCurrentPoint.x;
            }
            p.lineTo(x, mCurrentPoint.y);
          }
          mCurrentPoint.set(tempPoint1);
          break;
        }

        case 'V':
        case 'v': {
          // vertical line command
          if (mCurrentPoint.x == Float.NaN) {
            throw new ParseException("Relative commands require current point", mIndex);
          }

          while (advanceToNextToken() == TOKEN_VALUE) {
            float y = transformY(consumeValue());
            if (relative) {
              y += mCurrentPoint.y;
            }
            p.lineTo(mCurrentPoint.x, y);
          }
          mCurrentPoint.set(tempPoint1);
          break;
        }

        case 'Z':
        case 'z': {
          // close command
          p.close();
          break;
        }
      }
    }

    return p;
  }

  private int advanceToNextToken() {
    while (mIndex < mLength) {
      char c = mPathString.charAt(mIndex);
      if ('a' <= c && c <= 'z') {
        return (mCurrentToken = TOKEN_RELATIVE_COMMAND);
      } else if ('A' <= c && c <= 'Z') {
        return (mCurrentToken = TOKEN_ABSOLUTE_COMMAND);
      } else if (('0' <= c && c <= '9') || c == '.' || c == '-') {
        return (mCurrentToken = TOKEN_VALUE);
      }

      // skip unrecognized character
      ++mIndex;
    }

    return (mCurrentToken = TOKEN_EOF);
  }

  private char consumeCommand() throws ParseException {
    advanceToNextToken();
    if (mCurrentToken != TOKEN_RELATIVE_COMMAND && mCurrentToken != TOKEN_ABSOLUTE_COMMAND) {
      throw new ParseException("Expected command", mIndex);
    }

    return mPathString.charAt(mIndex++);
  }

  private void consumeAndTransformPoint(PointF out, boolean relative) throws ParseException {
    out.x = transformX(consumeValue());
    out.y = transformY(consumeValue());
    if (relative) {
      out.x += mCurrentPoint.x;
      out.y += mCurrentPoint.y;
    }
  }

  private float consumeValue() throws ParseException {
    advanceToNextToken();
    if (mCurrentToken != TOKEN_VALUE) {
      throw new ParseException("Expected value", mIndex);
    }

    boolean start = true;
    boolean seenDot = false;
    int index = mIndex;
    while (index < mLength) {
      char c = mPathString.charAt(index);
      if (!('0' <= c && c <= '9') && (c != '.' || seenDot) && (c != '-' || !start)) {
        // end of value
        break;
      }
      if (c == '.') {
        seenDot = true;
      }
      start = false;
      ++index;
    }

    if (index == mIndex) {
      throw new ParseException("Expected value", mIndex);
    }

    String str = mPathString.substring(mIndex, index);
    try {
      float value = Float.parseFloat(str);
      mIndex = index;
      return value;
    } catch (NumberFormatException e) {
      throw new ParseException("Invalid float value '" + str + "'.", mIndex);
    }
  }
}

我们看到,该方法中就是根据SVG路径的数据格式及功能,将其转换为了一条Path。如果你不知道SVG路径的相关定义语法,可以看下我之前的一篇文章。

上面给出了FillableLoader使用时的所有初始化过程,初始化完成后,会调动FillableLoader的start()方法,开始进行绘制。

FillableLoader中的start()方法

public void start() {
    checkRequirements();
    initialTime = System.currentTimeMillis();
    changeState(State.STROKE_STARTED);
    ViewCompat.postInvalidateOnAnimation(this);
  }
private void checkRequirements() {
    checkOriginalDimensions();
    checkPath();
  }

  private void checkOriginalDimensions() {
    if (originalWidth <= 0 || originalHeight <= 0) {
      throw new IllegalArgumentException(
          "You must provide the original image dimensions in order map the coordinates properly.");
    }
  }

  private void checkPath() {
    if (pathData == null) {
      throw new IllegalArgumentException(
          "You must provide a not empty path in order to draw the view properly.");
    }
  }

start()方法首先通过checkRequirements()方法检测是否满足绘制条件,如果满足,则初始化起始绘图时间,然后将绘制状态改为“轮廓绘制开始”,并触发开始绘制。开始绘制后,会调用View的onDraw()方法。

FillableLoader中的onDraw()方法

Override protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    if (!hasToDraw()) {
      return;
    }
    long elapsedTime = System.currentTimeMillis() - initialTime;
    drawStroke(canvas, elapsedTime);//根据时间逐渐绘制轮廓路径长度
    if (isStrokeTotallyDrawn(elapsedTime)) {//轮廓绘制完成
      if (drawingState < State.FILL_STARTED) {//状态转换到填充开始状态
        changeState(State.FILL_STARTED);
        previousFramePercentageTime = System.currentTimeMillis() - initialTime;
      }
      float fillPhase;
      if (percentageEnabled) {
        fillPhase = getFillPhaseForPercentage(elapsedTime);//获取当前填充率
      } else {
        fillPhase = getFillPhaseWithoutPercentage(elapsedTime);
      }
      clippingTransform.transform(canvas, fillPhase, this);//根据画布裁剪器,裁剪画布产生填充效果
      canvas.drawPath(pathData.path, fillPaint);//填充绘制路径
    }

    if (hasToKeepDrawing(elapsedTime)) {
      ViewCompat.postInvalidateOnAnimation(this);//没有绘制完成,持续触发绘制
    } else {
      changeState(State.FINISHED);
    }
  }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章