图像操纵大师Xfermode讲解与实战——Android高级UI
正值猿宵佳节,小盆友在此祝大家新年无BUG。😄
目录
一、前言
二、PorterDuffXfermode
三、实战
四、写在最后
一、前言
自定义UI中,少不了对多种图像的叠加覆盖,而需要达到预期的目的,我们便需要今天的主角Xfermode。Xfermode 有三个孩子,分别是:
- AvoidXfermode
- PixelXorXfermode
- PorterDuffXfermode
而 AvoidXfermode 和 PixelXorXfermode 已经在 API 16之后被标记为removed,所以就只剩下小儿子 PorterDuffXfermode 为我们合成图像,理所当然我们今天的重点也就在他身上。老规矩,先上几张实战图,然后开始我们今天的分享。
刮刮卡
心跳
二、PorterDuffXfermode
我们看以下两段源码,可知 PorterDuffXfermode 作用时通过 Paint的setXfermode 设置,而 PorterDuffXfermode 的实例化其实还需要一个参数,类型为 PorterDuff.Mode。
// Paint 类
public Xfermode setXfermode(Xfermode xfermode) {
int newMode = xfermode != null ? xfermode.porterDuffMode : Xfermode.DEFAULT;
int curMode = mXfermode != null ? mXfermode.porterDuffMode : Xfermode.DEFAULT;
if (newMode != curMode) {
nSetXfermode(mNativePaint, newMode);
}
mXfermode = xfermode;
return xfermode;
}
// PorterDuffXfermode 类
public class PorterDuffXfermode extends Xfermode {
public PorterDuffXfermode(PorterDuff.Mode mode) {
porterDuffMode = mode.nativeInt;
}
}
所以经过上面得知,最终起作用的是 PorterDuff.Mode。进入源码,会看到以下可用的模式,这段代码是API 22 的片段,如果你在比较高的版本看的话会有些许不同,但相同模式的计算公式一样。
public enum Mode {
/** [0, 0] */
CLEAR (0),
/** [Sa, Sc] */
SRC (1),
/** [Da, Dc] */
DST (2),
/** [Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc] */
SRC_OVER (3),
/** [Sa + (1 - Sa)*Da, Rc = Dc + (1 - Da)*Sc] */
DST_OVER (4),
/** [Sa * Da, Sc * Da] */
SRC_IN (5),
/** [Sa * Da, Sa * Dc] */
DST_IN (6),
/** [Sa * (1 - Da), Sc * (1 - Da)] */
SRC_OUT (7),
/** [Da * (1 - Sa), Dc * (1 - Sa)] */
DST_OUT (8),
/** [Da, Sc * Da + (1 - Sa) * Dc] */
SRC_ATOP (9),
/** [Sa, Sa * Dc + Sc * (1 - Da)] */
DST_ATOP (10),
/** [Sa + Da - 2 * Sa * Da, Sc * (1 - Da) + (1 - Sa) * Dc] */
XOR (11),
/** [Sa + Da - Sa*Da,
Sc*(1 - Da) + Dc*(1 - Sa) + min(Sc, Dc)] */
DARKEN (12),
/** [Sa + Da - Sa*Da,
Sc*(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)] */
LIGHTEN (13),
/** [Sa * Da, Sc * Dc] */
MULTIPLY (14),
/** [Sa + Da - Sa * Da, Sc + Dc - Sc * Dc] */
SCREEN (15),
/** Saturate(S + D) */
ADD (16),
OVERLAY (17);
Mode(int nativeInt) {
this.nativeInt = nativeInt;
}
/**
* @hide
*/
public final int nativeInt;
}
每个模式的效果是怎样的呢? 我们先看看官方给出的 Demo 图。小盆友也跟着手写了一遍,需要看源码的童鞋进传送门
但是,这个 demo 少了一样东西,那就是透明度,不能全面的体现出Xfermode的威力。所以我们需要先说明下参数的意思,然后给出我们较为全面的demo。
PorterDuff.Mode 源码中每个模式的组成都是 [xx, yy] 形式,我们拿 SRC_OUT 来举例。
/** [Sa * (1 - Da), Sc * (1 - Da)] */
SRC_OUT (7),
"xx" 指的就是 Sa * (1 - Da),其值决定了这张合成图的透明度。而透明度的取值范围为 [0, 1]。0代表着完全透明,而1代表完全可见。
“yy” 指的就是 Sc * (1 - Da),其值决定了这张合成图的颜色值。
聪明的童鞋还会注意到 Sa、Da、Sc、Dc这几个值。他们各自代表(结合着英文记,更容易):
- Sa(Source Alpha):源图像的透明值;
- Da(Destination Alpha):目标图像的透明值;
- Sc(Source Color):源图像的色值;
- Dc(Destination Color):目标图像的色值;
而 源图像 和 目标图像 又是什么呢?记住一句话就可以,先设置的为目标图(Dst),后设置的为源图(Src)。
所有的疑惑我们已经先点破,接下里就给出我们比较全面的Demo,这是小盆友以官方所示的十六种模式提供的Xfermode小工具,如果有时候拿捏不准具体使用什么模式时,可以进行加入这个工具来进行琢磨。对该小工具感兴趣的请进传送门。
接下来我们便逐个讲解模式,所使用的图片均来自 Xfermode工具 的zinc例子。
1、CLEAR
注释给出的是 [0, 0] , 透明度 为0,即完全看不见;颜色 为0,即无色;
最终呈现如下图,什么都没有。
2、SRC
注释给出的是[Sa, Sc], 透明度 为Sa,即取决于源图的透明值;颜色 为Sc,即取源图的色值;
最终呈现如下图,因为都是取源图的值,所以最终就是显示 源图。
3、DST
注释给出的是[Da, Dc],透明度 为Da,即取目标图的透明度;颜色 为Dc,即取目标图的色值;
最终呈现如下图,因为都取目标图的值,所以最终呈现的就是 目标图。
4、SRC_OVER
注释给出的是 [Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc] ,其实就是源图盖于目标图上,若有透明度,则会看到下一层,从名字也可以很好的记忆。
5、DST_OVER
注释给出的是 [Sa + (1 - Sa)*Da, Rc = Dc + (1 - Da)*Sc],和 SRC_OVER相反 ,目标图盖于源图上,有透明度的地方可以看到下一层。
6、SRC_IN
注释给出的是 [Sa * Da, Sc * Da]
透明度为 Sa * Da,说明 透明度取决源图和目标图的各自透明度,只有两者的透明均为1时(完全可见),最终成像区域的透明才为完全可见,否则会被相应弱化。
色值为 Sc * Da,说明呈现图像 色值以源图渲染。
最终呈现效果如下,成像的结果是 目标图和源图的交集 。
7、DST_IN
注释给出的是 [Sa * Da, Sa * Dc]
透明度为 Sa * Da,说明 透明度取决源图和目标图的各自透明度,只有两者的透明均为1时(完全可见),最终成像区域的透明才为完全可见,否则会被相应弱化。
色值为 Sa * Dc,说明呈现图像 色值以目标图渲染。
最终呈现效果如下,成像的结果是 目标图和源图的交集 。
8、SRC_OUT
注释给出的是 [Sa * (1 - Da), Sc * (1 - Da)]
透明度为 Sa * (1 - Da),说明 透明度取决源图和目标图的透明度,值得注意的是,目标图的透明值越大,反而最终结果越弱,即目标图透明度为1的地方,则最终图像不显示该地方。目标图透明度不为1的区域,则会对最终图进行削弱透明度。目标图透明度为0的区域,则不会影响到最终图像。
色值为 Sc * (1 - Da),说明呈现图像 色值以源图渲染。
最终呈现效果如下,成像的结果是 以源图为主,剔除与目标图交集的地方 (因为还受透明度影响)。
9、DST_OUT
注释给出的是 [Da * (1 - Sa), Dc * (1 - Sa)]
透明度为 Da * (1 - Sa),说明 透明度取决源图和目标图的透明度,值得注意的是,源图的透明值越大,反而最终结果越弱,即源图透明度为1的地方,则最终图像不显示该地方。源图透明度不为1的区域,则会对最终图进行削弱透明度。源图透明度为0的区域,则不会影响到最终图像。
色值为 Dc * (1 - Sa),说明呈现图像 色值以目标图渲染。
最终呈现效果如下,成像的结果是 以目标图图为主,剔除与源图交集的地方 (因为还受透明度影响)。
10、SRC_ATOP
注释给出的是 [Da, Sc * Da + (1 - Sa) * Dc]
透明度为 Da,说明 最终图像的可见区域只取决于目标图像。
色值 Sc * Da + (1 - Sa) * Dc,说明由 目标图和源图共同决定。
最终呈现的效果如下,成像的结果是在 目标图的区域内,源图覆盖在它上面。
11、DST_ATOP
注释给出的是 [Sa, Sa * Dc + Sc * (1 - Da)]
透明度为 Sa,说明 最终图像的可见区域只取决于源图像。
色值 Sa * Dc + Sc * (1 - Da),说明由 目标图和源图共同决定。
最终呈现的效果如下,成像的结果是在 源图的区域内,目标图覆盖在它上面。
12、XOR
注释给出的是 [Sa + Da - 2 * Sa * Da, Sc * (1 - Da) + (1 - Sa) * Dc]
透明度 Sa + Da - 2 * Sa * Da,说明 透明受源图和目标图的共同影响,当两者透明度为1时,最终此区域的透明度反而会为0。
色值 Sa * Dc + Sc * (1 - Da),说明由 目标图和源图共同决定。
最终呈现的效果如下,成像的结果为 不相交的地方,以各自的图像呈现。相交的地方受两者的透明度影响。
13、DARKEN
注释给出的是 [Sa + Da - Sa*Da, Sc*(1 - Da) + Dc*(1 - Sa) + min(Sc, Dc)]
透明度为 Sa + Da - Sa*Da,从公式可以知道 透明度受源图和目标图的共同影响,并且最终的透明度数值会大些或是保持原值。
色值 Sc*(1 - Da) + Dc*(1 - Sa) + min(Sc, Dc),说明由 目标图和源图共同决定。
最终呈现的效果如下,成像的结果为 图像的颜色会稍微偏重些。
14、LIGHTEN
注释给出的是 [Sa + Da - Sa*Da, Sc*(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)]
透明度为 Sa + Da - Sa*Da,从公式可以知道 透明度受源图和目标图的共同影响,并且最终的透明度数值会大些或是保持原值。
色值 Sc*(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc),说明由 目标图和源图共同决定。
最终呈现的效果如下,成像的结果为 相交部分图像的颜色会偏亮些。
15、MULTIPLY
注释给出的是 [Sa * Da, Sc * Dc],最终成像如下,与 DST_IN 和 SRC_IN 有些类似,只是以灰度显示。
16、SCREEN
注释给出的是 [Sa + Da - Sa * Da, Sc + Dc - Sc * Dc],最终成像如下,会削弱相交部分的颜色,呈现出更为亮的色泽。
17、ADD
注释给出的是 Saturate(S + D),效果图如下
18、OVERLAY
三、实战
1、刮刮卡
(1)效果图
(2)效果分析
想必大家能看出,这里需要两层图,一层为“黑蜘蛛”的图,一层为灰色遮罩。根据我们手指的滑动轨迹“擦拭掉”该地方的灰色遮罩。最后在手指抬起时,判断被“擦拭掉”的区域是否已经超出20%,如果超出,则不再绘制遮罩,达到底层图显现的效果。
(3)具体实现
第一步,我们通过 onTouchEvent 实现记录手指滑动的轨迹。 但值得注意的是,这里做了一个小优化,使用了贝塞尔曲线,使滑动轨迹会更加的顺滑,具体代码如下
对 “贝塞尔曲线” 感兴趣的童鞋,可以查看小盆友的另一片文章 自带美感的贝塞尔曲线原理与实战。
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mPreX = event.getX();
mPreY = event.getY();
mPath.moveTo(mPreX, mPreY);
break;
case MotionEvent.ACTION_MOVE:
float endX = (mPreX + event.getX()) / 2;
float endY = (mPreY + event.getY()) / 2;
// 此处使用贝塞尔曲线
mPath.quadTo(mPreX, mPreY, endX, endY);
mPreX = endX;
mPreY = endY;
break;
case MotionEvent.ACTION_UP:
post(calculatePixelsRunnable);
break;
}
postInvalidate();
return true;
}
第二步,我们需要将获取到的轨迹作用于 灰色涂层 上,达到“刮卡”效果。这里其实可以使用的模式不止一个,主要看设置的 灰色涂层 和 手指路径 的先后顺序。我们使用的为 DST_OUT。
这里值得注意的是,需要开辟一个新的图层, 以免模式效果作用到其他的图像上。具体代码如下
// 开辟新的一个图层
int layer = canvas.saveLayer(0, 0, getWidth(), getHeight(), mPaint, Canvas.ALL_SAVE_FLAG);
canvas.drawBitmap(mCoatingLayerBitmap, 0, 0, mPaint);
mPaint.setXfermode(mXfermode);
canvas.drawPath(mPath, mPaint);
mCanvas.drawPath(mPath, mPaint);
mPaint.setXfermode(null);
canvas.restoreToCount(layer);
经过这两步,效果就已经达到,因为我们继承的是 ImageView ,所以 “黑蜘蛛” 图层的放入便已经实现。
第三步,自动去除 “灰色图层” 的操作,在每次手指抬起时,就会开启一个线程来计算 “灰色图层” 的像素色值,如果超过20%被擦拭,则说明可以去除该 “灰色图层”。具体代码如下:
private Runnable calculatePixelsRunnable = new Runnable() {
@Override
public void run() {
int width = getWidth();
int height = getHeight();
float totalPixel = width * height;
int[] pixel = new int[width * height];
mCoatingLayerBitmap.getPixels(pixel, 0, width, 0, 0, width, height);
int cleanPixel = 0;
for (int col = 0; col < height; ++col) {
for (int row = 0; row < width; ++row) {
if (pixel[col * width + row] == 0) {
cleanPixel++;
}
}
}
float result = cleanPixel / totalPixel;
if (result >= PERCENT) {
isShowAll = true;
postInvalidate();
}
}
};
核心三步便已经在以上实现,剩余的便是组装起来,这里不再过多赘述,完整代码请进传送门。
2、心跳
(1)效果图
(2)动画分析
我们借助以上小盆友手绘的一张图来讲解,绿色的心跳作为目标图,蓝色的作为源图,通过不断的增大dx的距离,从而让蓝色的源图宽度不断缩小,最终使用 DST_IN 模式合成就可以达到一点点出现的效果。
至于如何让dx一点点增大,我们使用了属性动画。这个例子比较简单,我们就不再粘贴代码。有兴趣的童鞋请进传送门。
关于 属性动画 小盆友在另一篇博客中有详细讲述其原理和应用,感兴趣的话,可以进传送门。
四、写在最后
通过Xfermode的多种模式组合可以绘制出一些酷炫的图像和效果,限制我们的永远还是我们的想象力和那懒惰的双手😄。最后如果你从这篇文章有所收获,请给我个赞❤️,并关注我吧。文章中如有理解错误或是晦涩难懂的语句,请评论区留言,我们进行讨论共同进步。你的鼓励是我前进的最大动力。