Android矢量图(一)--VectorDrawable基础
背景
维基百科中的定义:
可缩放向量图形(Scalable Vector Graphics,SVG)是一种基于可扩展标记语言(XML),用于描述二维向量图形的图形格式。SVG由W3C制定,是一个开放标准。
1,SVG何以可以任意缩放而不会失真,drawable-(m|h|xh|xxh|xxxh)dpi和mipmap-(m|h|xh|xxh|xxxh)dpi这俩货就可以省省了;2,SVG文件一般都比较小,省去很去资源达到apk缩包的目的;3,SVG占用内存非常小,性能高。但是SVG明显的缺点是没有位图表达的色彩丰富。
Android API 21(5.0)引入了一个Drawable的子类VectorDrawable目的就是用来渲染矢量图,AnimatedVectorDrawable用来播放矢量动画。之前老的小于21的API设备可以分别使用VectorDrawableCompat和AnimatedVectorDrawableCompat这两个兼容包来同样达到渲染矢量图的目的。本文只讨论矢量图,不讨论矢量动画。
准备
使用矢量图要根据minSdkVersion
来分3中不同的情况:
-
minSdkVersion>=21
:用xml文件或者代码定义VectorDrawable,和普通的Drawable用法一样,不再需要额外任何东西;如何编写矢量图,下文有介绍; -
minSdkVersion<21
:如果想要渲染矢量图的话必须在app模块的build.gralde文件里添加一行代码:
defaultConfig {
vectorDrawables.useSupportLibrary = true
}
-
minSdkVersion<21以及更多
:上面的第二种情况是使用兼容包,但是兼容包仅支持AppCompatImageView和AppCompatImageButton及其子类矢量图,而且矢量图的引用必须放在app:srcCompat属性中才会被识别并生效,代码必须这样写才行:
<android.support.v7.widget.AppCompatImageView
app:layout_constraintBottom_toBottomOf="parent"
android:layout_width="100dp"
android:layout_height="100dp"
app:srcCompat="@drawable/ic_oval"/>
ic_oval.xml是我们使用xml编写的矢量图,如果想要TextView的drawableTop或者其他额外方式使用矢量图渲染,那么必须在Activity中加入代码:
static {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
}
同时这个Activity必须继承AppCompatActivity这个compat兼容包属性才会生效。
minSdkVersion<21
情况下在非app:srcCompat属性的地方使用矢量图时,需要将矢量图用drawable容器(如StateListDrawable, InsetDrawable, LayerDrawable, LevelListDrawable, 和RotateDrawable)包裹起来使用。否则会在低版本的情况下报错org.xmlpull.v1.XmlPullParserException: Binary XML file line #0: invalid drawable tag vector
。minSdkVersion>=21则没有任何限制。
矢量图使用
准备工作做好之后,我们就需要自己动手编辑矢量图了。VectorDrawable类在xml中对应的是标签是vector。我目前所知道的是只有xml文件才能决定矢量图的样子(也就是编辑pathData、fillColor等属性),貌似无法使用代码来决定矢量图的绘制逻辑,而只能使用代码加载编辑好的xml文件,这个xml文件有两种方法来创建:
- 右击drawable-->Drawable resource file-->设置root element为vector,这样的矢量图绘制逻辑完全掌握在开发者手里;
- 右击drawable-->Vector Asset,选择SVG或者PSD文件直接生成根标签为vector的xml文件,可以百度或者Google怎样把png转换成SVG。
写了这么多字,一直在瞎扯淡而没谈重点,下面我们看下根标签为vector的xml文件的真面目,代码:
图1
上图中标签vector使用了四个属性:android:width="24dp"
、android:height="24dp"
、android:viewportHeight="300.0"
、android:viewportWidth="300.0"
。
- width和height:当使用这个矢量图的View的宽高是wrap_content 的时候这两个属性才生效;
- viewportWidth和viewportHeight:决定画布的宽高,是定义的一个虚拟空间,方便编辑pathData属性,如果pathData中的点超出了这个虚拟空间,超出的部分将不会展现给用户;虚拟空间的原点仍然还是在左上角(R点就是原点)。
path标签是vector标签的子标签,它使用了以下属性:
-
android:name
:类似View的id属性,方便path被引用,如上图的edge是虚拟空间四个边界的path,oval是一个椭圆的path; -
android:fillColor
:填充path的颜色,如果没有定义则不填充path -
android:strokeColor
:path边框颜色,如果没有定义则不显示边框 -
android:strokeWidth
:path边框的粗细尺寸 -
android:pathData
:path指令,决定path的移动和绘制逻辑,这个是最主要的属性,下面详细讨论。
更多path属性请参考链接。
pathData的指令和Path类的API方法基本差不多,比如M指令对应moveTo方法,m指令对应rMoveTo方法,下面是一些基本的指令:
- Mx,y:移动到点(x,y)
- Lx,y:直线连到点x,y,简化命令H(x)水平连接和V(y)垂直连接;
- Qx1,y1 x2,y2:二阶贝塞尔曲线,控制点(x1,y1),终点x2,y2;
- Cx1,y1 x2,y2 x3,y3:三阶贝塞尔曲线,控制点(x1,y1)( x2,y2),终点x3,y3;
- Tx y:平滑的二阶贝塞尔曲线,参数只有一个点(x,y),这个点是结束点,控制点是前一个二阶贝塞尔曲线的控制点相对于前一个贝塞尔曲线的结束点的镜像点。
- Sx2,y2 x,y:平滑的三阶贝塞尔曲线,参数为(x2,y2 x,y) ,x2,y2 为第二个控制点,x,y为绘制终点,那么第一个控制点则是前一个三阶曲线的第二个控制点相对于前一个三阶曲线终点的镜像点。
- Arx,ry x-axis-rotation large-arc-flag,sweep-flag x,y:ellipse arc圆弧曲线
- z:close闭合
......
- z:close闭合
每个指令都有大小写形式,大写表示后面的参数是绝对坐标,小写表示相对于上一个点的相对坐标位置,参数可以用逗号或者空格分离。
只要掌握上面5个基本指令就能编辑pathData并且绘制一些酷炫的SVG。更详细全面的path指令请参阅链接。
估计你已经发现了,圆弧曲线指令A竟然那么多参数,这直接吓跑了很多的程序员,其实也并不难,且慢慢道来。
先根据图1里的代码来分析pathData指令。如图一所示,edge这个path使用了四个相对指令,首先指令h300 0相对向右水平移动300到点S,然后指令v0 300相对向下垂直移动300到T,再次指令h-300 0相对向左水平移动300到U,最后指令v0 -300相对向上垂直移动300到起点R,这样就根据属性strokeColor和strokeWidth绘制了四条直线,最后一个指令可以使用z代替。这很简单吧?!
再来看oval这个path。它使用了三条指令。第一条指令移动到点M处,第二条指令a75,75 0 1,1 150,0
绘制M-N-O的弧线,第三条指令a75,75 0 1,1 -150,0
绘制O-P-M的弧线。a指令共有7个参数:rx和ry表示椭圆的两个半径,x-axis-rotation表示x轴的旋转角度,x和y表示绘制椭圆弧线的终点,这5个参数很简单很好理解,large-arc-flag和sweep-flag这两个参数有点唬人。
解释large-arc-flag和sweep-flag这两个参数之前先考虑下这个题目:已知椭圆的半径rx和ry,请绘制若干条从起始点A到终点B的椭圆弧线。题目中是若干条,那到底几条啊?一般情况下会有四条椭圆弧线(特殊情况是rx=线段AB的一半或者ry=线段AB的一半,这时候的椭圆弧线只有两条),而large-arc-flag和sweep-flag这两个参数就从这四个椭圆弧线中选取了最终的一条进行绘制。large-arc-flag决定是大弧线还是小弧线,1大0小,sweep-flag决定是顺时针弧线还是逆时针弧线,1顺0逆。
有人可能会问,图1的oval path是个圆,竟然使用了两个a指令,使用一个a指令就能绘制圆的,只要终点回到起始点就能绘制圆的path了,刚开始我也是这样认为的,比如下面的代码:
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="300.0"
android:viewportWidth="300.0">
<path
android:name="circle"
android:fillColor="@android:color/holo_green_light"
android:pathData="
M150,150
a75,75 0 1,1 0,0"
android:strokeColor="#00000000" />
</vector>
上面的代码的path从起始点又回到了起始点,不会绘制任何东西,终点x y需要和起始点错开几个像素比如android:pathData="M150,150 a75,75 0 1,1 0,1"
就大约是一个圆path,为什么说是大约一个圆?因为起始点和终点不在一起,这只是一个圆的大弧线部分。推荐使用两条a指令绘制圆path,因为一条a指令绘制的不是真正的圆path。
group标签
path没有scale、rotate和translate这三种属性,因此也不能执行这三种属性动画,要达到这样的目的需要借助group这个标签。group标签也是vector的一个子标签,它可以作为path或者其他group的父标签使用,将path和group组合成一个组来附加一些变换操作,这些变换操作包括scale、rotate和translate共三种。这张图3是来自android官网的vector标签树型图:
图3 。<group>定义变换的细节,<clip-path>定义裁剪区域。根据这三个变换操作,group标签有以下属性:- android:name:group的名字;
- android:rotation:group的旋转角度,默认0。
- android:pivotX:scale和rotation变换中心点的X坐标,默认0;
- android:pivotY:scale和rotation变换中心点的Y坐标,默认0;
- android:scaleX:X轴方向的缩放,默认1;
- android:scaleY:Y轴方向的缩放,默认1;
- android:translateX:X轴方向的移动距离,默认0;
- android:translateY:Y轴方向的移动距离,默认0。
这是group的全部属性了,属性都很简单,不需要解释。
clip-path标签
<clip-path>定义当前绘制的剪切路径,就是图像的一部分剪切下来。注意,clip-path只对当前的vector和group以及当前vector和group的孩子有效。这个标签仅有两个属性:
- android:name:clip-path的名字;
- android:pathData:clip-path的路径。
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="300.0"
android:viewportWidth="300.0">
<clip-path android:name="clip_one" android:pathData="
M0 20a20 20 0 0 1 20 -20
l260 0a20 20 0 0 1 20 20
l0 260a20 20 0 0 1 -20 20
l-260 0a20 20 0 0 1 -20 -20
l0 -260"/>
<path
android:name="edge"
android:pathData="h300v300h-300v-300
M150 0 v300
M0 150 h300"
android:fillColor="@android:color/holo_green_light"
android:strokeColor="@android:color/holo_red_dark"
android:strokeWidth="1" />
<group>
<clip-path android:name="clip_two" android:pathData="M0 150h300v150h-300v-150"/>
<path
android:name="oval"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M20 20 l260,260M280 20 l-260,260h100"
android:strokeColor="#000000"
android:strokeWidth="15"/>
</group>
</vector>
上面代码定义了两个clip-path,其效果如图4所示。
图4.gif
build.gradle中vectorDrawables.useSupportLibrary属性
build.gradle中的vectorDrawables.useSupportLibrary默认是false,不设置为true的话会有什么问题吗?讨论这个问题也需要根据minSdkVersion具体分析:
-
minSdkVersion>=21
:这么高的API根本就不需要兼容包,仍然可以渲染矢量图; -
minSdkVersion<21
:不再使用矢量图兼容包,不能渲染矢量图,但是有趣的是vector标签仍然可以使用,低版本的API完全把VectorDrawable当作Drawable使用了,VectorDrawable的特性完全失效。原理是vector xml文件会生成对应的png文件,使用png方式渲染图片,和矢量图没有任何关系。值得注意的是生成的png图片size很小而且会忽略vector标签的android:tint属性(貌似只忽略这个属性,我试过vector标签的android:alpha属性在生成的png图片中仍然有效,生成的png文件目录是app/build/generated/res/pngs/debug,minSdkVersion>=21或者vectorDrawables.useSupportLibrary=true的话不会生成这些png图片)。而且path标签的color相关的属性不能引用colors.xml的值,android:strokeColor="@android:color/holo_red_dark"
这样写的话会编译失败,提示错误:Can't process attribute android:strokeColor="@android:color/holo_red_dark": references to other resources are not supported by build-time PNG generation
,而只能写原生的16进制color值比如android:strokeColor="#234aac"
。
想看具体信息请查看这篇文章。
SVG实战
我做的项目中一张扑克png资源大小2k左右,我试着用矢量图画这些扑克牌。代码如下:
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="400dp"
android:height="550dp"
android:viewportHeight="550"
android:viewportWidth="400.0">
<group android:name="poker_diamond_a">
<path
android:name="border"
android:strokeWidth="7"
android:strokeColor="#96999c"
android:fillColor="@android:color/white"
android:pathData="M5 25a20 20 0 0 1 20 -20
h350a20 20 0 0 1 20 20v500a20 20 0 0 1 -20 20h-350a20 20 0 0 1 -20 -20v-500"/>
<path android:name="a"
android:strokeWidth="8"
android:strokeColor="#cc0000"
android:strokeLineJoin="bevel"
android:pathData="M40 120
l40 -90
l40 90
l-16-35
h-48"/>
<path android:name="small_diamond" android:fillColor="#cc0000" android:pathData="M80 130l41 41l-41 41l-41 -41z"/>
<path android:name="big_diamond" android:fillColor="#cc0000" android:pathData="M260 310l100 100l-100 100l-100 -100z"/>
</group>
</vector>
图5
代码很简单,只有4条path。border路径顺序是1-2-3-4-5-6-7-8-1, a的路径是a-b-c-d-e,small_diamond的路径是e-f-g-h,big_diamond的路径是i-j-k-l。这个xml文件只有1k。
文章有错误的地方希望指正。
本文内容都是一些基础的东西,应该都能掌握,主要介绍了vector、group、path、clip-path这些标签常用的属性以及pathData属性对应的常用的指令,工作中掌握这些常用的知识就能比较熟练使用矢量图了。后续文章会剖析一些不常用的属性。