08.2 精通自定义 View 之 混合模式——PorterDuffXfermode

返回自定义 View 目录

8.2.1 PorterDuffXfermode 概述

官方文档链接
PorterDuffXfermode 的构造函数如下:

1
public PorterDuffXfermode(PorterDuff.Mode mode)

它只有一个参数 PorterDuff.Mode,表示混合模式,枚举值有 18 个,表示各种图形混合模式,每一种模式都对应着一种算法,如下图所示。

比如,LIGHTEN 的计算方式为 [Sa + Da - Sa * Da, Sc * (1 - Da) + Dc * (1 - Sa) + max(Sc, Dc)],其中 Sa 全称为 Source alpha 表示源图的 Alpha 通道;Sc 全称为 Source color 表示源图的颜色;Da 全称为 Destination alpha 表示目标图的 Alpha 通道;Dc 全称为 Destination color 表示目标图的颜色,在每个公式中,都会被分为两部分 [……,……],其中 “,” 前的部分为 “Sa + Da - Sa * Da” 这一部分的值代表计算后的 Alpha 通道;而 “,” 后的部分为 “Sc * (1 - Da) + Dc * (1 - Sa) + max(Sc, Dc)” 这一部分的值代表计算后的颜色值,图形混合后的图片就是依据这个公式来对 DST 和 SRC 两张图像中每一个像素进行计算,得到最终的结果的。

显示的是两个图形一圆一方通过一定的计算产生不同的组合效果,其中圆形是底部的目标图像,方形是上方的源图像。

在上面的公式中涉及到一个概念,目标图 DST,源图 SRC。那什么是源图,什么是目标图呢?我们简单举例子来说明一下:

首先需要自定义一个控件并进行初始化;然后禁用硬件加速;新建两张空白图片,然后在图片上分别画一个圆形 (DST) 和一个矩形 (SRC) 并填充相应的颜色,图形以外的位置都是空白像素;最后在离屏绘制部分,现在 (0, 0) 位置把圆形图像画出来,然后设置 PorterDuffXfermode 的模式为 Mode.SRC_IN,之后再以圆形中心为左上角点画出矩形,清空 Xfermode。

在 Xfermode 设置前画出的图像叫做目标图像,即给谁应用 Xfermode;在 Xfermode 设置后画出的图像叫做源图像,即拿什么应用 Xfermode。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class TestView extends View {
private Paint mPaint;
private Bitmap dstBmp;
private Bitmap srcBmp;
private int width = 200;
private int height = 200;
private PorterDuffXfermode mMode;
public TestView(Context context, AttributeSet attrs) {
super(context, attrs);
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
mPaint = new Paint();
dstBmp = makeBitmap(width, height, 0xFFFFCC44, "oval");
srcBmp = makeBitmap(width, height, 0xFF66AAFF, "rect");
mMode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
}
private Bitmap makeBitmap(int w, int h, int color, String type) {
Bitmap bmp = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bmp);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(color);
if ("oval".equals(type)) {
canvas.drawOval(new RectF(0, 0, w, h), paint);
} else {
canvas.drawRect(0, 0, w, h, paint);
}
return bmp;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.translate(100, 100);
int layerId = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
canvas.drawBitmap(dstBmp, 0, 0, mPaint);
mPaint.setXfermode(mMode);
canvas.drawBitmap(srcBmp, width / 2f, height / 2f, mPaint);
mPaint.setXfermode(null);
canvas.restoreToCount(layerId);
}
}

该示例的效果如下图所示。

对于 Mode.SRC_IN,它的计算公式为 [Sa * Da, Sc * Da]。在这个公式中,结果值的透明度和颜色值都是由 Sa、Sc 分别乘以目标图像的 Da 来计算的。当目标图像为空白像素时,计算结果也将为空白像素;当目标图像不透明时,相交区域将显示源图像像素。所以,从效果图中可以看出,两图像相交部分显示的是源图像;对于不相交的部分,此时目标图像的透明度是 0,源图像不显示。

8.2.2 颜色叠加相关模式

这部分涉及到的几个模式有 Mode.ADD(饱和度相加)、Mode.LIGHTEN(变亮)、Mode.DARKEN(变暗)、Mode.MULTIPLY(正片叠底)、Mode.OVERLAY(叠加),Mode.SCREEN(滤色)。

1. Mode.ADD(饱和度相加)

它的公式是 Saturate(S + D)。ADD 模式简单来说就是对 SRC 与 DST 两张图片相交区域的饱和度进行相加。使用 8.2.1 节中的例子,将 PorterDuff.Mode.SRC_IN 改为 PorterDuff.Mode.ADD,效果如下图所示。

从效果图中可以看出,只有源图与目标图像相交的部分的图像的饱和度产生了变化,没相交的部分是没有变的,因为对方的饱和度是 0,当然不相交的位置饱和度是不会变的。这个模式的应用范围比较少,暂时想不到哪里会用到。

2. Mode.LIGHTEN(变亮)

它的算法是: [Sa + Da - Sa * Da, Sc * (1 - Da) + Dc * (1 - Sa) + max(Sc, Dc)]。

这个效果比较容易理解,两个图像重合的区域才会有颜色值变化,所以只有重合区域才有变亮的效果,源图像非重合的区域,由于对应区域的目标图像是空白像素,所以直接显示源图像。

在实际应用中,会出现这种情况:当选中一本书时,给这本书加上灯光效果,如下图所示。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class TestView extends View {
private Paint mPaint;
private Bitmap bmpDST, bmpSRC;
private PorterDuffXfermode mMode;
public TestView(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint();
bmpDST = BitmapFactory.decodeResource(getResources(),R.drawable.book_bg,null);
bmpSRC = BitmapFactory.decodeResource(getResources(),R.drawable.book_light,null);
mMode = new PorterDuffXfermode(PorterDuff.Mode.LIGHTEN);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int layerId = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
// 先画书架,做为目标图像
canvas.drawBitmap(bmpDST,0,0,mPaint);
mPaint.setXfermode(mMode);
// 再图光点
canvas.drawBitmap(bmpSRC,0,0,mPaint);
mPaint.setXfermode(null);
canvas.restoreToCount(layerId);
}
}

3. Mode.DARKEN(变暗)

对应公式是: [Sa + Da - Sa * Da, Sc * (1 - Da) + Dc * (1 - Sa) + max(Sc, Dc)]。

4. Mode.MULTIPLY(正片叠底)

公式是:[Sa * Da, Sc * Dc]。

从公式中可以看出,计算 Alpha 值时的公式是 Sa * Da,是用源图像的 Alpha 值乘以目标图像的 Alpha 值。由于源图像的非相交区域所对应的目标图像像素的 Alpha 是 0,所以结果像素的 Alpha 值仍是 0,源图像的非相交区域在计算后是透明的。

5. Mode.OVERLAY(叠加)

Google 没有给出这种模式的算法,效果如下图所示。

6. Mode.SCREEN(滤色)

对应公式是:[Sa + Da - Sa * Da, Sc + Dc - Sc * Dc]。

到这里,这六种混合模式就讲完了,下面总结一下:

  • 这几种模式都是 PhotoShop 中存在的模式,是通过计算改变交合区域的颜色值的。
  • 除了 Mode.MULTIPLY(正片叠底)会在目标图像透明时将结果对应区域置为透明,其它图像都不受目标图像透明像素影响,即源图像非交合部分保持原样。

7. 示例:Twitter 标识的描边效果

在图一中,小鸟整个都是蓝色的。在图二中,只有小鸟的边缘部分是白色的,中间部分是透明的。在最终的合成图中:图一和图二中小鸟与边缘的是显示的,而且还有某种效果,但小鸟中间的区域变透明了,显示的是底部 Activity 的背景色。

前面学到的几种样式中,只有 Mode.MULTIPLY(正片叠底)会在两个图像的一方透明时,结果像素就是透明的。所以这里使用的模式就是 Mode.MULTIPLY 模式。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class TestView extends View {
private Paint mPaint;
private Bitmap bmpDST, bmpSRC;
private PorterDuffXfermode mMode;
public TestView(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint();
bmpDST = BitmapFactory.decodeResource(getResources(),R.drawable.twiter_bg,null);
bmpSRC = BitmapFactory.decodeResource(getResources(),R.drawable.twiter_light,null);
mMode = new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int layerId = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
canvas.drawBitmap(bmpDST,0,0,mPaint);
mPaint.setXfermode(mMode);
canvas.drawBitmap(bmpSRC,0,0,mPaint);
mPaint.setXfermode(null);
canvas.restoreToCount(layerId);
}
}