正值猿宵佳节,小盆友在此祝你们新年无BUG。😄java
目录git
1、前言github
2、PorterDuffXfermodecanvas
3、实战微信
4、写在最后ide
自定义UI中,少不了对多种图像的叠加覆盖,而须要达到预期的目的,咱们便须要今天的主角Xfermode。Xfermode 有三个孩子,分别是:工具
而 AvoidXfermode 和 PixelXorXfermode 已经在 API 16以后被标记为removed,因此就只剩下小儿子 PorterDuffXfermode 为咱们合成图像,理所固然咱们今天的重点也就在他身上。老规矩,先上几张实战图,而后开始咱们今天的分享。post
Xfermode小工具优化
刮刮卡 动画
心跳
咱们看如下两段源码,可知 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 图。小盆友也跟着手写了一遍,须要看源码的童鞋进传送门
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这几个值。他们各自表明(结合着英文记,更容易):
而 源图像 和 目标图像 又是什么呢?记住一句话就能够,先设置的为目标图(Dst),后设置的为源图(Src)。
全部的疑惑咱们已经先点破,接下里就给出咱们比较全面的Demo,这是小盆友以官方所示的十六种模式提供的Xfermode小工具,若是有时候拿捏不许具体使用什么模式时,能够进行加入这个工具来进行琢磨。对该小工具感兴趣的请进传送门。
接下来咱们便逐个讲解模式,所使用的图片均来自 Xfermode工具 的zinc例子。
注释给出的是 [0, 0] , 透明度 为0,即彻底看不见;颜色 为0,即无色; 最终呈现以下图,什么都没有。
注释给出的是[Sa, Sc], 透明度 为Sa,即取决于源图的透明值;颜色 为Sc,即取源图的色值; 最终呈现以下图,由于都是取源图的值,因此最终就是显示 源图。
注释给出的是[Da, Dc],透明度 为Da,即取目标图的透明度;颜色 为Dc,即取目标图的色值; 最终呈现以下图,由于都取目标图的值,因此最终呈现的就是 目标图。
注释给出的是 [Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc] ,其实就是源图盖于目标图上,如有透明度,则会看到下一层,从名字也能够很好的记忆。
注释给出的是 [Sa + (1 - Sa)*Da, Rc = Dc + (1 - Da)*Sc],和 SRC_OVER相反 ,目标图盖于源图上,有透明度的地方能够看到下一层。
注释给出的是 [Sa * Da, Sc * Da]
透明度为 Sa * Da,说明 透明度取决源图和目标图的各自透明度,只有二者的透明均为1时(彻底可见),最终成像区域的透明才为彻底可见,不然会被相应弱化。
色值为 Sc * Da,说明呈现图像 色值以源图渲染。
最终呈现效果以下,成像的结果是 目标图和源图的交集 。
注释给出的是 [Sa * Da, Sa * Dc]
透明度为 Sa * Da,说明 透明度取决源图和目标图的各自透明度,只有二者的透明均为1时(彻底可见),最终成像区域的透明才为彻底可见,不然会被相应弱化。
色值为 Sa * Dc,说明呈现图像 色值以目标图渲染。
最终呈现效果以下,成像的结果是 目标图和源图的交集 。
注释给出的是 [Sa * (1 - Da), Sc * (1 - Da)]
透明度为 Sa * (1 - Da),说明 透明度取决源图和目标图的透明度,值得注意的是,目标图的透明值越大,反而最终结果越弱,即目标图透明度为1的地方,则最终图像不显示该地方。目标图透明度不为1的区域,则会对最终图进行削弱透明度。目标图透明度为0的区域,则不会影响到最终图像。
色值为 Sc * (1 - Da),说明呈现图像 色值以源图渲染。
最终呈现效果以下,成像的结果是 以源图为主,剔除与目标图交集的地方 (由于还受透明度影响)。
注释给出的是 [Da * (1 - Sa), Dc * (1 - Sa)]
透明度为 Da * (1 - Sa),说明 透明度取决源图和目标图的透明度,值得注意的是,源图的透明值越大,反而最终结果越弱,即源图透明度为1的地方,则最终图像不显示该地方。源图透明度不为1的区域,则会对最终图进行削弱透明度。源图透明度为0的区域,则不会影响到最终图像。
色值为 Dc * (1 - Sa),说明呈现图像 色值以目标图渲染。
最终呈现效果以下,成像的结果是 以目标图图为主,剔除与源图交集的地方 (由于还受透明度影响)。
注释给出的是 [Da, Sc * Da + (1 - Sa) * Dc]
透明度为 Da,说明 最终图像的可见区域只取决于目标图像。
色值 Sc * Da + (1 - Sa) * Dc,说明由 目标图和源图共同决定。
最终呈现的效果以下,成像的结果是在 目标图的区域内,源图覆盖在它上面。
注释给出的是 [Sa, Sa * Dc + Sc * (1 - Da)]
透明度为 Sa,说明 最终图像的可见区域只取决于源图像。
色值 Sa * Dc + Sc * (1 - Da),说明由 目标图和源图共同决定。
最终呈现的效果以下,成像的结果是在 源图的区域内,目标图覆盖在它上面。
注释给出的是 [Sa + Da - 2 * Sa * Da, Sc * (1 - Da) + (1 - Sa) * Dc]
透明度 Sa + Da - 2 * Sa * Da,说明 透明受源图和目标图的共同影响,当二者透明度为1时,最终此区域的透明度反而会为0。
色值 Sa * Dc + Sc * (1 - Da),说明由 目标图和源图共同决定。
最终呈现的效果以下,成像的结果为 不相交的地方,以各自的图像呈现。相交的地方受二者的透明度影响。
注释给出的是 [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),说明由 目标图和源图共同决定。
最终呈现的效果以下,成像的结果为 图像的颜色会稍微偏重些。
注释给出的是 [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),说明由 目标图和源图共同决定。
最终呈现的效果以下,成像的结果为 相交部分图像的颜色会偏亮些。
注释给出的是 [Sa * Da, Sc * Dc],最终成像以下,与 DST_IN 和 SRC_IN 有些相似,只是以灰度显示。
注释给出的是 [Sa + Da - Sa * Da, Sc + Dc - Sc * Dc],最终成像以下,会削弱相交部分的颜色,呈现出更为亮的色泽。
注释给出的是 Saturate(S + D),效果图以下
想必你们能看出,这里须要两层图,一层为“黑蜘蛛”的图,一层为灰色遮罩。根据咱们手指的滑动轨迹“擦拭掉”该地方的灰色遮罩。最后在手指抬起时,判断被“擦拭掉”的区域是否已经超出20%,若是超出,则再也不绘制遮罩,达到底层图显现的效果。
第一步,咱们经过 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();
}
}
};
复制代码
核心三步便已经在以上实现,剩余的即是组装起来,这里再也不过多赘述,完整代码请进传送门。
至于如何让dx一点点增大,咱们使用了属性动画。这个例子比较简单,咱们就再也不粘贴代码。有兴趣的童鞋请进传送门。
关于 属性动画 小盆友在另外一篇博客中有详细讲述其原理和应用,感兴趣的话,能够进传送门。
经过Xfermode的多种模式组合能够绘制出一些酷炫的图像和效果,限制咱们的永远仍是咱们的想象力和那懒惰的双手😄。最后若是你从这篇文章有所收获,请给我个赞❤️,并关注我吧。文章中若有理解错误或是晦涩难懂的语句,请评论区留言,咱们进行讨论共同进步。你的鼓励是我前进的最大动力。
高级UI系列的Github地址:请进入传送门,若是喜欢的话给我一个star吧😄
若是须要更为深刻的探讨,加我微信吧😁。