Android高级动画(3)

一波未平

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏逍遥剑客的游戏开发

Introduction to RenderMonkey

28910
来自专栏牛客网

新鲜出炉的百度-春招-前端面经

1. 自我介绍 2. 项目经验,我说项目名说错了,然后又说了一遍,面试官不知道什么心态的鬼魅一下 3. Html5不同于以前版本的特点 4. Html5新增ap...

38211
来自专栏数据小魔方

ggplot2多维分面多图层对应规则

今天只给大家讲一个知识点,是属于ggplot2高阶用法中的分面与多图层关系如何对应,这个用法之前困扰我很久,也是最近帮朋友做东西才发现这个漏洞,于是感觉分享给大...

3384
来自专栏数据小魔方

Excel单元格内容合并的技巧!!!

今天给大家分享单元格内容合并的技巧! ▽ 之前推送过一篇单元格数据分裂的技巧,很多同学都私信我说很实用,并且希望以后能够多写一些这种可以瞬间提升工作效率的小技巧...

3087
来自专栏hotqin888的专栏

microstation vba起步——建立实体

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/hotqin888/article/det...

1293
来自专栏偏前端工程师的驿站

动手写个数字输入框1:input[type=number]的遗憾

前言  最近在用Polymer封装纯数字的输入框,开发过程中发现不少坑,也有很多值得研究的地方。本系列打算分4篇来叙述这段可歌可泣的踩坑经历: 《动手写个数字输...

2785
来自专栏hightopo

原 荐 快速开发 HTML5 WebGL 的

9103
来自专栏性能与架构

将 Redis 作为图数据库

1. 简介 Redis 在 4.0 中正式支持了Module模块系统,使其可以进行丰富的扩展 图数据库的应用越来越广泛,RedisGraph 就是一个 Redi...

4616
来自专栏练小习的专栏

可视化格式模型基础应用实例

今天在群里看到一个需求,先放demo <!DOCTYPE HTML> <html> <head> <meta charset="UTF-8"> ...

20210
来自专栏小灰灰

zxing二维码生成服务之深度定制

二维码生成服务之深度定制 之前写了一篇二维码服务定制的博文,现在则在之前的基础上,再进一步,花样的实现深度定制的需求,我们的目标是二维码上的一切都是可以由用户...

5286

扫码关注云+社区

领取腾讯云代金券