干货 |Android高级动画(3)

2,866 阅读13分钟
原文链接: mp.weixin.qq.com

作者简介

本文作者:大公爵

地址:http://www.jianshu.com/p/d6cc8d218900

一波未平

上一篇文章我们讲了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如下:

<?xml version="1.0" encoding="UTF-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:width="150dp"
        android:height="150dp"
        android:viewportHeight="800"
        android:viewportWidth="800" >

        <path
            android:name="path1"
            android:fillColor="#2458ff"
            android:pathData="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"/>
</vector>
<?xml version="1.0" encoding="UTF-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:width="150dp"
        android:height="150dp"
        android:viewportHeight="800"
        android:viewportWidth="800" >

        <path
            android:name="path1"
            android:fillColor="#2458ff"
            android:pathData="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"/>
</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<Float> valueFrom;    
   private List<Float> 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<Float> valueFrom = new ArrayList<>();            
       for (String value : values1) {            valueFrom.add(Float.parseFloat(value));        }        List<Float> 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<Float> 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);
// 绘制Pathcanvas.dra
wPath(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

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
              "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">

<svg xmlns="http://www.w3.org/2000/svg"
     width="5.55556in" height="2.77778in"
     viewBox="0 0 400 200">
  <path id="选区"
        fill="none" stroke="black" stroke-width="1"
        d="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" />
</svg>

问题2

问题1真的这么简单就解决了?NoNoNo,还远没有解决。

我们回头看一下上面的示例,图片中就是一个简单的拱形,我们即便不计算都知道它的路径大体上应该是这样的:

Mx,y Cx1,y1,x2,y2,x3,y3 Z

一个M起点,一个C贝塞尔曲线,最后一个Z闭合就可以了。但是我们再看上面GIMP自动生成的path:

M 123.00,58.70C 171.19,36.80 228.81,36.80 277.00,58.70C 316.53,76.66 346.59,109.92 347.00,155.00C 347.00,155.00 53.00,155.00 53.00,155.00C 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