Android矢量图(一)--VectorDrawable基础

背景

维基百科中的定义:

可缩放向量图形Scalable Vector GraphicsSVG)是一种基于可扩展标记语言(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设备可以分别使用VectorDrawableCompatAnimatedVectorDrawableCompat这两个兼容包来同样达到渲染矢量图的目的。本文只讨论矢量图,不讨论矢量动画。

准备

使用矢量图要根据minSdkVersion来分3中不同的情况:

  1. minSdkVersion>=21:用xml文件或者代码定义VectorDrawable,和普通的Drawable用法一样,不再需要额外任何东西;如何编写矢量图,下文有介绍;
  2. minSdkVersion<21:如果想要渲染矢量图的话必须在app模块的build.gralde文件里添加一行代码:
defaultConfig {
    vectorDrawables.useSupportLibrary = true
}
  1. 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文件有两种方法来创建:

  1. 右击drawable-->Drawable resource file-->设置root element为vector,这样的矢量图绘制逻辑完全掌握在开发者手里;
  2. 右击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"

  1. width和height:当使用这个矢量图的View的宽高是wrap_content 的时候这两个属性才生效;
  2. viewportWidth和viewportHeight:决定画布的宽高,是定义的一个虚拟空间,方便编辑pathData属性,如果pathData中的点超出了这个虚拟空间,超出的部分将不会展现给用户;虚拟空间的原点仍然还是在左上角(R点就是原点)。

path标签是vector标签的子标签,它使用了以下属性:

  1. android:name:类似View的id属性,方便path被引用,如上图的edge是虚拟空间四个边界的path,oval是一个椭圆的path;
  2. android:fillColor:填充path的颜色,如果没有定义则不填充path
  3. android:strokeColor:path边框颜色,如果没有定义则不显示边框
  4. android:strokeWidth:path边框的粗细尺寸
  5. android:pathData:path指令,决定path的移动和绘制逻辑,这个是最主要的属性,下面详细讨论。

更多path属性请参考链接

pathData的指令和Path类的API方法基本差不多,比如M指令对应moveTo方法,m指令对应rMoveTo方法,下面是一些基本的指令:

    1. Mx,y:移动到点(x,y)
    1. Lx,y:直线连到点x,y,简化命令H(x)水平连接和V(y)垂直连接;
    1. Qx1,y1 x2,y2:二阶贝塞尔曲线,控制点(x1,y1),终点x2,y2;
    1. Cx1,y1 x2,y2 x3,y3:三阶贝塞尔曲线,控制点(x1,y1)( x2,y2),终点x3,y3;
    1. Tx y:平滑的二阶贝塞尔曲线,参数只有一个点(x,y),这个点是结束点,控制点是前一个二阶贝塞尔曲线的控制点相对于前一个贝塞尔曲线的结束点的镜像点。
    1. Sx2,y2 x,y:平滑的三阶贝塞尔曲线,参数为(x2,y2 x,y) ,x2,y2 为第二个控制点,x,y为绘制终点,那么第一个控制点则是前一个三阶曲线的第二个控制点相对于前一个三阶曲线终点的镜像点。
    1. Arx,ry x-axis-rotation large-arc-flag,sweep-flag x,y:ellipse arc圆弧曲线
    1. 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逆。

图2
。看图2希望你能明白这两个参数的意义。

有人可能会问,图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没有scalerotatetranslate这三种属性,因此也不能执行这三种属性动画,要达到这样的目的需要借助group这个标签。group标签也是vector的一个子标签,它可以作为path或者其他group的父标签使用,将path和group组合成一个组来附加一些变换操作,这些变换操作包括scalerotatetranslate共三种。这张图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具体分析:

  1. minSdkVersion>=21:这么高的API根本就不需要兼容包,仍然可以渲染矢量图;
  2. 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属性对应的常用的指令,工作中掌握这些常用的知识就能比较熟练使用矢量图了。后续文章会剖析一些不常用的属性。

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