首页 > 代码库 > Android开发笔记(一百三十二)矢量图形与矢量动画

Android开发笔记(一百三十二)矢量图形与矢量动画

矢量图形VectorDrawable

与水波图形RippleDrawable一样,矢量图形VectorDrawable也是Android5.0之后新增的图形类。矢量图不同于一般的图形,它是由一系列几何曲线构成的图像,这些曲线以数学上定义的坐标点连接而成。具体到实现上,则需开发者提供一个xml格式的矢量图形定义,然后系统根据矢量定义自动计算该图形的绘制区域。因为绘图结果是动态计算得到,所以不管缩放到多少比例,矢量图形都会一样的清晰,不像位图那样拉大后会变模糊。

矢量图形的xml定义有点复杂,其结构可分为三个层次:根标签、组标签、路径标签。


根标签vector

首先是vector标签,它表示当前定义的是一个完整的矢量图形。该标签支持的主要属性说明如下:
android:name:指定矢量图形的名称。
android:width:指定矢量图形的默认宽度,一般使用dp数值。如果在layout布局文件中将ImageView的layout_width设置为wrap_content,同时src设置为该矢量图形,则ImageView控件的宽度就是此处的android:width。
android:height:指定矢量图形的默认高度,一般使用dp数值。
android:viewportWidth:指定视图空间的宽度,即虚拟坐标系的宽度,后续路径的坐标信息都位于该视图空间之内。
android:viewportHeight:指定视图空间的高度,即虚拟坐标系的高度。
android:alpha:指定矢量图形的的透明度,取值为0.0到1.0。

这里要注意width/height与viewportWidth/viewportHeight两组宽高的区别,前者指的是矢量图形被外部世界观察到的尺寸大小,故而采用了带dp单位的绝对数值;而后者指的是矢量图形为内部几何路径所参照的空间范围,故而采用了不带单位的相对数值,正因为矢量图形中的几何路径以相对坐标来标记,所以不管矢量图形缩放到多少比例,其内部的几何形状也会按同样比例缩放。


组标签group

然后是group标签,它定义了一组路径的共同行为(如一起旋转、一起缩放、一起平移等等)。该标签支持的主要属性说明如下:
android:name:指定分组对象的名称。
android:pivotX:指定旋转中心点的横轴坐标。
android:pivotY:指定旋转中心点的纵轴坐标。
android:rotation:指定分组对象的旋转角度。
android:scaleX:指定分组对象在横轴上的缩放比例。取值0.5表示缩小一半,取值2.0表示放大一倍。
android:scaleY:指定分组对象在纵轴上的缩放比例。
android:translateX:指定分组对象在横轴上的平移距离。
android:translateY:指定分组对象在纵轴上的平移距离。


路径标签path

最后是path标签,它定义了一个路径的几何描述,既可以表示一根曲线,也可以表示一块平面区域。该标签支持的主要属性说明如下:
android:name:指定几何路径的名称。
android:pathData:指定几何路径的数据定义。数据格式需符合SVG标准。
android:fillColor:指定平面区域的颜色。若不指定,则不绘制平面区域。
android:fillAlpha:指定平面区域的透明度。
android:strokeColor:指定曲线的颜色。若不指定,则不绘制曲线颜色。
android:strokeWidth:指定曲线的宽度。
android:strokeAlpha:指定曲线的透明度。
android:strokeLineCap:指定曲线的首尾外观。取值说明有三个:butt(默认值,直线边缘)、round(圆形边缘)、square(方形边缘)。
android:strokeLineJoin:指定两条曲线相交的边角外观。取值说明有三个:miter(默认值,锐角)、round(圆角)、bevel(钝角)。
android:trimPathStart:指定几何路径从哪里开始绘制。取值为0.0到1.0,比如取值0.4表示只绘制后面十分之六的内容,前面十分之四不予绘制。
android:trimPathEnd:指定几何路径到哪里结束绘制。取值为0.0到1.0,比如取值0.4表示只绘制前面十分之四的内容,后面十分之六不予绘制。
android:trimPathOffset:指定几何路径的绘制偏移。取值为0.0到1.0,表示线条从trimPathOffset+trimPathStart处一直绘制到trimPathOffset+trimPathEnd处。

路径信息有几个地方容易混淆,下面把相关细节详细说明一下:
1、关于butt和square的区别,乍看起来直线边缘与方形边缘没什么差别,但矢量图形的方形边缘其实是套上一个方形的帽子,既然是套上去,就会比没戴帽子的时候高一点,所以使用square的线条会比使用butt的线条要长一点。
2、关于butt和square的区别,miter保留了原样的尖角,而bevel会把尖角部分切掉一小块,看起来就变钝了。
3、trimPathOffset+trimPathEnd的和如果超过1,也会画出来。只是没有全部画出来,而是绘制从起点到trimPathOffset+trimPathEnd-1所处的位置。


可缩放矢量图形SVG标记

前面说到,path标签的android:pathData属性,取值需符合SVG标准。SVG全称为“Scalable Vector Graphics”,意即可缩放的矢量图形,它是一种图形格式,专门用于描述矢量图形的定义。

SVG标记比较抽象,下面先举个简单的例子,有了直观的概念更方便理解,如下所示:
        android:pathData=http://www.mamicode.com/"
            M 30,50
            L 75 35"
这个标记定义不难,首先“M 30,50”指的是把画笔移动到坐标点(30,50)的位置,后面的“L 75 35”指的是从当前位置画一根线段到坐标点(75,35)。说白了,就是在(30,50)和(75,35)两点之间画一根线段。


好了,每行定义一个动作,每行的第一个字符表示动作的类型,后面的数字表示动作经过的坐标点。这便是SVG标记的大概格式,万变不离其宗,掌握了规律学得更好更快。详细的SVG标记定义说明如下:
移动画笔 “M x0,y0”把画笔移动到坐标点(x0,y0)。
画线段 “L x1 y1” 从当前位置(x0,y0)画一根线段到坐标点(x1,y1)。
画水平线段 “H x1” 从当前位置(x0,y0)画一根水平线到坐标点(x1,y0)。
画垂直线段 “V y1” 从当前位置(x0,y0)画一根垂直线到坐标点(x0,y1)。
画二次贝塞尔曲线 “Q xa ya x1 y1”二次贝塞尔曲线的起点是当前位置,终点是(x1,y1),曲线中部向控制点(xa,ya)凸出。
画三次贝塞尔曲线 “C xa ya xb yb x1 y1”三次贝塞尔曲线的起点是当前位置,终点是(x1,y1),曲线中部有两个控制点,分别向(xa,ya)和(xb,yb)两方向凸出。
画椭圆的圆弧 “A radius-x radius-y x-axis-rotation large-arc-flag sweep-flag x1 y1”从当前位置拉出一段圆弧,圆弧的参数比较多,分别说明如下:
-- radius-x表示椭圆的横轴半径。
-- radius-y表示椭圆的纵轴半径。横轴半径等于纵轴半径时,表示这是个圆圈的圆弧。
-- x-axis-rotation表示圆弧的旋转角度。
-- large-arc-flag表示大弧标志,为0时表示取小弧度,1时取大弧度。
-- sweep-flag表示轨迹标志,为0表示逆时针方向,为1表示顺时针方向。
-- 圆弧经过某点,该点的横坐标为x1
-- 圆弧经过某点,该点的纵坐标为y1
闭合路径 “Z” 连接起点跟终点,即在起点(x0,y0)与终点之间画一根线段。

再来补充一下SVG标记的若干说明,如下所示:
1、每个命令都有大小写形式,大写代表后面的参数是绝对坐标,小写表示相对坐标。
2、参数之间用空格或逗号隔开,两种分隔符的效果是一样的。
3、关于圆弧的large-arc-flag和sweep-flag两个标志,光看文字说明其实不易理解,还是上个图观察观察:
技术分享

下面使用SVG标记定义一个心形,先上个心形的效果图:
技术分享

心形对应的矢量图形定义示例如下:
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="256dp"
    android:height="256dp"
    android:viewportHeight="32"
    android:viewportWidth="32">

    <path
        android:fillColor= "#ffaaaa"
        android:pathData= http://www.mamicode.com/"M20.5,9.5>

矢量动画AnimatedVectorDrawable

费了老大的劲搞清楚SVG标记,如果仅仅画个静态的矢量图形,未免大材小用了。其实矢量图形真正的意义在于矢量动画,通过动态计算几何路径的坐标,从而实现局部或整体的动画效果,这才是矢量图形的杀手锏呀。


Android提供了AnimatedVectorDrawable这么一个矢量动画类,但开发者还得通过属性动画及其xml标签方可实现动画定义。先看看AnimatedVectorDrawable的几个常用方法:
registerAnimationCallback : 注册动画监听器,需实现Animatable2.AnimationCallback接口的两个方法:onAnimationStart和onAnimationEnd。
start : 开始播放动画。
stop : 停止播放。
reverse : 倒过来播放。
再看看如何通过属性动画实现矢量动画效果。理论上,矢量图形的三个标签(vector、group、path)都有可以用来播放动画的属性;不过实际开发的时候,常用的只有三类属性可用作动画,说明如下:

变换类属性

这类属性包括vector标签的android:alpha,以及group标签的android:rotation、android:scaleX、android:scaleY、android:translateX、android:translateY等等,这几个属性分别对应于补间动画的灰度动画、旋转动画、缩放动画、平移动画。
因为该类属性实现的是大家熟悉的补间动画效果,所以这里就不再做演示了。


路径类属性

这类属性主要指path标签的android:pathData,通过设置几何路径的起始状态与终止状态,可实现两个几何形状之间的渐变效果,如一个圆圈从小变大,又如一条曲线变成直线等等。
下面是个从哭丧脸变为笑脸的动画截图:
技术分享

下面是人脸的矢量图形定义文件vector_face_eye.xml:
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:height="200dp"
    android:width="200dp"
    android:viewportHeight="100"
    android:viewportWidth="100" >
  <path
      android:fillColor="@color/yellow"
      android:pathData=http://www.mamicode.com/"@string/path_circle"/>>

接着是脸部三处器官变化的属性动画定义文件。
下面是左眼的属性动画定义文件anim_smile_eye_left.xml:
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
  android:duration="3000"
  android:propertyName="pathData"
  android:valueFrom="@string/path_eye_left_sad"
  android:valueTo="@string/path_eye_left_happy"
  android:valueType="pathType"
  android:interpolator="@android:anim/accelerate_interpolator"/>
下面是右眼的属性动画定义文件anim_smile_eye_right.xml:
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
  android:duration="3000"
  android:propertyName="pathData"
  android:valueFrom="@string/path_eye_right_sad"
  android:valueTo="@string/path_eye_right_happy"
  android:valueType="pathType"
  android:interpolator="@android:anim/accelerate_interpolator"/>
下面是嘴巴的属性动画定义文件anim_smile_mouth.xml:
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
  android:duration="3000"
  android:propertyName="pathData"
  android:valueFrom="@string/path_face_mouth_sad"
  android:valueTo="@string/path_face_mouth_happy"
  android:valueType="pathType"
  android:interpolator="@android:anim/accelerate_interpolator"/>


最后是笑脸的矢量动画定义例子animated_vector_smile_eye.xml:
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/vector_face_eye" >

    <target
        android:name="mouth"
        android:animation="@anim/anim_smile_mouth" />

    <target
        android:name="eye_left"
        android:animation="@anim/anim_smile_eye_left" />
    
    <target
        android:name="eye_right"
        android:animation="@anim/anim_smile_eye_right" />
    
</animated-vector>


不要忘了在代码中进行矢量动画的播放操作:
	private void startVectorSmile() {
		iv_vector_smile.setImageResource(R.drawable.animated_vector_smile_eye);
		Drawable drawable = iv_vector_smile.getDrawable();
		if (drawable instanceof AnimatedVectorDrawable) {
			((AnimatedVectorDrawable) drawable).start();
		}
	}


修剪类属性

这类属性包括path标签的android:trimPathStart和android:trimPathEnd,可实现矢量图形逐步展开或者逐步消失的动画效果。
下面是个支付宝支付成功的动画截图:
技术分享

支付成功动画包含两个形状,首先在外面画个圆圈,然后在圆圈里面画个打勾符号。因为圆圈和打勾并不相连,如果按照一般的处理,就会一边画圆圈一边画打勾,这不是我们所希望的画完圆圈再画打勾的效果。所以要想让圆圈动画和打勾动画按顺序播放,得分别定义圆圈的矢量图形和打勾的矢量图形,然后等圆圈动画播放完毕,再开始播放打勾动画。


下面是圆圈的矢量图形定义文件vector_pay_circle.xml:
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:height="100dp"
    android:viewportHeight="100"
    android:viewportWidth="100"
    android:width="100dp" >

    <path
        android:name="circle"
        android:pathData=http://www.mamicode.com/">下面是打勾的矢量图形(含圆圈图形)定义文件vector_pay_success.xml:
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:height="100dp"
    android:viewportHeight="100"
    android:viewportWidth="100"
    android:width="100dp" >

    <path
        android:name="circle"
        android:pathData=http://www.mamicode.com/">

接着是支付成功的属性动画的xml定义文件anim_pay.xml:
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="1000"
    android:interpolator="@android:interpolator/linear"
    android:propertyName="trimPathEnd"
    android:valueFrom="0"
    android:valueTo="1"
    android:valueType="floatType" />


最后是矢量动画的定义文件,下面这个用来播放圆圈动画:
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/vector_pay_circle">

    <target
        android:name="circle"
        android:animation="@anim/anim_pay" />

</animated-vector>
下面这个用来播放圆圈动画后继的打勾动画:
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/vector_pay_success">

    <target
        android:name="hook"
        android:animation="@anim/anim_pay" />

</animated-vector>


圆圈动画播放完毕,接着播放打勾动画,这要在代码中控制,具体的是调用AnimatedVectorDrawable对象的registerAnimationCallback方法,一旦监听到原动画播放结束,然后开始播放新动画。


点击下载本文用到的矢量图形与矢量动画的工程代码


点此查看Android开发笔记的完整目录

Android开发笔记(一百三十二)矢量图形与矢量动画