阅读 2624

图像操纵大师Xfermode讲解与实战——Android高级UI

正值猿宵佳节,小盆友在此祝大家新年无BUG。😄

目录

一、前言

二、PorterDuffXfermode

三、实战

四、写在最后

一、前言

自定义UI中,少不了对多种图像的叠加覆盖,而需要达到预期的目的,我们便需要今天的主角XfermodeXfermode 有三个孩子,分别是:

  1. AvoidXfermode
  2. PixelXorXfermode
  3. PorterDuffXfermode

而 AvoidXfermode 和 PixelXorXfermode 已经在 API 16之后被标记为removed,所以就只剩下小儿子 PorterDuffXfermode 为我们合成图像,理所当然我们今天的重点也就在他身上。老规矩,先上几张实战图,然后开始我们今天的分享。

Xfermode小工具

刮刮卡

心跳

二、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),其值决定了这张合成图的颜色值。

聪明的童鞋还会注意到 SaDaScDc这几个值。他们各自代表(结合着英文记,更容易):

  • 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_INSRC_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的多种模式组合可以绘制出一些酷炫的图像和效果,限制我们的永远还是我们的想象力和那懒惰的双手😄。最后如果你从这篇文章有所收获,请给我个赞❤️,并关注我吧。文章中如有理解错误或是晦涩难懂的语句,请评论区留言,我们进行讨论共同进步。你的鼓励是我前进的最大动力。

高级UI系列的Github地址:请进入传送门,如果喜欢的话给我一个star吧😄

如果需要更为深入的探讨,加我微信吧😁。

关注下面的标签,发现更多相似文章
评论