08.3 精通自定义 View 之 混合模式——PorterDuffXfermode 之源图像模式

返回自定义 View 目录

除 Photoshop 中存在的几个模式以外,还有几种是在处理结果时以源图像显示为主的几个模式,所以大家在遇到图像相交,需要显示源图像的情况时,就需要从这几种模式中考虑了,主要有 Mode.SRC、Mode.SRC_IN、Mode.SRC_OUT、Mode.SRC_OVER、Mode.SRC_ATOP。

8.3.1 Mode.SRC

计算公式为:[Sa, Sc]。
从公式中也可以看出,在处理源图像所在区域的相交问题时,全部以源图像显示。示例图像如下图所示。

8.3.2 Mode.SRC_IN

1. 概述

计算公式为:[Sa * Da, Sc * Da]。
在这个公式中结果值的透明度和颜色值都是由 Sa、Sc 分别乘以目标图像的 Da 来计算的。所以当目标图像为空白像素时,计算结果也将会为空白像素。示例图像如下图所示。

大家注意 SRC_IN 模式与 SRC 模式的区别。一般而言,是在相交区域时无论 SRC_IN 还是 SRC 模式都是显示源图像,而唯一不同的是,当目标图像是空白像素时,在 SRC_IN 所对应的区域也将会变成空白像素。

其实更严格的来讲,SRC_IN 模式是在相交时利用目标图像的透明度来改变源图像的透明度和饱和度。当目标图像透明度为 0 时,源图像就完全不显示。

利用这个特性,我们能完成很多功能,比如圆角效果和图片倒影。

圆角效果的生成非常简单,依然使用两张图片合成,如下图所示。

小狗图像是源图像,目标图像是一张遮罩图,可以看到这张遮罩图的 4 个角都是圆形切角,而且是透明的。这里我们就需要使用 SRC_IN 模式的特性:当目标图像与源图像相交时,根据目标图像的透明度来决定显示源图像的哪部分。

代码如下:

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.dog_shade,null);
bmpSRC = BitmapFactory.decodeResource(getResources(),R.drawable.dog,null);
mMode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
}
@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);
}
}

2. 图片倒影效果

SRC_IN 模式是在相交时利用目标图像的透明度来改变源图像的透明度和饱和度。所以当目标图像的透明度在 0~255 之间时,就会把源图像的透明度和颜色值都会变小。利用这个特性,可以做出倒影效果,如下图所示。

很明显,由于 SRC_IN 模式的特性是根据目标图像的透明度来决定如何显示源图像,而我们要显示的是小狗图像,所以,源图像是小狗图像,目标图像是一张遮罩图,它是一个从上到下的白色填充渐变,白色的透明度从 49% 到 0。

小效果图中,我们先画出小狗图像,然后将画布下移,最后将源图像与目标图像再次合成,画出倒影即可。代码如下:

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
public class TestView extends View {
private Paint mBitPaint;
private Bitmap bmpDST, bmpSRC, bmpRevert;
private PorterDuffXfermode mode;
public TestView(Context context, AttributeSet attrs) {
super(context, attrs);
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
mBitPaint = new Paint();
mode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
bmpDST = BitmapFactory.decodeResource(getResources(),R.drawable.dog_invert_shade,null);
bmpSRC = BitmapFactory.decodeResource(getResources(),R.drawable.dog,null);
Matrix matrix = new Matrix();
matrix.setScale(1F, -1F);
// 生成倒影图
bmpRevert = Bitmap.createBitmap(bmpSRC, 0, 0, bmpSRC.getWidth(), bmpSRC.getHeight(), matrix, true);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 先画出小狗图片
canvas.drawBitmap(bmpSRC,0,0,mBitPaint);
// 再画出倒影
int layerId = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
canvas.translate(0, bmpSRC.getHeight());
canvas.drawBitmap(bmpDST,0,0,mBitPaint);
mBitPaint.setXfermode(mode);
canvas.drawBitmap(bmpRevert,0,0,mBitPaint);
mBitPaint.setXfermode(null);
canvas.restoreToCount(layerId);
}
}

8.3.3 Mode.SRC_OUT

1. 概述

计算公式为:[Sa * (1 - Da), Sc * (1 - Da)]。
示例图像如下图所示。

从公式中可以看出,计算结果的透明度为 Sa * (1 - Da);也就是说当目标图像图像完全不透明时,计算结果将是透明的。

从示例图中也可以看出,源图像与目标图像的相交部分由于目标图像的不透明度为 100%,所以相交部分的计算结果为空白像素。在目标图像为空白像素时,完全以源图像显示。

所以这个模式的特性可以概括为:以目标图像的透明度的补值来调节源图像的透明度和色彩饱和度。即当目标图像为空白像素时,就完全显示源图像,当目标图像的不透明度为 100%时,交合区域为空像素。简单来说,当目标图像有图像时结果显示空白像素,当目标图像没有图像时,结果显示源图像。

2. 橡皮擦效果

利用 SRC_OUT 模式的特性,可以实现橡皮擦效果,如下图所示。

原理:对于 Mode.SRC_OUT 模式,当目标图像有图像时计算结果为空白像素;当目标图像没有图像时,显示源图像。所以我们把手指轨迹做为目标图像,在与源图像计算时,有手指轨迹的地方就变为空白像素了,看起来的效果就是被擦除了。

代码如下:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public class TestView extends View {
private Paint mPaint;
private Bitmap mDstBmp;
private Bitmap mSrcBmp;
private Path mPath;
private float mPreX, mPreY;
private PorterDuffXfermode mMode;
private Canvas mTempCanvas;
public TestView(Context context, AttributeSet attrs) {
super(context, attrs);
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(100);
mPaint.setStrokeCap(Paint.Cap.ROUND);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2;
mSrcBmp = BitmapFactory.decodeResource(getResources(), R.drawable.meinv, options);
mDstBmp = Bitmap.createBitmap(mSrcBmp.getWidth(), mSrcBmp.getHeight(), Bitmap.Config.ARGB_8888);
mPath = new Path();
mTempCanvas = new Canvas();
mMode = new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mPath.moveTo(event.getX(), event.getY());
mPreX = event.getX();
mPreY = event.getY();
return true;
case MotionEvent.ACTION_MOVE:
float endX = (mPreX + event.getX()) / 2;
float endY = (mPreY + event.getY()) / 2;
mPath.quadTo(mPreX, mPreY, endX, endY);
mPreX = event.getX();
mPreY = event.getY();
break;
case MotionEvent.ACTION_UP:
break;
}
postInvalidate();
return super.onTouchEvent(event);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int layerId = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
// 先把手势轨迹画到目标图像上
mTempCanvas.setBitmap(mDstBmp);
mTempCanvas.drawPath(mPath, mPaint);
// 然后把目标图像画到画布上
canvas.drawBitmap(mDstBmp, 0 , 0, mPaint);
// 计算源图像区域
mPaint.setXfermode(mMode);
canvas.drawBitmap(mSrcBmp, 0, 0, mPaint);
mPaint.setXfermode(null);
canvas.restoreToCount(layerId);
}
}

3. 刮刮卡效果

需要准备两张图片,一张是刮奖遮罩层图片(scratch_over.png),一张是中奖图片图片(scratch.png),如下图所示。

scratch_over.png

scratch.png

示例代码如下:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
public class TestView extends View {
private Paint mPaint;
private Bitmap mDstBmp;
private Bitmap mSrcBmp;
private Bitmap mTextBmp;
private Path mPath;
private float mPreX, mPreY;
private PorterDuffXfermode mMode;
private Canvas mTempCanvas;
private RectF mRect;
public TestView(Context context, AttributeSet attrs) {
super(context, attrs);
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(100);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mTextBmp = BitmapFactory.decodeResource(getResources(), R.drawable.scratch);
mSrcBmp = BitmapFactory.decodeResource(getResources(), R.drawable.scratch_over);
mDstBmp = Bitmap.createBitmap(mSrcBmp.getWidth(), mSrcBmp.getHeight(), Bitmap.Config.ARGB_8888);
mPath = new Path();
mTempCanvas = new Canvas();
mMode = new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT);
mRect = new RectF();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mPath.moveTo(event.getX(), event.getY());
mPreX = event.getX();
mPreY = event.getY();
return true;
case MotionEvent.ACTION_MOVE:
float endX = (mPreX + event.getX()) / 2;
float endY = (mPreY + event.getY()) / 2;
mPath.quadTo(mPreX, mPreY, endX, endY);
mPreX = event.getX();
mPreY = event.getY();
break;
case MotionEvent.ACTION_UP:
break;
}
postInvalidate();
return super.onTouchEvent(event);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float width = mTextBmp.getWidth();
float height = mTextBmp.getHeight();
float screenWidth = getWidth();
if (width > screenWidth) {
height = height * screenWidth / width;
width = screenWidth;
}
mRect.set(0, 0, width, height);
// 先画底层奖励文案图片
canvas.drawBitmap(mTextBmp, null, mRect, mPaint);
int layerId = canvas.saveLayer(140, 70, width - 140, height - 70,
null, Canvas.ALL_SAVE_FLAG);
// 把手势轨迹画到目标图像上
mTempCanvas.setBitmap(mDstBmp);
mTempCanvas.drawPath(mPath, mPaint);
// 然后把目标图像画到画布上
canvas.drawBitmap(mDstBmp, 0, 0, mPaint);
// 计算源图像区域
mPaint.setXfermode(mMode);
mRect.set(140, 70, width-140, height-70);
canvas.drawBitmap(mSrcBmp, null, mRect, mPaint);
mPaint.setXfermode(null);
canvas.restoreToCount(layerId);
}
}

8.3.4 Mode.SRC_OVER

计算公式为:[Sa + (1 - Sa) * Da, Rc = Sc + (1 - Sa) * Dc]。
示例图像为:

在计算结果中,源图像没有改变。它的意思就是,在目标图像的顶部绘制源图像。从公式中也可以看出,目标图像的透明度为 Sa + (1 - Sa) * Da;即在源图像的透明度基础上增加一部分目标图像的透明度。增加的透明度是源图像透明度的补量;目标图像的色彩值的计算方式同理,所以当源图像透明度为 100% 时,就原样显示源图像。

8.3.5 Mode.SRC_ATOP

计算公式为:[Da, Sc * Da + (1 - Sa) * Dc]。

很奇怪,它的效果图竟然与 SRC_IN 模式是相同的,我们来对比一下它们的公式:

SRC_IN:[Sa * Da, Sc * Da]
SRC_ATOP:[Da, Sc * Da + (1 - Sa) * Dc]

先看透明度:在 SRC_IN 中是 Sa * Da,在 SRC_ATOP 是 Da。
SRC_IN 是源图像透明度乘以目标图像的透明度做为结果透明度,而SRC_ATOP 是直接使用目标图像的透明度做为结果透明度。

再看颜色值:SRC_IN 的颜色值为 Sc * Da,SRC_ATOP 的颜色值为Sc * Da + (1 - Sa) * Dc,SRC_ATOP 在 SRC_IN 的基础上还增加了(1 - Sa) * Dc。

所以,结论为:
1)当透明度是 100% 和 0 时,SRC_ATOP 和 SRC_IN 模式是通用的。
2)当透明度不是 100% 和 0 时,SRC_ATOP 相比 SRC_IN 源图像的饱和度会增加,即会显得更亮。