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