PorterDuffXfermode误区总结

2,663 阅读6分钟

[TOC]

概述

  类android.graphics.PorterDuffXfermode继承自android.graphics.Xfermode。在用Android中的Canvas进行绘图时,可以通过使用PorterDuffXfermode将所绘制的图形的像素与Canvas中对应位置的像素按照一定规则进行混合,形成新的像素值,从而更新Canvas中最终的像素颜色值,这样会创建很多有趣的效果。当使用PorterDuffXfermode时,需要将将其作为参数传给Paint.setXfermode(Xfermode xfermode)方法,这样在用该画笔paint进行绘图时,Android就会使用传入的PorterDuffXfermode,如果不想再使用Xfermode,那么可以执行Paint.setXfermode(null)。
  上面的概述中我们提炼出两点:

  • PorterDuffXfermode的作用是将所绘制的图形的像素与Canvas中对应位置的像素进行混合,形成新的像素。这是基本原理,请谨记;
  • 如果PorterDuffXfermode不再使用,请调用Paint.setXfermode(null),关闭效果;

PorterDuffXfermode正确使用

  借用google官方demo中的图

image

很多同学在测试过程中发现和demo的实现有很大的出入,其实我们仔细看官方demo,就会发现有很大不同,我将核心代码抽取如下:

static Bitmap makeDst(int w, int h) {
        Bitmap bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
        Canvas c = new Canvas(bm);
        Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);

        p.setColor(0xFFFFCC44);
        c.drawOval(new RectF(0, 0, w * 3 / 4, h * 3 / 4), p);
        return bm;
    }

// create a bitmap with a rect, used for the "src" image
static Bitmap makeSrc(int w, int h) {
    Bitmap bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
    Canvas c = new Canvas(bm);
    Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);

    p.setColor(0xFF66AAFF);
    c.drawRect(w / 3, h / 3, w * 19 / 20, h * 19 / 20, p);
    return bm;
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    canvas.drawColor(Color.GREEN);

    int sc = canvas.saveLayer(0, 0, W, H, null, Canvas.ALL_SAVE_FLAG);

    canvas.drawBitmap(makeDst(W, H), 0, 0, mPaint);

    //mPaint.setXfermode(sModes[mIndex]);
    mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
    canvas.drawBitmap(makeSrc(W, H), 0, 0, mPaint);

    mPaint.setXfermode(null);

    canvas.restoreToCount(sc);
}

代码中有几个核心的地方我们要注意:

  • 调用saveLayer在一个新的Layer上进行图像的混合;

  这是因为在调用saveLayer时,会生成了一个全新的bitmap,这个bitmap的大小就是我们指定的保存区域的大小,新生成的bitmap是全透明的,在调用saveLayer后所有的绘图操作都是在这个bitmap上进行的。

image

没有saveLayer的绘图流程

  由于我们先把整个画布给染成了绿色,然后再画上了一个圆形,所以在应用xfermode来画源图像的时候,目标图像当前Bitmap上的所有图像了,也就是整个绿色的屏幕和一个圆形了。所以这时候源图像的相交区域是没有透明像素的,透明度全是100%,这也就不难解释结果是这样的原因了。

image

调用saveLayer的目的就是让你的目标图像独立的存在于一个layer上,而不受原始画布图像的干扰。这样才能进行目标图像和源图像的对应位置的像素混合。

  • 创建的目标图像bitmap与源图像bitmap大小是一致(其实不一致也是可以,只是计算图像坐标时更麻烦,没有必要);

注:

  1. 代码中w代表View的宽,h代表View的高;
  2. 我们将canvas的背景绘制成GREEN;

PorterDuffXfermode使用误区

测试一 使用saveLayer得到的正确图像如下

image
进行SRC_IN变换,得到正确图像:
image

测试二 不使用saveLayer

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    canvas.drawColor(Color.GREEN);

    //int sc = canvas.saveLayer(0, 0, W, H, null, Canvas.ALL_SAVE_FLAG);

    canvas.drawBitmap(makeDst(W, H), 0, 0, mPaint);

    //mPaint.setXfermode(sModes[mIndex]);
    mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
    canvas.drawBitmap(makeSrc(W, H), 0, 0, mPaint);

    mPaint.setXfermode(null);

    //canvas.restoreToCount(sc);
}

得到的图像如下:

image

PorterDuff.Mode.SRC_IN的含义是在两者相交的地方绘制源图像,因为没有使用saveLayer所以我们目标图像是绿色的背景+黄色的圆,源图像是蓝色正方形。

  1. 目标图像bitmap与源图像btmap大小是完全一样的;
  2. 源图像有图像区域小于源图像btmap大小的,而目标图像因为绘制了背景其有图像的区域与bitmap的实际大小是一样的;

那么这里有两个错误点:

  1. 我们的目的是对黄色圆与蓝色方框进行图像混合,显然上面的结果是不对的,这就是图层的重要性;
  2. 根据SRC_IN的含义得到的结果应该只保留蓝色的正方形,除了蓝色的正方形外,其它区域应该是透明颜色,显然这也不符合我们的预期;

为什么得到的是黑色图像,而不是透明颜色,其实我没弄明白明白,但是我们知道了正确使用PorterDuffXfermode的代码如下:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    .......

    int sc = canvas.saveLayer(0, 0, W, H, null, Canvas.ALL_SAVE_FLAG);
    canvas.drawBitmap(makeDst(W, H), 0, 0, mPaint);
    //绘制你的目标图像
    mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
    //绘制你的源图像
    mPaint.setXfermode(null);
    canvas.restoreToCount(sc);
    
    .......
}

测试三 你真的了解SRC_IN吗?

我们在测试一的基础上修改一段代码

static Bitmap makeDst(int w, int h) {
    Bitmap bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
    Canvas c = new Canvas(bm);
    Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);

    p.setColor(0x7fFFCC44);
    c.drawOval(new RectF(0, 0, w * 3 / 4, h * 3 / 4), p);
    return bm;
}

将目标图像中0xFFFFCC44改为0x7fFFCC44,也就透明度改为0.5

image
image

右边是没有修改透明度的图像,左边是修改后的;我们发现SRC_IN的结果发生了变化。让我们来看坎SRC_IN完整的解释:

  • 在两者相交的地方绘制源图像,并且绘制的效果会受到目标图像对应地方透明度的影响;

细节决定成败,小小的细节很有可能成为你日常工作中的大坑,更多PorterDuff.Mode含义,请参考

测试四 LAYER_TYPE_SOFTWARE使用

网上有不少说使用LAYER_TYPE_SOFTWARE的,我们也来实践一下:
初始化时添加如下代码,使用软件加速

setLayerType(LAYER_TYPE_SOFTWARE, null);

测试代码还是使用调用了saveLayer的代码,但是使用CLEAR模式

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    canvas.drawColor(Color.GREEN);

    int sc = canvas.saveLayer(0, 0, W, H, null, Canvas.ALL_SAVE_FLAG);

    canvas.drawBitmap(makeDst(W, H), 0, 0, mPaint);

    //mPaint.setXfermode(sModes[mIndex]);
    mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
    canvas.drawBitmap(makeSrc(W, H), 0, 0, mPaint);

    mPaint.setXfermode(null);

    canvas.restoreToCount(sc);
}

image
image

左边是没有设置setLayerType得到的混合图像,符合CLEAR模式的预期;右边是设置LAYER_TYPE_SOFTWARE后得到的图像,我们发现混合后全部变成透明的了,这是为什么呢?我给出这样一个猜想,在没有设置LAYER_TYPE_SOFTWARE时,只有有效图像区域参与像素混合,透明区域不参与像素混合;而设置LAYER_TYPE_SOFTWARE后透明区域也参与像素混合了。为了验证这一点,我们进行测试五。

测试五

在测试四的基础上,目标bitmap和源bitmap是一样大的,我们的测试方法是将源bitmap的大小修改为只有目标bitmap的1/4,这样一来源bitmap将只占据整个图像左上角1/4区域,得到的测试结果如下:

image

根据我们的猜想,源bitmap的整个区域(透明区域和图像区域)都参与到与目标图像的像素混合中,导致左上角1/4区域变为了透明的。

以上分析根据个人经验得出,并不透彻,如有不对,望指正。