先看一下使用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);
}
}