Android 矢量图 I--VectorDrawable 基础 介绍了 VectorDrawable 的用法及其常用属性,掌握上篇文章的基础知识差不多就能在项目中使用矢量图应对一些基本的需求开发了.这篇文章介绍其余的全部属性,其中的一些属性在实际开发中可能用到的比较少.
vector 属性
android:alpha:矢量图的透明度,范围 0-1,默认 1;
android:tint:矢量图的颜色,这个颜色值会覆盖所有与 color 相关的属性比如 path 的 fillColor 和 strokeColor 等;这个属性会被 API setColorFilter(ColorFilter) 覆盖.
android:tintMode:色彩混合模式,可选值有很多,下面详细讨论,默认 src_in.
android:autoMirrored
:当布局方向变成 right-to-left 的时候,矢量图是否自动镜像,默认 false,这个属性在 API>=19 才生效.
关于 tintMode,先看下 PorterDuff 这类的源码:
public class PorterDuff {
public PorterDuff() {
throw new RuntimeException("Stub!");
}
public static enum Mode {
CLEAR, /** [Sa, Sc] */
DST, /** [Da, Dc] */
DST_ATOP, /** [Sa, Sa * Dc + Sc * (1 - Da)] */
DST_IN, /** [Sa * Da, Sa * Dc] */
DST_OUT, /** [Da * (1 - Sa), Dc * (1 - Sa)] */
DST_OVER, /** [Sa + (1 - Sa)*Da, Rc = Dc + (1 - Da)*Sc] */
SRC, /** [Sa, Sc] */
SRC_ATOP, /** [Da, Sc * Da + (1 - Sa) * Dc] */
SRC_IN, /** [Sa * Da, Sc * Da] */
SRC_OUT, /** [Sa * (1 - Da), Sc * (1 - Da)] */
SRC_OVER, /** [Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc] */
DARKEN, /** [Sa + Da - Sa*Da, Sc*(1 - Da) + Dc*(1 - Sa) + min(Sc, Dc)] */
XOR, /** [Sa + Da - 2 * Sa * Da, Sc * (1 - Da) + (1 - Sa) * Dc] */
LIGHTEN, /** [Sa + Da - Sa*Da, Sc*(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)] */
MULTIPLY, /** [Sa * Da, Sc * Dc] */
SCREEN, /** [Sa + Da - Sa * Da, Sc + Dc - Sc * Dc] */
/** Saturate(S + D) */
ADD,
OVERLAY;
private Mode() {
}
}
}
首先类名 PorterDuff 是什么意思呢?PorterDuff 是两个人名的组合: Thomas Porter 和 Tom Duff,他们 1984 年在 ACM SIGGRAPH 计算机图形学发表论文《Compositing digital images》,最早提出图形混合概念,极大地推动了图形图像学的发展,有兴趣的同学可以自行查阅资料.
图 1
PorterDuff 共有 18 个模式可选,但是 android:tintMode 可选值只有六个:MULTIPLY,SCREEN,ADD,SRC_ATOP,SRC_IN,SRC_OVER,(xml 文件只提供这 6 个的原因我不知道,请大神留言告知).当然想使用其余 12 个 tintMode 模式也是可以的,需要用代码调用 API Drawable.setTintMode(PorterDuff.Mode) 即可,可以达到相应 tintMode 的效果.
tint 属性是 Android 5.0 引入的,Android 6.0 又引入了 drawableTint 的属性.
Button 和 TextView 等一些组件会多出下面 6 个属性:
图 2
ImageView 会多出下面 6 个属性:
图 3
图 4
我们对矢量图进行调色,先看效果如图 4 所示,图 4 中红色文字表示 xml 允许使用的 6 个 tintMode.下面贴出代码:
// poker_a.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="400dp"
android:height="550dp"
android:tint="@android:color/holo_purple"
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="@android:color/holo_red_dark"
android:strokeLineJoin="bevel"
android:pathData="M40 120
l40 -90
l40 90
l-16-35
h-48"/>
<path android:name="small_diamond" android:fillColor="@android:color/holo_blue_dark" android:pathData="M80 130l41 41l-41 41l-41 -41z"/>
<path android:name="big_diamond" android:fillColor="@android:color/holo_green_dark" android:pathData="M260 310l100 100l-100 100l-100 -100z"/>
</group>
</vector>
<dimen name="width">64dp</dimen>
<dimen name="height">88dp</dimen>
// activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:orientation="vertical"
android:padding="5dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:layout_width="@dimen/width"
android:layout_height="@dimen/height"
android:src="@drawable/poker_a"
android:layout_marginLeft="10dp"
android:tint="@android:color/transparent"/>
<View
android:layout_width="@dimen/width"
android:layout_height="@dimen/height"
android:layout_marginLeft="10dp"
android:background="@android:color/holo_purple"/>
<ImageView
android:id="@+id/CLEAR"
android:layout_width="@dimen/width"
android:layout_height="@dimen/height"
android:src="@drawable/poker_a"
android:layout_marginLeft="10dp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="5dp">
<ImageView
android:id="@+id/DST"
android:layout_width="@dimen/width"
android:layout_height="@dimen/height"
android:src="@drawable/poker_a" />
<ImageView
android:id="@+id/DST_ATOP"
android:layout_width="@dimen/width"
android:layout_height="@dimen/height"
android:src="@drawable/poker_a"
android:layout_marginLeft="5dp" />
<ImageView
android:id="@+id/DST_IN"
android:layout_width="@dimen/width"
android:layout_height="@dimen/height"
android:src="@drawable/poker_a"
android:layout_marginLeft="5dp" />
<ImageView
android:id="@+id/DST_OUT"
android:layout_width="@dimen/width"
android:layout_height="@dimen/height"
android:src="@drawable/poker_a"
android:layout_marginLeft="5dp" />
<ImageView
android:id="@+id/DST_OVER"
android:layout_width="@dimen/width"
android:layout_height="@dimen/height"
android:src="@drawable/poker_a"
android:layout_marginLeft="5dp"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="5dp">
<ImageView
android:id="@+id/SRC"
android:layout_width="@dimen/width"
android:layout_height="@dimen/height"
android:src="@drawable/poker_a" />
<ImageView
android:id="@+id/SRC_ATOP"
android:layout_width="@dimen/width"
android:layout_height="@dimen/height"
android:src="@drawable/poker_a"
android:layout_marginLeft="5dp" />
<ImageView
android:id="@+id/SRC_IN"
android:layout_width="@dimen/width"
android:layout_height="@dimen/height"
android:src="@drawable/poker_a"
android:layout_marginLeft="5dp" />
<ImageView
android:id="@+id/SRC_OUT"
android:layout_width="@dimen/width"
android:layout_height="@dimen/height"
android:src="@drawable/poker_a"
android:layout_marginLeft="5dp" />
<ImageView
android:id="@+id/SRC_OVER"
android:layout_width="@dimen/width"
android:layout_height="@dimen/height"
android:src="@drawable/poker_a"
android:layout_marginLeft="5dp"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="5dp">
<ImageView
android:id="@+id/DARKEN"
android:layout_width="@dimen/width"
android:layout_height="@dimen/height"
android:src="@drawable/poker_a" />
<ImageView
android:id="@+id/XOR"
android:layout_width="@dimen/width"
android:layout_height="@dimen/height"
android:src="@drawable/poker_a"
android:layout_marginLeft="5dp" />
<ImageView
android:id="@+id/LIGHTEN"
android:layout_width="@dimen/width"
android:layout_height="@dimen/height"
android:src="@drawable/poker_a"
android:layout_marginLeft="5dp" />
<ImageView
android:id="@+id/MULTIPLY"
android:layout_width="@dimen/width"
android:layout_height="@dimen/height"
android:src="@drawable/poker_a"
android:layout_marginLeft="5dp" />
<ImageView
android:id="@+id/SCREEN"
android:layout_width="@dimen/width"
android:layout_height="@dimen/height"
android:src="@drawable/poker_a"
android:layout_marginLeft="5dp"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="5dp">
<ImageView
android:id="@+id/ADD"
android:layout_width="@dimen/width"
android:layout_height="@dimen/height"
android:src="@drawable/poker_a" />
<ImageView
android:id="@+id/OVERLAY"
android:layout_width="@dimen/width"
android:layout_height="@dimen/height"
android:src="@drawable/poker_a"
android:layout_marginLeft="5dp" />
</LinearLayout>
</LinearLayout>
MainActivity.java 代码:
// MainActivity.java
public class MainActivity extends AppCompatActivity {
static {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
((ImageView) findViewById(R.id.CLEAR)).getDrawable().mutate().setTintMode(PorterDuff.Mode.CLEAR);
((ImageView) findViewById(R.id.DST)).getDrawable().mutate().setTintMode(PorterDuff.Mode.DST);
((ImageView) findViewById(R.id.DST_ATOP)).getDrawable().mutate().setTintMode(PorterDuff.Mode.DST_ATOP);
((ImageView) findViewById(R.id.DST_IN)).getDrawable().mutate().setTintMode(PorterDuff.Mode.DST_IN);
((ImageView) findViewById(R.id.DST_OUT)).getDrawable().mutate().setTintMode(PorterDuff.Mode.DST_OUT);
((ImageView) findViewById(R.id.DST_OVER)).getDrawable().mutate().setTintMode(PorterDuff.Mode.DST_OVER);
((ImageView) findViewById(R.id.SRC)).getDrawable().mutate().setTintMode(PorterDuff.Mode.SRC);
((ImageView) findViewById(R.id.SRC_ATOP)).getDrawable().mutate().setTintMode(PorterDuff.Mode.SRC_ATOP);
((ImageView) findViewById(R.id.SRC_IN)).getDrawable().mutate().setTintMode(PorterDuff.Mode.SRC_IN);
((ImageView) findViewById(R.id.SRC_OUT)).getDrawable().mutate().setTintMode(PorterDuff.Mode.SRC_OUT);
((ImageView) findViewById(R.id.SRC_OVER)).getDrawable().mutate().setTintMode(PorterDuff.Mode.SRC_OVER);
((ImageView) findViewById(R.id.DARKEN)).getDrawable().mutate().setTintMode(PorterDuff.Mode.DARKEN);
((ImageView) findViewById(R.id.XOR)).getDrawable().mutate().setTintMode(PorterDuff.Mode.XOR);
((ImageView) findViewById(R.id.LIGHTEN)).getDrawable().mutate().setTintMode(PorterDuff.Mode.LIGHTEN);
((ImageView) findViewById(R.id.MULTIPLY)).getDrawable().mutate().setTintMode(PorterDuff.Mode.MULTIPLY);
((ImageView) findViewById(R.id.SCREEN)).getDrawable().mutate().setTintMode(PorterDuff.Mode.SCREEN);
((ImageView) findViewById(R.id.ADD)).getDrawable().mutate().setTintMode(PorterDuff.Mode.ADD);
((ImageView) findViewById(R.id.OVERLAY)).getDrawable().mutate().setTintMode(PorterDuff.Mode.OVERLAY);
}
}
色彩混合涉及到两个对象,目标对象和源对象,这里的目标对象是 poker_a.xml 这个矢量图,源对象是 tint 设置的颜色值,为啥这样说呢,记着这个原则,先绘制的是目标对象,我们就是要对目标对象进行调色.对目标对象调色后的对象叫做复合对象.
有了这两个对象,怎么进行调色呢?这里有很多计算公式,公式中又有很多元素,先来看下这些元素:
* Sa:Source alpha,源对象的Alpha通道;
* Sc:Source color,源对象的颜色;
* Da:Destination alpha,目标对象的Alpha通道;
* Dc:Destination color,目标对象的颜色;
* [a,c]:对象的ARGB值,a表示alpha,c表示color.
下面对这 18 个 tintMode 进行剖析,该文受到 这篇文章 的启发.
CLEAR
复合对象的 ARGB 值是 [0,0],完全透明,相当于清除画布上的图像了.
DST
[Da, Dc],只保留目标对象的 alpha 和 color 值,因此绘制出来的只有目标对象,相当于根本就没有进行调色.
DST_ATOP
[Sa, Sa * Dc + Sc * (1 - Da)],两者相交处绘制目标对象,不相交的地方绘制源对象,并且相交处的效果会受到源对象和目标对象 alpha 的影响.
DST_IN
[Sa * Da, Sa * Dc],两者相交的地方绘制目标对象,不相交的地方不进行绘制,并且相交处的效果会受到源对象对应地方透明度的影响.
DST_OUT
[Da * (1 - Sa), Dc * (1 - Sa)],两者不相交的地方绘制目标对象,相交处根据源对象 alpha 进行过滤,完全不透明处则完全过滤,完全透明则不过滤.(亲测正确)
DST_OVER
[Sa + (1 - Sa)Da, Rc = Dc + (1 - Da)Sc],目标对象绘制在源对象的上方.
SRC
[Sa, Sc],只保留源对象的 alpha 和 color,因此绘制出来只有源对象.
SRC_ATOP
[Da, Sc * Da + (1 - Sa) * Dc],两者相交处绘制源对象,不相交的地方绘制目标对象,并且相交处的效果会受到源对象和目标对象 alpha 的影响.
SRC_IN
[Sa * Da, Sc * Da],两者相交处绘制源对象,不相交的地方不进行绘制,并且相交处的效果会受到源对象对应地方透明度的影响.
SRC_OUT
[Sa * (1 - Da), Sc * (1 - Da)],两者不相交的地方绘制源对象,相交处根据目标对象 alpha 进行过滤,完全不透明处则完全过滤,完全透明则不过滤.(亲测正确)
SRC_OVER
[Sa + (1 - Sa)Da, Rc = Sc + (1 - Sa)Dc],源对象绘制在目标对象的上方.
DARKEN
[Sa + Da - SaDa, Sc(1 - Da) + Dc(1 - Sa) + min(Sc, Dc)],顾名思义,效果会变暗.进行对应像素比较,取较暗值 (即较小值),如果色值相同则进行混合;如果两个对象都完全不透明,取较暗值,否则使用上面算法进行计算,受到源对象和目标对象对应色值和 alpha 值影响.结果复合对象的 alpha 值会变大.
XOR
[Sa + Da - 2 * Sa * Da, Sc * (1 - Da) + (1 - Sa) * Dc],不相交的地方按原样绘制源对象和目标对象,相交的地方受到对应 alpha 和颜色值影响,按公式进行计算,如果都完全不透明则相交处完全不绘制.
LIGHTEN
[Sa + Da - SaDa, Sc(1 - Da) + Dc(1 - Sa) + max(Sc, Dc)],顾名思义,效果会变亮.进行对应像素比较,取较亮值 (即较大值),如果色值相同则进行混合;如果两个对象都完全不透明,取较亮值,否则使用上面算法进行计算,受到源对象和目标对象对应色值和 alpha 值影响.
MULTIPLY
[Sa * Da, Sc * Dc],正片叠底,即查看每个通道中的颜色信息,目标色与源色复合.结果色总是较暗的颜色.任何颜色与黑色复合产生黑色,任何颜色与白色复合保持不变.(这个理论貌似和现实生活的颜色混合的结果不一致,现实生活中黄色和白色混合会是黄白色而不是白色).
SCREEN
[Sa + Da - Sa * Da, Sc + Dc - Sc * Dc],滤色模式,这个模式与我们所用的显示屏原理相同,因此也被翻译成屏幕模式;保留两个图层中较白的部分,较暗的部分被遮盖,图层中纯黑的部分变成完全透明,纯白部分完全不透明,其他的颜色根据颜色级别产生半透明的效果.
ADD
Saturate(S + D),饱和度叠加
OVERLAY
算法同时进行进行 Multiply(正片叠底)混合还是 Screen(屏幕)混合,是进行 Multiply 混合还是 Screen 混合,取决于目标对象的颜色值,目标对象颜色的高光与阴影部分的亮度等细节会被保留.
这 18 个模式终于介绍完了,再次感谢 这篇文章 的作者.
path 属性
android:strokeLineCap:顾名思义,设置线条的帽子,round 圆角,square 正方形,butt 臀,默认是 butt;
android:strokeLineJoin:线条拐弯处的样式,round 圆角,bevel 斜角,miter 斜切尖角,默认是 miter;
android:strokeMiterLimit:android:strokeLineJoin 为 miter 的时候这个属性才发挥作用.设置 miter 斜切尖角长度 (用 miter_length 表示) 与线条宽度 (用 line_width 表示) 比例值的上限,默认是 4,strokeMiterLimit = miter_length / line_width,这个属性设定了这个比例的最大值,超过这个值的尖角不再显示尖角而是 bevel 斜角.
图 5
图 6
如果你希望尖角多一些,就把这个属性设置大一些.在特别尖的拐弯处的点,点的这个比例可能大与 strokeMiterLimit,那么就不显示尖角效果而是类似 bevel 斜角的效果,这样看起来不是很突兀,比较美观.
图 5 和图 6 来自 文章 ;
android:trimPathStart:从路径起始位置截断路径的比率,取值范围 0-1,默认 0;
android:trimPathEnd:从路径结束位置截断路径的比率,取值范围 0-1,默认 1;
android:trimPathOffset:设置路径截取的偏移比例,取值范围 0-1,默认 0;
利用 android:trimPathStart 和 android:trimPathEnd 可以做一些入场和出场动画, 链接
android:fillType:API 24 才引入的这个属性,取值 nonZero 和 evenOdd,默认 nonZero.
关于 android:fillType 这个属性,需要花点篇幅讨论下.讨论这个属性之前,先看下代码及其对应的效果:
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportHeight="600"
android:viewportWidth="600">
<path
android:name="noneZero"
android:strokeWidth="2"
android:strokeColor="#B32D20"
android:fillColor="#3C8FC1"
android:pathData="M20 120 a100 100 0 1 1 200 0 a100 100 0 1 1 -200 0
M40 120 a80 80 0 1 1 160 0 a80 80 0 1 1 -160 0"/>
<path
android:name="evenOdd"
android:strokeWidth="2"
android:strokeColor="#B32D20"
android:fillColor="#3C8FC1"
android:fillType="evenOdd"
android:pathData="M260 120 a100 100 0 1 1 200 0 a100 100 0 1 1 -200 0
M280 120 a80 80 0 1 1 160 0 a80 80 0 1 1 -160 0"/>
</vector>
图 7
代码中有两条 path,前者的 fillType 是默认的 noneZero,后者的 fillType 是 evenOdd,除了这两个属性外,其余属性一模一样 (name 属性和 M 指令的起始位置为了显示的区别,忽略好吧
来源: http://www.jianshu.com/p/89efdbe01ac9