前言
在平时自定义View的过程中,基础的测量绘制等操作能够打造一个基本的自定义View,但如果想让你的自定义View玩出更多花样,就要结合一些比较灵活的绘制方式,比如本文要讲的混合模式——Xfermode。之前写的几个自定义View也有用到它来实现:
传送门:『Android自定义View实战』给我一个图标,还你一个水波纹进度球
传送门:『Android自定义View实战』自定义完美的刮刮乐效果
之前一直没有去完整地总结Xfermode,本文将针对 Xfermode的各个模式展开详细的分析,理解其各个模式的作用。
正文
什么是混合模式?
其概念最早来自于SIGGRAPH的Tomas Proter和Tom Duff,混合图形的概念极大地推动了图形图像学的发展,延伸到计算机图形图像学像Adobe和AutoDesk公司著名的多款设计软件都可以说一定程度上受到影响。从字面上其实大概猜到了它的作用,混合模式就是将画布中的两个图像,按照一定的算法,合成一个新的图像。在Android中提供了混合模式相关的类——Xfermode,它派生出来的一个子类——ProterDuffXfermode就是我们平常在Android中使用混合模式时需要用到的类,它定义了很多模式供我们选择,实现两个图像叠加在一起时的多样化呈现。
混合模式有哪些?
首先理解下图形的一些概念,图像是由很多个像素点组合而成,每个像素点都有一个自己的颜色通道组合,即所谓的ARGB:
A代表透明度Alpha
R代表红色通道Red
G代表绿色通道Green
B代表蓝色通道Blue
混合模式中,将ARGB划分为两个部分,即A+RGB的格式,也就是每个像素都会被划分为这样的形式的来描述:[Alpha,RGB],即透明度+颜色值,那么如果是两个图层叠在在一起时,它们交集的区域可以有很多种可能性,例如[取图层1的的透明度,取图层1的颜色值]
,[取图层2的透明度,取图层2的颜色值]
,稍微复杂一点可以是[取图层1的透明度*图层2的透明度,取图层1的色值*图层2的透明度]
....等等,将两个图像分别称之为源图像(Src)和目标图像(Dst),从而可以根据这些算法衍生出很多种模式,也就是我们的18种混合模式:
模式 | 组合算法 | 效果 |
---|---|---|
ADD | Saturate(S + D) | 饱和相加,对图像饱和度进行相加 |
CLEAR | [0, 0] | 交集区域alpha和rgb值均为0 |
DARKEN | [Sa + Da - SaDa, Sc(1 - Da) + Dc*(1 - Sa) + min(Sc, Dc)] | 变暗,较深的颜色会覆盖浅色 |
DST | [Da, Dc] | 交集区域只显示DST的透明度和色值 |
DST_ATOP | [Sa, Sa * Dc + Sc * (1 - Da)] | 相交处绘制DST的部分,其他区域只显示SRC |
DST_IN | [Sa * Da, Sa * Dc] | 只在DST和SRC相交的地方绘制DST的部分 |
DST_OUT | [Da * (1 - Sa), Dc * (1 - Sa)] | 只在DST和SRC交集之外的区域绘制DST的部分 |
DST_OVER | [Sa + (1 - Sa)Da, Rc = Dc + (1 - Da)Sc] | DST盖在SRC上面 |
LIGHTEN | [Sa + Da - SaDa, Sc(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)] | 相交的区域变亮 |
MULTIPLY | [Sa * Da, Sc * Dc] | 透明度和色值均不为0的地方绘制 |
OVERLAY | 叠加效果 | |
SCREEN | [Sa + Da - Sa * Da, Sc + Dc - Sc * Dc] | 保留两个图层中较白的部分,较暗的部分被遮盖 |
SRC | [Sa, Sc] | 交集区域只显示SRC的透明度和色值 |
SRC_ATOP | [Da, Sc * Da + (1 - Sa) * Dc] | 相交处绘制SRC的部分,其他区域只显示DST |
SRC_IN | [Sa * Da, Sc * Da] | 只在DST和SRC相交的地方绘制SRC的部分 |
SRC_OUT | [Sa * (1 - Da), Sc * (1 - Da)] | 只在DST和SRC交集之外的区域绘制SRC的部分 |
SRC_OVER | [Sa + (1 - Sa)Da, Rc = Sc + (1 - Sa)Dc] | SRC盖在DST上面 |
XOR | [Sa + Da - 2 * Sa * Da, Sc * (1 - Da) + (1 - Sa) * Dc] | 相交的区域受透明度和色值的影响,如果完全不透明相交处不绘制 |
它们都是根据以上公式计算出来对应的绘制形式,其中,Sa、Sc代表源图像的透明度(Src Alpha)和色值(Src Color),Ds、Dc代表目标图像的透明度(Dst Alpha)和色值(Dst Color),例如 DST_IN 模式,它是根据 [Sa * Da, Sa * Dc] 算法来进行绘制,按照上文讲的 [Alpha,RGB] 的公式套进去,那么就是:
交集区域的透明度 = 源图像的透明度 * 目标图像的透明度
交集区域的色值 = 源图像的透明度 * 目标图像的色值
可以看到,无论是透明度还是色值,源图像只有透明度派上用场,换句话说,就是去除了源图像的色值,那么就只剩透明度了,所以在源图像和目标图像的交集区域,只会显示出目标图像的效果,但是其透明度会受到源图像的透明度的影响。
如何使用混合模式?
1.禁用硬件加速
Android中混合模式的操作其实不复杂,首先,也是容易遗漏的一个步骤,就是禁用硬件加速,部分混合模式的使用必须在禁用硬件加速的前提下进行,否则出来的效果会所出入,因此一般保险起见,使用混合模式之前都禁用硬件加速,如果不想影响应用的其他地方,可以只在自定义View的构造方法中调用:
//禁用硬件加速
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
2.绘制目标图像
先用画笔绘制一个形状或图片,作为DST图:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(dstBm, 0, 0, paint);
}
3.设置混合模式
接着为画笔设置一个混合模式:
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
4.绘制源图像
canvas.drawBitmap(srcBm, 0, 0, paint);
5.清除混合模式
最后清除画笔混合模式,以防止下次调用onDraw的时候受影响:
paint.setXfermode(null);
汇总起来也就是:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(dstBm, 0, 0, paint);
paint.setXfermode(duffXfermode);
canvas.drawBitmap(srcBm, 0, 0, paint);
paint.setXfermode(null);
}
混合模式效果
纸上得来终觉浅,我们通过一个demo,来看下这些模式真正呈现出来的效果是怎样的,创建一个自定义View,并绘制一个圆形和一个方形分别作为目标图和源图:
public class DuffModeView extends View{
private Paint paint;
private int width, height;
private Bitmap srcBm, dstBm;
public DuffModeView(Context context) {
this(context, null);
}
public DuffModeView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public DuffModeView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
//禁用硬件加速
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
//初始化画笔
paint = new Paint();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
width = right - left;
height = bottom - top;
srcBm = createSrcBitmap(width, height);
dstBm = createDstBitmap(width, height);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(dstBm, 0, 0, paint);
canvas.drawBitmap(srcBm, 0, 0, paint);
}
public Bitmap createDstBitmap(int width, int height) {
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Paint dstPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
dstPaint.setColor(Color.parseColor("#00b7ee"));
canvas.drawCircle(width / 3, height / 3, width / 3, dstPaint);
return bitmap;
}
public Bitmap createSrcBitmap(int width, int height) {
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Paint scrPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
scrPaint.setColor(Color.parseColor("#ec6941"));
canvas.drawRect(new Rect(width / 3, height / 3, width, height), scrPaint);
return bitmap;
}
}
将View的宽高划分为了3等分,将圆形绘制在左上角,正方形绘制在右下角,并分别设置不同的颜色,中间有一部分相交,效果如下:
“素材”准备好了,那么就可以开始“动刀”了,我们实例化一个PorterDuffXfermode
对象,将其设置给刚才中的画笔:
duffXfermode = new PorterDuffXfermode(PorterDuff.Mode.ADD);
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//离屏绘制
int layerID = canvas.saveLayer(0, 0, getWidth(), getHeight(), paint, Canvas.ALL_SAVE_FLAG);
canvas.drawBitmap(dstBm, 0, 0, paint);
paint.setXfermode(duffXfermode);
canvas.drawBitmap(srcBm, 0, 0, paint);
paint.setXfermode(null);
canvas.restoreToCount(layerID);
}
这里onDraw中有几个注意的点,我们通过Canvas的saveLayer和restoreToCount将混合模式置于单独的层面中进行,以免混合模式相关的操作对画布的其他区域产生影响,通俗的讲,可以理解成从Canvas中抽取出一层单独的图层,一切绘制都不会影响到原本的画布,等调用restoreToCount之后,就会把我们的最终效果加回到Canvas上。(是不是有点类似Photoshop的图层概念)
接着经过以下几个步骤:
1.调用drawBitmap绘制了DST图
2.为画笔设置混合模式
3.用设置了混合模式的画笔绘制SRC图
4.清除画笔混合模式
这里采用了PorterDuff.Mode.ADD模式,呈现出来的效果如下:
可以看到中间相交区域的不是蓝色也不是红色,而是它们的饱和度相加之后得到的结果。
我们只需要替换PorterDuffXfermode的构造方法参数,改成其他模式,即可看到其他模式的效果,依次如下:
PorterDuff.Mode.CLEAR
CLEAR模式的公式是 [0, 0],也就是透明度和色值均为0,由于我们是将混合模式设置给画笔之后才绘制红色方形,所以可以看到蓝色(DST)与红色(SRC)相交的区域,蓝色部分也一并被清除掉,也就是交集区域透明度和色值均为0,因此此时的SRC就类似是一块“橡皮擦”的效果。
PorterDuff.Mode.DARKEN
相交的区域,看起来像是变成了另外一种颜色,其实这种模式就是比较深色的会覆盖浅色的,可以尝试把其中一个改成黑色或者白色,就更明显了~
PorterDuff.Mode.DST
DST模式的公式是 [Da, Dc],也就是整个红色方形的区域,都取决于这两个值来显示,那么在红色方形范围内,也就只有两者相交的区域才有蓝色的透明度和色值,圆形是DST图,方形是SRC图,可以看到最终的效果只剩下DST图了,也就是交集区域只显示DST的透明度和色值。
PorterDuff.Mode.DST_ATOP
DST_ATOP的公式为 [Sa, Sa * Dc + Sc * (1 - Da)],透明度取决于SRC的透明度,在两者相交的区域,Da为1,所以其实色值只剩Sa*Dc,就是在交集的区域显示DST图的色值,不相交的区域只显示SRC的部分。
PorterDuff.Mode.DST_IN
DST_IN的公式为 [Sa * Da, Sa * Dc],可以看到,只在交集的区域有颜色和透明值的显示,且显示的是DST图的部分,这是因为在两者相交的区域,Sa不为0,所以Sa*Da显示DST的透明度,Sa*Dc显示DST的色值,在相交之外的区域,要么是Sa为0,要么是Da为0,所以这些区域计算出来的结果都是[0,0]。
PorterDuff.Mode.DST_OUT
DST_OUT的公式为 [Da * (1 - Sa), Dc * (1 - Sa)],仔细看公式,它与DST_IN的计算方式区别仅仅在于(1-Sa),在两者不相交的区域,且Sa为0的地方,显示DST的部分,可以看到,它与DST_IN的效果恰恰就是互补。
PorterDuff.Mode.DST_OVER
呈现出来的效果,如同DST图盖住了SRC图,所以称之为DST_OVER~
PorterDuff.Mode.LIGHTEN
与DARKEN相反,浅色覆盖深色,会呈现出变亮的效果。
PorterDuff.Mode.MULTIPLY
MULTIPLY模式的公式为 [Sa * Da, Sc * Dc],透明度为Sa*Da,说明只在相交的区域有透明度,其他区域均由于Sa=0或者Da=0导致透明度为0,色值为SRC和DST的色值相乘后的结果。
PorterDuff.Mode.OVERLAY
在这种模式下,虽然是覆盖,但是并不会完全遮挡,而是形成叠加的视觉效果。
PorterDuff.Mode.SCREEN
两个形状的交集区域,呈现出了另一种颜色,但其实是取了两者较白的部分混合而成的效果。
PorterDuff.Mode.SRC
SRC的公式为 [Sa, Sc],在两者相交区域,Sa和Sc均不为0,在两者相交区域之外的地方,就取决于SRC的透明度和色值了,形成的效果就是混合后只显示SRC的部分。
PorterDuff.Mode.SRC_ATOP
SRC_ATOP的公式为 [Da, Sc * Da + (1 - Sa) * Dc],透明度取决于DST的透明度,在两者相交的区域,Sa为1,所以其实色值只剩Sc*Da,就是在交集的区域显示SRC图的色值,不相交的区域只显示DST的部分。
PorterDuff.Mode.SRC_IN
SRC_IN的公式为 [Sa * Da, Sc * Da],可以看到,只在交集的区域有颜色和透明值的显示,且显示的是SRC图的部分,这是因为在两者相交的区域,Da不为0,所以Sa*Da显示DST的透明度,Sc*Da显示SRC的色值,在相交之外的区域,要么是Sa为0,要么是Da为0,所以这些区域计算出来的结果都是[0,0]。
PorterDuff.Mode.SRC_OUT
SRC_OUT的公式为 [Sa * (1 - Da), Sc * (1 - Da)],可以看到,只在交集之外的区域有颜色和透明值的显示,且显示的是SRC图的部分,这是因为在两者相交的区域,Da为1,所以Sa*(1-Da)为0,Sc*(1 - Da)显示也为0,在相交之外的区域,只有SRC的区域Da才为0,所以这些区域计算出来的结果就是[Sa,Sc]。
PorterDuff.Mode.SRC_OVER
呈现出来的效果,如同SRC图盖住了DST图,所以称之为SRC_OVER~
PorterDuff.Mode.SRC_XOR
在SRC图像和DST图像相交的区域之外绘制它们,在相交的区域受到对应alpha和色值影响,如果完全不透明则相交处完全不绘制。
运用混合模式实现效果
裁剪任意形状图片
圆形图片View的实现方式有很多种,比如说继承ImageView在onDraw中ClipPath,混合模式也能实现这种效果,只需要将我们要加载的背景图设置为目标图像,然后绘制一个圆形形状的源图像,设置混合模式为DST_IN,那么就只会绘制出圆形范围内的背景图,从而实现圆形图片的效果,关键代码如下:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//离屏绘制
int layerID = canvas.saveLayer(0, 0, width, height, paint, Canvas.ALL_SAVE_FLAG);
canvas.drawBitmap(dstBm, 0, 0, paint);
paint.setXfermode(duffXfermode);
canvas.drawBitmap(srcBm, 0, 0, paint);
paint.setXfermode(null);
canvas.restoreToCount(layerID);
}
public Bitmap createDstBitmap(int width, int height) {
return BitmapFactory.decodeResource(getResources(), R.drawable.bg_duffmode_test);
}
public Bitmap createSrcBitmap(int width, int height) {
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Paint dstPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
dstPaint.setColor(Color.parseColor("#ec6941"));
canvas.drawCircle(width/2, height/2, height/2, dstPaint);
return bitmap;
}
效果如下:
甚至我们可以自定义裁剪形状,只要修改上面代码中创建源图像的代码如下:
public Bitmap createSrcBitmap(int width, int height) {
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Paint srcPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
srcPaint.setStyle(Paint.Style.FILL);
srcPaint.setColor(Color.parseColor("#ec6941"));
Path path = new Path();
path.moveTo(width/2, 0);
path.lineTo(width/6, height);
path.lineTo(width*5/6, height);
path.close();
canvas.drawPath(path, srcPaint);
//canvas.drawCircle(width/2, height/2, height/2, srcPaint);
return bitmap;
}
就能得到一个三角形的效果:
水波纹进度球效果
原理其实就是利用贝塞尔曲线绘制水波纹性状并且将其闭合起来,作为目标图像,然后再绘制一个圆形作为源图像,也就是用圆形来“裁剪”水波纹,利用混合模式,只显示出圆形范围内的水波纹部分,从而形成水波纹进度球效果:
甚至可以利用图标去填充水波纹的区域,代码比较多就不贴了,详见我另一篇文章 『Android自定义View实战』给我一个图标,还你一个水波纹进度球
结语
简单来讲,其实目标图像就类似于底片,源图像就类似于叠加在上面的图层,混合模式就相当于各种滤镜。混合模式虽然一共只有18种,但巧妙地利用这些模式已经足以帮助我们轻松实现一些特殊效果,本文只是列举一些利用混合模式实现的效果,如果你有更好的创意,欢迎一起讨论~ 文中Demo完整代码已上传到 一个集合酷炫效果的自定义组件库,欢迎Issue。
欢迎关注 Android小Y 的,更多Android精选自定义View
『Android自定义View实战』实现一个小清新的弹出式圆环菜单
『Android自定义View实战』玩转PathMeasure之自定义支付结果动画
『Android自定义View实战』自定义弧形旋转菜单栏——卫星菜单
『Android自定义View实战』自定义带入场动画的弧形百分比进度条
GitHub:GitHub-ZJYWidget
CSDN博客:IT_ZJYANG
简 书:Android小Y
在 GitHub 上建了一个集合炫酷自定义View的项目,里面有很多实用的自定义View源码及demo,会长期维护,欢迎Star~ 如有不足之处或建议还望指正,相互学习,相互进步,如果觉得不错动动小手点个喜欢, 谢谢~