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》,最早提出图形混合概念,极大地推动了图形图像学的发展,有兴趣的同学可以自行查阅资料。
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个属性:
ImageView会多出下面6个属性:
我们对矢量图进行调色,先看效果如图4所示,图4中红色文字表示xml允许使用的6个tintMode。下面贴出代码:
// poker_a.xml
64dp
88dp
// activity_main.xml
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斜角。
如果你希望尖角多一些,就把这个属性设置大一些。在特别尖的拐弯处的点,点的这个比例可能大与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
这个属性,需要花点篇幅讨论下。讨论这个属性之前,先看下代码及其对应的效果:
代码中有两条path,前者的fillType是默认的noneZero,后者的fillType是evenOdd,除了这两个属性外,其余属性一模一样(name属性和M指令的起始位置为了显示的区别,忽略好吧)。两者的效果图如图7所示。fillType的原理是什么呢?为啥会导致这样的效果呢?
这里需要提一点,代码中每一条path都绘制了两个圆,四个圆的每一个圆都是通过两条a指令绘制完成的,如果你不清楚a指令的参数,请仔细看完Android矢量图I--VectorDrawable基础这篇文章并搞明白a指令后再回来看本文。这8条a指令的第5个参数都是1表示顺时针,请记住都是顺时针,因为fillType属性值noneZero跟path的方向有很大关系。
首先来看下默认的noneZero值。多看维基百科,那里的解释最权威。有一个多边形C,我们判断是否要对C的子多边形C1形成的一块区域进行填充,那么先定义一个变量value=0,根据value的值来决定是否对C1进行填充;在这个C1区域内任意选择一个点P,从这个点P向任意方向发射一条无限长的直线L,但是这个方向需要直线L与C1至少有一个交点,然后找出直线L与C所有的交点,每个交点处的方向是顺时针value++,顺时针value--,如果最后的value不是0,那么就对C1填充,否则不填充。
如果你不明白上面粗体文字的含义,那就根据上面代码名字为noneZero这个path来分析下。这条path包含4个a指令,其实也就是4个形状为弧线的子path:弧线AB、弧线BA、弧线CD、弧线DC,相应地我们就要判断四块区域(outer_a、outer_b、inner_a、inner_b)是否要进行fill(填充,这里的填充默认是填充内部)。对于区域outer_a,我们在其内部选择任意一点P,从P发射一条无限长直线,直线需要与子多边形弧线AB相交至少一个点,该点是P1,直线与多边形C的交点是P1,是顺时针,value=P1=1,不为0,该区域填充;对于区域inner_a,我们在其内部选择任意一点R,从R发射一条无限长直线,直线需要与子多边形弧线CD相交至少一个点,该点是R1,直线与多边形C的交点是R1和R2,value=R1+R1=1+1=2,不为0,该区域填充。如图8所示,
为了加深下印象,再来举个例子。看下面的代码:
你运行上面的代码,不出问题的矢量图应该如图9所示。代码首先从A到B逆时针绘制椭圆弧,然后从C到D顺时针绘制椭圆弧,最后从D到C顺时针绘制椭圆弧。最终的多边形C包含三个子区域outer_a、inner_a、inner_b。对于outer_a,其value=P=-1,非0那就填充;对于inner_a,其value=Q1=1,非0那就填充;对于inner_b,其value=R1+R2=1+(-1)=0,0那就不填充。
如果你已经明白了noneZero,那么evenOdd就非常非常简单了,先看维基百科,它与指针方向没有关系了,一句话你就能明白:如果虚拟直线与多边形C的交点数目为奇数就填充内部,偶数就不填充内部。如果还不明白,就举个例子,看代码:
上面代码的结果也是如图9所示,那么拿图9来解释:对于outer_a,其只有一个交点P,奇数那就填充;对于inner_a,其只有一个交点Q,奇数那就填充;对于inner_b,有两个交点R1和R2,偶数那就不填充。
怀着紧张的心情,fillType属性终于介绍了完了,真害怕写错而误导读者,如果有错误的地方或者有更好的分析方法,请大神批评指正。这里有两篇文章,帮助理解fillType:文章1、文章2。再说一点,如果你是用tint属性对矢量图进行调色,注意fillType属性值会对结果造成同样的影响。
最后,你如果查看Paint类的内部类FillType的源码,会发现还有另外两种fillType:
/**
* Enum for the ways a path may be filled.
*/
public enum FillType {
// these must match the values in SkPath.h
/**
* Specifies that "inside" is computed by a non-zero sum of signed
* edge crossings.
*/
WINDING (0),
/**
* Specifies that "inside" is computed by an odd number of edge
* crossings.
*/
EVEN_ODD (1),
/**
* Same as {@link #WINDING}, but draws outside of the path, rather than inside.
*/
INVERSE_WINDING (2),
/**
* Same as {@link #EVEN_ODD}, but draws outside of the path, rather than inside.
*/
INVERSE_EVEN_ODD(3);
FillType(int ni) {
nativeInt = ni;
}
final int nativeInt;
}
对这另外两种fillType,Google程序员的注释已经解释的很清楚了,我就不解释了,否则可能越解释越乱,越乱越糊涂。
这篇文章和上篇文章Android矢量图I--VectorDrawable基础一起把VectorDrawable的全部属性都介绍了,相信使用矢量图应该更加轻松了,。
后续文章会对矢量动画AnimatedVectorDrawable进行研究,敬请期待。