setXfermode
Paint#setXfermode()接口是在绘制时设置画笔的图形混合模式的,下面是官网的介绍:
public Xfermode setXfermode (Xfermode xfermode)
Set or clear the transfer mode object. A transfer mode defines how source pixels (generate by a drawing command) are composited with the destination pixels (content of the render target).
Pass null to clear any previous transfer mode. As a convenience, the parameter passed is also returned.
PorterDuffXfermode is the most common transfer mode.
| Parameters | |
|---|---|
xfermode | Xfermode: May be null. The xfermode to be installed in the paint   | 
翻译一下,setXfermode()函数就是用来为画笔设置Xfermode对象,Xfermode将决定绘制时源图形和目标图形的混合模式。其中源图形是指你将要绘制的内容,目标图形是指View中已有的内容,最终根据Xfermode中的模式将源图形和目标图形混合后的图形展示出来。
目前Xfermode的子类只有一个 PorterDuffXfermode ,所以只需要关注这一个类就行了,看官网:
Specialized implementation of Paint's transfer mode. Refer to the documentation of the PorterDuff.Mode enum for more information on the available alpha compositing and blending modes.
 Public constructors | |
|---|---|
PorterDuffXfermode(PorterDuff.Mode mode) Create an xfermode that uses the specified porter-duff mode.  | |
构建PorterDuffXfermode需要传入一个枚举类 PorterDuff.Mode ,该类一共有18个枚举值,每一个枚举值都代表一种图像混合模式,“正片叠底”就是其中一种混合模式,关于每种混合模式更详细的内容可以看谷歌官网的介绍: PorterDuff.Mode | Android Developers
现在主要看 正片叠底模式:
public static final PorterDuff.Mode MULTIPLY Multiplies the source and destination pixels.
对应的图像合成计算公式是:[Sa * Da, Sc * Dc],其中 Sa Da 分别代表源图像和目标图像透明度,Sc Dc 分别代表源图像和目标图像的颜色值,通过正片叠底模式混合后的图像效果一般比混合前的图像颜色较深,并且任何颜色和黑色正片叠底得到的仍然是黑色,任何颜色和白色进行正片叠底得到的颜色依然是原来的颜色保持不变,听起来好像是两个很高大上的结论,在谷歌官网也只给了一个公式,具体怎么计算并没有详细介绍,接下来通过一个例子实际操作一下就清楚了。
写代码前首先还需要关注两个点:硬件加速和离屏绘制
如果:1.你的应用跑在API14的版本以后 2.你又要用到那些不支持硬件加速的函数,这个时候就需要禁用硬件加速。关于硬件加速更详细的内容,可以看下这篇文章:自定义控件三部曲之绘图篇(十)——Paint之setXfermode(一)_启舰-CSDN博客
离屏绘制先了解一种比较常用的可以实现离屏绘制的方法,在绘制的代码前后加上下面两句
        //绘制代码前加上这一句
        int saveId = canvas.saveLayer(0,0,width*2,
                height*2,mPaint,Canvas.ALL_SAVE_FLAG);
        ......//中间是绘制的代码
        canvas.drawBitmap(mDestBitmap,0,0,mPaint);
        mPaint.setXfermode(xfermode);
        canvas.drawBitmap(mSrcBitmap,width/2,height/2,mPaint);
      
        //绘制代码后加上这句
        canvas.restoreToCount(saveId); 
具体离屏绘制的原因,这篇文章里有介绍:HenCoder Android 开发进阶: 自定义 View 1-2 Paint 详解
好了接下来看一个很简单的正片叠底的例子,最后会一步步了解前面的公式到底是怎么计算的
package com.example.xfermodedemo.view;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.RectF;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
/**
 * @author 
 */
public class PorterDuffView extends View {
    private Bitmap mSrcBitmap;
    private Bitmap mDestBitmap;
    private Paint mPaint = new Paint();
    private int width = 500;
    private int height = 500;
    private int bg_green = 0xFF00FF00;
    private int mSrcColor = 0xFF66AAFF;//源图像的颜色值
    private int mDestColor = 0xFFFFCC44;//目标图像的颜色值 最高位FF 代表 Alpha = 1
    private final int multyColor = 0XFF668844;
    private final int multy_src_bg = 0xFF00AA00;
    private RectF rectF = new RectF(0,500,500,1000);
    /**
     * Mode.SRC_OUT
     * 计算公式为:[Sa * (1 - Da), Sc * (1 - Da)]
     *
     * Mode.SRC_IN
     * 计算公式为:[Sa * Da, Sc * Da]
     *
     * Mode.MULTIPLY(正片叠底)
     * 公式是:[Sa * Da, Sc * Dc]
     *
     */
    private PorterDuffXfermode xfermode = new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY);
    public PorterDuffView(Context context) {
        super(context);
        init();
    }
    public PorterDuffView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    private void init() {
        mSrcBitmap = makeSrcBitmap();
        mDestBitmap = makeDestBitmap();
    }
    @RequiresApi(api = Build.VERSION_CODES.Q)
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //save() restore() 不生效
        //canvas.save();
        //不在 canvas.saveLayer()和 canvas.restoreToCount()之间的代码都不会
        //参与正片叠底
        //canvas.drawColor(bg_green);//未离屏绘制不会参与正片叠底
        int saveId = canvas.saveLayer(0,0,width*2,
                height*2,mPaint,Canvas.ALL_SAVE_FLAG);
        //canvas.drawColor(bg_green);//会参与正片叠底
        canvas.drawBitmap(mDestBitmap,0,0,mPaint);
        mPaint.setXfermode(xfermode);
        canvas.drawBitmap(mSrcBitmap,width/2,height/2,mPaint);
        mPaint.setXfermode(null);
        //canvas.restore();
        canvas.restoreToCount(saveId);
        mPaint.setColor(multyColor);
        mPaint.setStyle(Paint.Style.FILL);
        
        //以正片叠底最终合成后的颜色作为画笔颜色值 画四分之一圆作为对比
        canvas.drawArc(rectF,180,90,true,mPaint);
        //canvas.drawCircle(250f,750f,250f,mPaint);
    }
    //创建目标图像
    private Bitmap makeDestBitmap() {
        Paint paint = new Paint();
        paint.setColor(mDestColor);
        Bitmap bitmap = Bitmap.createBitmap(width,height, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        canvas.drawOval(0,0,width,height,paint);
        return bitmap;
    }
    //创建源图像
    private Bitmap makeSrcBitmap() {
        Paint paint = new Paint();
        paint.setColor(mSrcColor);
        Bitmap bitmap = Bitmap.createBitmap(width,height, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        canvas.drawRect(0,0,width,height,paint);
        return bitmap;
    }
} 
正片叠底最终的颜色值是怎么计算的?
执行正片叠底混合模式后的最终颜色是通过下面两个颜色计算得来的
private int mSrcColor = 0xFF66AAFF;//源图像的颜色值
private int mDestColor = 0xFFFFCC44;//目标图像的颜色值 最高位FF 代表 Alpha = 1
 
正片叠底的计算公式是:[Sa * Da, Sc * Dc],如果直接套用这个公式 mSrcColor * mDestColor,这样计算出的结果会是一个很大的值,并且结果也不能通过一个int值来定义,所以不能这么直接地套用公式,这里有涉及到一些图像处理技术的知识,既然不能直接一起相乘来计算,那就分开来计算。每一个颜色值从高位到低位,依次代表 ARGB,其中A是透明度,RGB代表颜色值,在RGB模式下每一个像素点的色阶范围都是 0~255,纯黑色阶值是 0,纯白色阶值是 255 也就是16进制的FF,所以我们需要依次计算出 A R G B的值,并且要通过下面的公式来计算:
正片叠底(Multiply):C = A * B/255 (注意255换成16进制就是FF)
比如还是上面的两个颜色值进行正片叠底,先算透明度 A = 0xFF * 0xFF/255 = FF,然后R = 0x66 * 0xFF/255 = 0X66,依次类推最终得出源图像和目标图像进行正片叠底后的颜色值为 0XFF668844,也就是0XFF668844,代码里定义为了 multyColor,现在这两个结论是不是也很好理解了:任何颜色和黑色正片叠底得到的仍然是黑色,任何颜色和白色进行正片叠底得到的颜色依然是原来的颜色保持不变,其他模式的计算方法跟正片叠底也是一样的,最后来看下效果。

正片叠底的绘制区域
如果画笔通过 setXfermode()设置了一个Xfermode那么在绘制的时候,会按照你传入的Xfermode的规则,在 源图像和目标图像区域相交的区域根据对应的规则进行运算,先把相交区域清空,再把运算结果覆盖到相交的区域。如果没有设置任何Xfermode,绘制的时候会直接按顺序覆盖上去。
接下来根据对应的代码看下效果
1. 直接绘制(每个代码块下方是对应的效果图)
protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawBitmap(mDestBitmap,0,0,mPaint);
        canvas.drawBitmap(mSrcBitmap,width/2,height/2,mPaint);
        
    }
 

2.设置正片叠底模式(注意:要离屏绘制)
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int saveId = canvas.saveLayer(0,0,width*2,height*2,mPaint,Canvas.ALL_SAVE_FLAG);
        canvas.drawBitmap(mDestBitmap,0,0,mPaint);
        mPaint.setXfermode(xfermode);
        canvas.drawBitmap(mSrcBitmap,width/2,height/2,mPaint);
        
        canvas.restoreToCount(saveId);
    } 

3.如果我们在离屏绘制前加一个背景色,这个背景色将不会参与正片叠底的运算
protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(bg_green);//未离屏绘制不会参与正片叠底
        int saveId = canvas.saveLayer(0,0,width*2,height*2,mPaint,Canvas.ALL_SAVE_FLAG);
        canvas.drawBitmap(mDestBitmap,0,0,mPaint);
        mPaint.setXfermode(xfermode);
        canvas.drawBitmap(mSrcBitmap,width/2,height/2,mPaint);
        
        canvas.restoreToCount(saveId);
    } 
 
如果我们要背景参与正片叠底,只需改一下位置
protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
      
        //canvas.drawColor(bg_green);//未离屏绘制不会参与正片叠底
        int saveId = canvas.saveLayer(0,0,width*2,height*2,mPaint,Canvas.ALL_SAVE_FLAG);
        canvas.drawColor(bg_green);//会参与正片叠底
        canvas.drawBitmap(mDestBitmap,0,0,mPaint);
        mPaint.setXfermode(xfermode);
        canvas.drawBitmap(mSrcBitmap,width/2,height/2,mPaint);
        
        canvas.restoreToCount(saveId);
    } 




















