Android高级动画(3)

目录

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例子太复杂了,我们换一个稍微简单的例子。

pathMorphing

这是一个简单的两个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);
}

最终效果如下,基本达到我们的期望,可以通过代码动态构建矢量动画,并且可以控制动画进度。

pathMorphing

换个path,再来一个。

pathMorphing

trimPath

OK,Path转换已经实现了,一个难题已经搞定了,下面来想想trimPath类型动画我们怎么自己实现呢?

首先看个效果:

search

和上一篇文章的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动画,网上有一个库应用的不错,可以实现很漂亮的效果。

androidsvg

它可以直接读取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.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路径。

PathController

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,504评论 4 365
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,898评论 1 300
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,218评论 0 248
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,322评论 0 214
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,693评论 3 290
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,812评论 1 223
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,010评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,747评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,476评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,700评论 2 251
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,190评论 1 262
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,541评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,206评论 3 240
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,129评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,903评论 0 199
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,894评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,748评论 2 274

推荐阅读更多精彩内容

  • 目录 Android高级动画(1)http://www.jianshu.com/p/48554844a2dbAnd...
    大公爵阅读 13,050评论 23 95
  • 永远也想不到,我会变成这样,在谎言的世界里遨游。 学会刚好可能我们每一个人都做不到为什么
    学会刚好阅读 188评论 0 0
  • 今生有幸认识你 相逢的那一刻,你便成为我的知己,爱人,朋友…… 以后的日子里,只要有你,我便不再孤独,不再痛苦,无...
    乔玉儿阅读 211评论 1 9