Android动画之VectorDrawable矢量图实战

效果图

1. 矢量图SVG简介

Android 5.0系统中引入了 VectorDrawable 来支持矢量图(SVG),同时还引入了 AnimatedVectorDrawable 来支持矢量图动画。

所谓SVG(Scalable Vector Graphics),直译为可伸缩矢量图,具体内容可以参考矢量图百科。和一般的栅格图(比如PNG)相比,虽然其绘制速度较慢,却有以下的优点:

  • 保存最少的信息,文件大小比位图要小,并且文件大小与物体的大小无关;
  • 任意放大矢量图形,不会丢失细节或影响清晰度,因为矢量图形是与分辨率无关的。

从以上两个优点来看,在项目中使用矢量图至少可以缩小我们apk包的尺寸,而且可以在屏幕适配时提供很大的方便,因为矢量图是分辨率无关的。

2. Android中的Vector

SVG是一套标准,VectorDrawable是Android中的实现,但是VectorDrawable 并没有支持所有的 SVG 规范,目前只支持 PathData 和有限的 Group 功能。 所以对于使用 VectorDrawable 而言,我们只需要了解 SVG 的 PathData 规范即可。通过查看 PathData 文档,可以看到 path 数据包含了一些绘图命令,比如 :

  • M: move to 移动绘制点;
  • L:line to 直线;
  • Z:close 闭合;
  • C:cubic bezier 三次贝塞尔曲线;
  • Q:quatratic bezier 二次贝塞尔曲线;
  • A:ellipse 圆弧;

每个命令都有大小写形式,大写代表后面的参数是绝对坐标,小写表示相对坐标。参数之间用空格或逗号隔开
命令详解:

  • M (x y) 移动到x,y;
  • L (x y) 直线连到x,y,还有简化命令H(x) 水平连接、V(y)垂直连接;
  • Z,没有参数,连接起点和终点;
  • C(x1 y1 x2 y2 x y),控制点x1,y1 x2,y2,终点x,y;
  • Q(x1 y1 x y),控制点x1,y1,终点x,y;
  • A(rx ry x-axis-rotation large-arc-flag sweep-flag x y) :
    • rx ry 椭圆半径
    • x-axis-rotation x轴旋转角度
    • large-arc-flag 为0时表示取小弧度,1时取大弧度
    • sweep-flag 0取逆时针方向,1取顺时针方向

以下是绘制三角形的VectorDrawable实例:

<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" 
    android:height="64dp"
    android:width="64dp"
    android:viewportHeight="100"
    android:viewportWidth="100">
    
    <path
         android:fillColor="#000000"
         android:pathData="M25,0 l 50,50 -50,50Z"/>

</vector>

首先vector 标签是一个drawable对象,所以是放在res/drawable目录的。

vector 标签下有android:widthandroid:height属性,这两个属性是必填的,定义矢量图形的绝对大小,虽然说是矢量图形随意缩放,但是不能说这里不定义宽高直接到要设置到的目标控件上定义控件的宽高,这样是不允许的,一定要设置这个绝对宽高,要不然会报错。

然后还有个android:viewportHeightandroid:viewportWidth属性,这个是画布宽高,也是必填的,定义Path路径的时候就必须在这个画布大小里去绘制,超出画布就显示不出来了。

path标签android:fillColor属性定义绘制颜色,android:pathData定义绘制路径。

M25,0 l 50,50 -50,50Z这个路径表示:

  1. 在100*100的画布内,先把绘制点移动到绝对坐标(25,0)这个点,然后画直线到(50,50)这个点,l指令是相对坐标,大写的L表示绝对坐标,那么l 50,50就是在原点(25,0)的x轴往前移50,往下移50,绝对坐标就是(75,50),也就是三角形的右边那个点;
  2. 然后从(50,50)这个点绘制到三角形最下面那个点(-50,50),这也是相对右边那个点相对坐标,也就是把(75,50)这个绝对坐标当作是原点(0,0),参作这个原点往后移动50再往下移动50,在整个画布中的绝对坐标就是(25,100)。

效果如下:


绘制出的三角形

如果要绘制标准的SVG,可以是使用在线SVG Editor
另外,如果需要将SVG转化为VectorDrawable,还可以是使用在线工具Android SVG to VectorDrawable

更为重要的是,Android Studio自带Vector Assert工具,可以帮助我们生成矢量图;

  • Material icon:使用官方自带的各种矢量图,内容很丰富;
  • Local SVG file:使用第三方的SVG文件,比如从第三方图标网站:iconfont.cn中下载的图片都支持SVG;

3. Vector的配置和兼容性

正如上文所说,Android L开始提供了新的API VectorDrawable
可以使用SVG类型的资源,最初只能在21以上版本中使用。

为了让低版本也可以支持矢量图,Gradle Plugin 1.5加入了如下功能:

  • buildVersion>=21时,Vector矢量图功能不变;
  • buildVersion<21时,编译时,自动转换把Vector矢量图转化为PNG;

再后来Google升级了support library,官方向后兼容了矢量图的使用。矢量图兼容到API7,矢量图动画兼容到API11。

3.1 appcompat-v:23.2.0

因此要在低版本上兼容矢量图,就需要在项目中引入新的兼容库support-vector-drawable,并且appcompat-v7库的版本要在23.2.0+

compile 'com.android.support:appcompat-v7:23.2.0'

3.2 gradle

而且你还要修改下gradle的相关配置,不要让gradle在构建的时候为你在低版本(API21以下)的情况下生成针对于不同密度的png文件,因为android studio1.4的时候支持了矢量图。

如果你的gradle插件的版本为2.0以下,你应该这么修改:

android {
  defaultConfig {
    // 不让gradle自动生成不同屏幕分辨率的png图
    generatedDensities = []
  }
  aaptOptions {
    additionalParameters "--no-version-vectors"
  }
}

现在大部分人的gradle插件版本是2.0+,只要这样修改就好:

android {
  defaultConfig {
    vectorDrawables.useSupportLibrary = true
  }
}

经过上面这几步的修改,就可以在项目中使用矢量图了。下面我们就正式来说说怎么使用。

4. Vector和属性动画结合使用

先看下效果图,这是一个代表文件下载的图片,是通过矢量图绘制的。我们通过将其和属性动画结合,实现了下载过程中的动画效果:箭头跳跃+横线反复出现


FileDownloading.gif

4.1 Vector矢量图

首先我们看一下图标的矢量图:

<vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:width="24dp"
        android:height="24dp"
        android:viewportWidth="24.0"
        android:viewportHeight="24.0">
    <group android:name="arrow_location">
        <path
            android:name="arrow_pic"
            android:fillColor="#FF000000"
            android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7z"/>
        </group>

    <path
        android:name="bar"
        android:fillColor="#FF000000"
        android:pathData="M5,18v2h14v-2H5z"/>

</vector>

矢量图中有两个Path标签,第一个来绘制箭头,第二个来绘制底下的横线。细心的读者已经发现,第一个Path标签外被Group标签包裹,这是因为path标签中没有坐标变化的属性,这些属性在group标签中,要使用这些标签就必须在group中绘制。

4.2 ObejctAnimator

Android属性动画Animator实现卫星Button中已经介绍了关于如何动态使用属性动画。本文的属性动画将以静态XML文件的出现,但是其内容是一样的。

如有要实现箭头跳跃的动画,就要让箭头的"translateY"属性向上移动,然后反复执行。为了让移动更有跳跃的感觉,interpolator被设为overshoot
“arrow_jump.xml”文件如下:

<set xmlns:androd="http://schemas.android.com/apk/res/android">
    <objectAnimator
        androd:duration="500"
        androd:interpolator="@android:interpolator/overshoot"
        androd:propertyName="translateY"
        androd:repeatCount="infinite"
        androd:repeatMode="reverse"
        androd:valueFrom="0"
        androd:valueTo="-3"
        androd:valueType="floatType"
        >
    </objectAnimator>
</set>

"bar_drawing.xml"实现动画下方横线反复出现的过程。

<set xmlns:androd="http://schemas.android.com/apk/res/android">
    <objectAnimator
        androd:duration="500"
        androd:propertyName="trimPathEnd"
        androd:repeatCount="infinite"
        androd:repeatMode="reverse"
        androd:valueFrom="0"
        androd:valueTo="1"
        androd:valueType="floatType">
    </objectAnimator>
</set>

这里要特别说明下属性* trimPathEnd,另外还有个属性 trimStartEnd*,其中trim是截取的意思,PathStart代表开始时的图像,PathEnd代表结束时的图像,变化的初始值和终值代表绘制的比率,从0变化到1,也就代表的是从完全没有到完全绘制出来,即从无到有。

持外两个属性属性动画最外层都是set标签,用户可以根据自己设计加入更多的属性动画标签ObjectAnimator

4.3 animated-vector 粘合剂

如何将矢量图和属性动画结合起来呢,这就需要们的粘合剂*animated-vector *文件,在其中建立矢量图和动画的对应关系,如下:
“file_download.xml"

<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/ic_file_download_black_24dp">
    <target
        android:animation="@animator/arrow_jump"
        android:name="arrow_location"/>

    <target
        android:animation="@animator/bar_drawing"
        android:name="bar"/>
</animated-vector>

其中animated-vectorandroid:drawable属性代表要控制的对象是哪一张矢量图。targe标签代表的动画和对象的一个对应关系,animation代表要加载的动画文件,name代表矢量图文件中的哪一个部分。这里两个target分别给。

更需要注意的是,位置移动所操控的属性并不是矢量图中path标签的,而是group标签的内容,所以箭头的name标准建立在group之中,arrow_jump动画绑定的对象也是group。同时,* trimPathEnd属性是Path的属性,所以动画绑定对象应该是Path*。
4.4 布局和模拟
Activity中的布局如下:

<ScrollView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <LinearLayout
        android:orientation="vertical"
        android:gravity="center"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <ImageView
            android:id="@+id/image"
            android:layout_marginTop="5dp"
            android:onClick="anim"
            android:layout_width="100dp"
            android:layout_height="100dp"
            app:srcCompat="@drawable/file_download"/>
    </LinearLayout>
</ScrollView>

需要注意的是,ImageViewapp:srcCompat被设为粘合剂文件” file_download“。另外因为使用了app:srcCompat这个自定义的属性,需要加入命名空间xmlns:app="http://schemas.android.com/apk/res-auto"

Activity中对应的源码也很简单,点击图片模拟下载过程并开始动画,5秒之后下载结束,停止动画。

import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageView;
import android.widget.Toast;

import static java.lang.Thread.sleep;

public class MainActivity extends AppCompatActivity {
    ImageView mImageView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mImageView = (ImageView) findViewById(R.id.image);
    }

        
    public void anim(View view) {//图片点击事件
        ImageView imageView = (ImageView) view;
        Drawable drawable = imageView.getDrawable();//获得图片的Drawable属性
        if(drawable instanceof Animatable) {//如果是属性动画
            ((Animatable) drawable).start(); //开始动画
            Toast.makeText(view.getContext(), "下载开始!",Toast.LENGTH_SHORT).show();
        }
        new Thread(new Runnable() {//开启计时线程
            @Override
            public void run() {
                try {
                    sleep(5000); //睡眠5s,代表下载过程
                    runOnUiThread(new Runnable() {//在UI线程中停止动画
                        @Override
                        public void run() {
                            ((Animatable) mImageView.getDrawable()).stop();
                            Toast.makeText(getApplicationContext(),"下载完毕",Toast.LENGTH_SHORT).show();
                        }
                    });
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

5. VectorDrawable的一些兼容问题:

如果使用appcompat-v:23.2.0+,兼容的API版本有比Android 5小的情况,还要注意有一些兼容新的问题:

  1. 在5.0以下的版本中,还无法使用Path Morphing(路径变化)属性;
  2. 在5.0以下的版本中,只能使用系统插值器;
  3. 在5.0以上版本中,需要把Drawable对象转化成AnimatedVectorDrawable之后才能使用
    public void animL(View view) {
        ImageView imageView = (ImageView) view;
        AnimatedVectorDrawable drawable = (AnimatedVectorDrawable) getDrawable(R.drawable.fivestar_anim);
        imageView.setImageDrawable(drawable);
        if (drawable != null) {
            drawable.start();
        }
    }

当然,如果最小版本都是5以上的,就没有这些问题了。

6. VectorDrawable的使用场景

最后借用前辈"徐医生"总结的VectorDrawable和Bitmap对比,为介大家绍适合VectorDrawable使用的场景:


VectorDrawable和Bitmap对比

7. 参考文献与阅读扩展

  1. Android VectorDrawable与SVG
  2. VectorDrawable怎么玩
  3. Androidの矢量图形之VectorDrawable研究
  4. Android 5.0学习之AnimatedVectorDrawable
  5. VectorDrawable详解
  6. Android属性动画Animator实现卫星Button

推荐阅读更多精彩内容