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);
    }
  }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章