目录
Android高级动画(1)http://www.jianshu.com/p/48554844a2db
Android高级动画(2)http://www.jianshu.com/p/89cfd9042b1e
Android高级动画(3)http://www.jianshu.com/p/d6cc8d218900
Android高级动画(4)http://www.jianshu.com/p/91f8363c3a8c
一波未平
上一篇文章我们讲了Android中的矢量动画,虽然文中展示的Demo并不多,但是相信大家还是体会到了矢量动画的强大。这里再做一个温故总结:
Android中的矢量动画看似很繁杂,其实很简单,就三个类:vector、animated-vector、animated-selector
(1)vector:显示一个矢量图形,用SVG的语法构建path
(2)animated-vector:组合两个vector,让vector动起来
(3)animated-selector:组合两个animated-vector,实现双向切换动画
三个类的递进关系很明显。
一波又起
充分利用Android的矢量动画框架,我们已经可以做出非常惊艳的特效了,上篇文章展示的Demo简直就是渣渣。但是肯定有人发现问题了,Android系统提供的矢量动画框架有两个显著的缺点:
(1)vector、animated-vector、animated-selector都是通过xml文件来构建的,所有的效果都是写死的,并且Android没有为我们提供用代码动态构建矢量动画的方法。
(2)动画过程不受控制,不能控制动画进度,甚至连相关回调都没有
如何解决上面两个问题呢?下一位靓仔在哪里?
很尴尬,这次没有现成的方法给我们用,我们只能自己想办法解决了。
代码构建矢量动画
上面两个问题很明显第一个问题是关键点,第一个问题解决了,第二个问题就是小case。
上篇文章提到两种动画类型:pathMorphing和trimPath。
pathMorphing
我们要自己实现代码构建pathMorphing动画,首先得明白系统自带的动画是怎么实现的。由于上篇的Twitter例子太复杂了,我们换一个稍微简单的例子。
这是一个简单的两个path转换的demo,两个vector如下:
我们现在知道,path转换主要在path,其它参数无关紧要,所以我们单独把两段path提出来:
M99,349 C193,240,283,165,400,99 C525,172,611,246,701,348 C521,416,433,511,400,700 C356,509,285,416,99,349
M99,349 C297,346,376,210,400,99 C432,208,506,345,701,348 C629,479,549,570,400,700 C227,569,194,522,99,349
系统在两个path做转换时,其实就是把一个path中的每一个命令符参数渐变到第二个path对应的命令符参数,如下图所示:
这就是为什么pathMorphing要求两个path必须是同形path,否则是在变换时就找不到对应的值了。所以如果我们可以自己模拟出这个过程那不就ok了吗?实现这一点的关键就是Path类。
Path
android.graphics.Path类提供了一系列构建矢量路径的方法,每一个方法和SVG中的命令符对应:
M 对应 path.moveTo()
L 对应 path.lineTo()
Q 对应 path.quadTo()
C 对应 path.cubicTo()
所以我们可以解析上面的path路径字符串,然后转换成Path类对应的方法,构建出一个Path对象,最后调用canvas.drawPath(path, paint);把路径绘制出来就可以了。效果如下:
但是这样只是绘制一个path,并不是动画,我们要在两个path之间做转换动画,那就要解析两个path路径,然后开启一个ValueAnimator,根据ValueAniator的动画进度,把第一个path中的数据值变到第二个path对应的数值。
这里我就不把全部的源码写出来了,只列举一些关键性代码,全部代码请参考Github。
(1)SVGAction类,用于记录命令符和对应的命令参数
public static class SVGAction {
...
private String action;
private List valueFrom;
private List valueTo;
...
}
(2)解析path字符串为SVGAction
private void buildActions() {
if(path1 == null || path1.isEmpty() || path2 == null || path2.isEmpty()) {
Log.e(LOG_TAG, "pathString is null.");
return;
}
String[] arr1 = path1.split(" ");
String[] arr2 = path2.split(" ");
if(arr1.length != arr2.length) {
Log.e(LOG_TAG, "The length of path1 do not equals path2.");
return;
}
actions.clear();
for(int i = 0; i < arr1.length; i++) {
String str1 = arr1[i];
String str2 = arr2[i];
SVGAction action = new SVGAction();
if(str1.equalsIgnoreCase(SVGAction.ACTION_Z) && str2.equalsIgnoreCase(SVGAction.ACTION_Z)) {
action.setAction(SVGAction.ACTION_Z);
} else {
String actionStr1 = str1.substring(0, 1);
String actionStr2 = str2.substring(0, 1);
if(!actionStr1.equals(actionStr2)) {
Log.e(LOG_TAG, "path1 is not suitable for path2.");
return;
}
String valueStr1 = str1.substring(1, str1.length()).trim();
String valueStr2 = str2.substring(1, str2.length()).trim();
String[] values1 = valueStr1.split(",");
String[] values2 = valueStr2.split(",");
List valueFrom = new ArrayList<>();
for (String value : values1) {
valueFrom.add(Float.parseFloat(value));
}
List valueTo = new ArrayList<>();
for (String value : values2) {
valueTo.add(Float.parseFloat(value));
}
action.setAction(actionStr1);
action.setValueFrom(valueFrom);
action.setValueTo(valueTo);
}
actions.add(action);
}
}
(3)动画更新SVGAction中的数值,重新构建一个新的Path
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
float fraction = valueAnimator.getAnimatedFraction();
for (SVGAction a : actions) {
a.computeValue(fraction);
}
path.reset();
for (SVGAction a : actions) {
actionPath(a, path);
}
invalidate();
}
(4)根据当前SVGAction值构建Path对象
private void actionPath(SVGAction action, Path buildPath) {
List value = action.getValue();
switch (action.getAction().toUpperCase()) {
case SVGAction.ACTION_M:
buildPath.moveTo(value.get(0) * scale, value.get(1) * scale);
break;
case SVGAction.ACTION_Q:
buildPath.quadTo(value.get(0) * scale, value.get(1) * scale, value.get(2) * scale, value.get(3) * scale);
break;
case SVGAction.ACTION_C:
buildPath.cubicTo(value.get(0) * scale, value.get(1) * scale, value.get(2) * scale, value.get(3) * scale, value.get(4) * scale, value.get(5) * scale);
break;
case SVGAction.ACTION_L:
buildPath.lineTo(value.get(0) * scale, value.get(1) * scale);
break;
case SVGAction.ACTION_Z:
buildPath.close();
break;
}
}
(5)绘制path
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawPath(path, paint);
}
最终效果如下,基本达到我们的期望,可以通过代码动态构建矢量动画,并且可以控制动画进度。
换个path,再来一个。
trimPath
OK,Path转换已经实现了,一个难题已经搞定了,下面来想想trimPath类型动画我们怎么自己实现呢?
首先看个效果:
和上一篇文章的Demo一样,这个Demo有两个path,一个是放大镜,一个外面的圆圈(中间的点请忽略,这是另外一个问题,这里先不讲),用上一篇文章的知识实现这个效果并不难,通过改变放大镜和圆圈的截取长度就可以实现。那要用代码动态构建这个动画呢?思路并不难,我们要想办法从一个path中动态截取一段呢,问题是怎么截取呢?
答案是PathMeasure,PathMeasure是一个Path辅助类,用于辅助测量Path,PathMeasure中有一个神奇的方法:PathMeasure.getSegment(),它可以从一个path中截取出指定位置和长度的一段子path,基于这一点,我们就可以实现上面的效果。动画开始时,我们把放大镜的截取长度从1渐变到0,然后把圆圈的截取长度从0渐变到1再渐变到0,同时,截取位置从0渐变到0.25再渐变到0,每一次渐变都截取出新的一段path,然后绘制出来,最终就是这个效果。
同样这里只列举一些核心代码,全部源码请参考Github,或者自己尝试写
// 创建PathMeasure
PathMeasure mMeasure = new PathMeasure();
// 关联Path对象
mMeasure.setPath(path_search, false);
// 创建目标Path对象
Path dst = new Path();
// 屏蔽系统bug,先不解释
dst.rLineTo(0, 0);
start = mMeasure.getLength() * mAnimatorValue;
end = mMeasure.getLength();
// 获取子Path
mMeasure.getSegment(start == end ? start - 0.01f : start, end, dst, true);
// 绘制Path
canvas.drawPath(dst, mPaint);
androidsvg
说到trimPath动画,网上有一个库应用的不错,可以实现很漂亮的效果。
它可以直接读取SVG文件,使用起来比较简单,但是可控性不强,这里不做详细的解释,喜欢这个效果的可以参考demo工程的实现。这个Android文字的路径是我先用GIMP生成SVG,然后再手动修改值,弄得我欲生欲死。。。
短暂的幸福
哇,开篇提出的两个问题都解决了,先开心一会。我们已经可以自己写出一些好玩的东西了,比如:
但是!But!问题又来了,到目前为止,path路径都是我们自己手动算出来的,实际项目开发中,UED通常只会给我们两个图形,然后要在两个图形间作变换。我们怎么根据两个图片生成path呢?手动算肯定不现实,比如那个Twitter转变成爱心,如果只给我Twitter和爱心的两个图片,即便是Google大神也不可能手动把路径算出来的。那不用手动算怎么才能获得path路径呢?
最终的目标
这里先不说太多废话,我们先定一下我们期望达到的最终目标:
(1)UED任意给一个图形,我们能转换成矢量图
(2)UED任意给两个图形,我们能实现两个图形的变换
问题1
单纯地看这个问题的话,其实是比较简单的,把位图转换成矢量图,有很多工具都可以做,百度一下一大堆,比如我曾经用过GMIP,Illustrator等,我们只要把图片传进去,就可以自动生成路径。所以第一个问题就这么轻松搞定了。
示例:初始位图
转成SVG
问题2
问题1真的这么简单就解决了?NoNoNo,还远没有解决。
我们回头看一下上面的示例,图片中就是一个简单的拱形,我们即便不计算都知道它的路径大体上应该是这样的:
Mx,y Cx1,y1,x2,y2,x3,y3 Z
一个M起点,一个C贝塞尔曲线,最后一个Z闭合就可以了。
但是我们再看上面GIMP自动生成的path:
M 123.00,58.70
C 171.19,36.80 228.81,36.80 277.00,58.70
C 316.53,76.66 346.59,109.92 347.00,155.00
C 347.00,155.00 53.00,155.00 53.00,155.00
C 54.00,109.48 82.79,76.97 123.00,58.70 Z
怎么这么一大串?M起点和Z闭合没问题,但是它中间用了四个C贝塞尔曲线,它把一段曲线分成了四段曲线,这就是自动化工具的缺点,生成过程不受我们控制,我们不能保证生成的路径一定是最简洁的形式。
(PS:实际上有时候一条曲线分成7、8条曲线都是有可能的,甚至连一条直线都可能会被分成几条曲线来显示)
path生成不受控制就不受控制呗,有什么问题呢?反正只要最后显示的形状是对的就行。问题就只在于“同形Path”
前面说到过,要想做两个path的转换,就必须要求两个path是同形path,如果path的生成过程是不可控的,但是就不能保证两个图片生成的path一定是同形的,不是同形就无法做转换。
这个问题怎么解决呢?
桑心,这次真的没招了。。。
这个问题想了很久,没有什么好的解决办法,我也尝试找了很多矢量工具,没有找到可以控制Path生成过程的,没有哪个软件可以保证两个图片生成两个Path一定是同形的。
一次尴尬的尝试
既然没有现成的软件能使用,那自己开发一个软件呢?于是一次尴尬的尝试就开始了。为什么说是尴尬的尝试呢,因为最终的产品并不能完美地解决问题,实在迫不得已的时候,可以拿出来顶个用场。
PathController
基于Processing语言开发的桌面小工具,可以帮助我们生成指定锚点的Path路径。
A:在【添加模式】下点击鼠标左键添加锚点
E:在【编辑模式】下移动锚点和控点,调整曲线
L:切换显示辅助网格
V:预览最终形状
I:背景反向色,用于不同背景图的显示效果
D:删除末尾一个锚点
C:删除所有锚点
Z:闭合曲线
S:到处路径
上图我们已经调整好了所有的锚点和控点,按S键导出路径,会生成在工程根目录
{
"path": "M473.0,336.0 C295.0,323.33,196.0,263.66998,86.0,139.0 C28.669998,248.0,72.33,342.0,142.0,388.0
C113.33,393.33,84.67,385.67,59.0,368.0 C55.67,451.0,117.33,533.0,208.0,554.0 C175.67,565.67,156.33,564.33,126.0,556.0
C150.67,632.67,222.33,686.33,299.0,687.0 C199.0,758.33,141.0,774.67,23.0,766.0 C236.0,895.0,501.0,872.0,674.0,709.0
C797.33,586.0,838.67,465.0,846.0,289.0 C886.33,259.67,908.67,239.33,937.0,192.0 C901.67,212.33,870.33,220.67,834.0,223.0
C881.33,184.67,890.67,171.33,913.0,120.0 C875.33,142.0,833.67,159.0,796.0,165.0 C720.0,94.0,648.0,93.0,581.0,120.0
C493.0,156.0,452.0,262.0,473.0,333.0",
"viewPortWidth": 960,
"viewPortHeight": 960
}
最终就是我们想要的path。
限于个人水平有限,这个工具并不智能,所以也就不多作介绍了,实在迫不得已的时候,可以拿出来顶个用场。
为什么用Processing开发,主要是Processing提供了丰富的绘图api和向量运算api。简单地介绍下Procssing。Processing是一门绘图语言,一门不是给程序员用的编程语言。Processing主要应用场景是数据可视化和工程设计。
PathManager工程地址:https://github.com/mime-mob/PathController
总结
这一篇可能看起来会比较乱,简单来总结下,Android系统的矢量动画框架只能在xml中写死,并且不能控制动画过程和进度,于是我们想自己用代码模拟系统的矢量动画。我们分别实现了pathMorphing和trimPath类型动画。接下来,为了解决path生成的问题,我找了很多矢量软件都不理想,于是自己尝试开发了一个桌面工具,但是限于水平有限,工具并不太智能。
下一篇
下一篇会是本系列终结篇,简单讲一下通用动画库。整个系列所有的demo都放在了一个工程中。
Github工程地址:https://github.com/mime-mob/AndroidAdvanceAnimation