Android 自定义View灵魂画笔——Xfermode混合模式全解析

3字数 2583阅读 683

前言

在平时自定义View的过程中,基础的测量绘制等操作能够打造一个基本的自定义View,但如果想让你的自定义View玩出更多花样,就要结合一些比较灵活的绘制方式,比如本文要讲的混合模式——Xfermode。之前写的几个自定义View也有用到它来实现:

传送门:『Android自定义View实战』给我一个图标,还你一个水波纹进度球


 
传送门:『Android自定义View实战』自定义完美的刮刮乐效果


之前一直没有去完整地总结Xfermode,本文将针对Xfermode的各个模式展开详细的分析,理解其各个模式的作用。

 

正文

什么是混合模式?

其概念最早来自于SIGGRAPH的Tomas Proter和Tom Duff,混合图形的概念极大地推动了图形图像学的发展,延伸到计算机图形图像学像Adobe和AutoDesk公司著名的多款设计软件都可以说一定程度上受到影响。从字面上其实大概猜到了它的作用,混合模式就是将画布中的两个图像,按照一定的算法,合成一个新的图像。在Android中提供了混合模式相关的类——Xfermode,它派生出来的一个子类——ProterDuffXfermode就是我们平常在Android中使用混合模式时需要用到的类,它定义了很多模式供我们选择,实现两个图像叠加在一起时的多样化呈现。

 

混合模式有哪些?

首先理解下图形的一些概念,图像是由很多个像素点组合而成,每个像素点都有一个自己的颜色通道组合,即所谓的ARGB:

A代表透明度Alpha
R代表红色通道Red
G代表绿色通道Green
B代表蓝色通道Blue

混合模式中,将ARGB划分为两个部分,即A+RGB的格式,也就是每个像素都会被划分为这样的形式的来描述:[Alpha,RGB],即透明度+颜色值,那么如果是两个图层叠在在一起时,它们交集的区域可以有很多种可能性,例如[取图层1的的透明度,取图层1的颜色值][取图层2的透明度,取图层2的颜色值],稍微复杂一点可以是[取图层1的透明度*图层2的透明度,取图层1的色值*图层2的透明度]....等等,将两个图像分别称之为源图像(Src)和目标图像(Dst),从而可以根据这些算法衍生出很多种模式,也就是我们的18种混合模式:

模式 组合算法 效果
ADD Saturate(S + D) 饱和相加,对图像饱和度进行相加
CLEAR [0, 0] 交集区域alpha和rgb值均为0
DARKEN [Sa + Da - SaDa, Sc(1 - Da) + Dc*(1 - Sa) + min(Sc, Dc)] 变暗,较深的颜色会覆盖浅色
DST [Da, Dc] 交集区域只显示DST的透明度和色值
DST_ATOP [Sa, Sa * Dc + Sc * (1 - Da)] 相交处绘制DST的部分,其他区域只显示SRC
DST_IN [Sa * Da, Sa * Dc] 只在DST和SRC相交的地方绘制DST的部分
DST_OUT [Da * (1 - Sa), Dc * (1 - Sa)] 只在DST和SRC交集之外的区域绘制DST的部分
DST_OVER [Sa + (1 - Sa)Da, Rc = Dc + (1 - Da)Sc] DST盖在SRC上面
LIGHTEN [Sa + Da - SaDa, Sc(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)] 相交的区域变亮
MULTIPLY [Sa * Da, Sc * Dc] 透明度和色值均不为0的地方绘制
OVERLAY 叠加效果
SCREEN [Sa + Da - Sa * Da, Sc + Dc - Sc * Dc] 保留两个图层中较白的部分,较暗的部分被遮盖
SRC [Sa, Sc] 交集区域只显示SRC的透明度和色值
SRC_ATOP [Da, Sc * Da + (1 - Sa) * Dc] 相交处绘制SRC的部分,其他区域只显示DST
SRC_IN [Sa * Da, Sc * Da] 只在DST和SRC相交的地方绘制SRC的部分
SRC_OUT [Sa * (1 - Da), Sc * (1 - Da)] 只在DST和SRC交集之外的区域绘制SRC的部分
SRC_OVER [Sa + (1 - Sa)Da, Rc = Sc + (1 - Sa)Dc] SRC盖在DST上面
XOR [Sa + Da - 2 * Sa * Da, Sc * (1 - Da) + (1 - Sa) * Dc] 相交的区域受透明度和色值的影响,如果完全不透明相交处不绘制

它们都是根据以上公式计算出来对应的绘制形式,其中,Sa、Sc代表源图像的透明度(Src Alpha)和色值(Src Color),Ds、Dc代表目标图像的透明度(Dst Alpha)和色值(Dst Color),例如 DST_IN 模式,它是根据 [Sa * Da, Sa * Dc] 算法来进行绘制,按照上文讲的 [Alpha,RGB] 的公式套进去,那么就是:

交集区域的透明度 = 源图像的透明度 * 目标图像的透明度
交集区域的色值 = 源图像的透明度 * 目标图像的色值

可以看到,无论是透明度还是色值,源图像只有透明度派上用场,换句话说,就是去除了源图像的色值,那么就只剩透明度了,所以在源图像和目标图像的交集区域,只会显示出目标图像的效果,但是其透明度会受到源图像的透明度的影响

 

如何使用混合模式?

1.禁用硬件加速

Android中混合模式的操作其实不复杂,首先,也是容易遗漏的一个步骤,就是禁用硬件加速,部分混合模式的使用必须在禁用硬件加速的前提下进行,否则出来的效果会所出入,因此一般保险起见,使用混合模式之前都禁用硬件加速,如果不想影响应用的其他地方,可以只在自定义View的构造方法中调用:

//禁用硬件加速
setLayerType(View.LAYER_TYPE_SOFTWARE, null);

 

2.绘制目标图像

先用画笔绘制一个形状或图片,作为DST图:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawBitmap(dstBm, 0, 0, paint);
}

 

3.设置混合模式

接着为画笔设置一个混合模式:

paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));

 

4.绘制源图像

canvas.drawBitmap(srcBm, 0, 0, paint);

 

5.清除混合模式

最后清除画笔混合模式,以防止下次调用onDraw的时候受影响:

paint.setXfermode(null);

汇总起来也就是:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawBitmap(dstBm, 0, 0, paint);
    paint.setXfermode(duffXfermode);
    canvas.drawBitmap(srcBm, 0, 0, paint);
    paint.setXfermode(null);
}

 

混合模式效果

纸上得来终觉浅,我们通过一个demo,来看下这些模式真正呈现出来的效果是怎样的,创建一个自定义View,并绘制一个圆形和一个方形分别作为目标图和源图:

public class DuffModeView extends View{

    private Paint paint;
    private int width, height;
    private Bitmap srcBm, dstBm;

    public DuffModeView(Context context) {
        this(context, null);
    }

    public DuffModeView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public DuffModeView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        //禁用硬件加速
        setLayerType(View.LAYER_TYPE_SOFTWARE, null);
        //初始化画笔
        paint = new Paint();
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        width = right - left;
        height = bottom - top;
        srcBm = createSrcBitmap(width, height);
        dstBm = createDstBitmap(width, height);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawBitmap(dstBm, 0, 0, paint);
        canvas.drawBitmap(srcBm, 0, 0, paint);
    }

    public Bitmap createDstBitmap(int width, int height) {
        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        Paint dstPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        dstPaint.setColor(Color.parseColor("#00b7ee"));
        canvas.drawCircle(width / 3, height / 3, width / 3, dstPaint);
        return bitmap;
    }

    public Bitmap createSrcBitmap(int width, int height) {
        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        Paint scrPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        scrPaint.setColor(Color.parseColor("#ec6941"));
        canvas.drawRect(new Rect(width / 3, height / 3, width, height), scrPaint);
        return bitmap;
    }
}

将View的宽高划分为了3等分,将圆形绘制在左上角,正方形绘制在右下角,并分别设置不同的颜色,中间有一部分相交,效果如下:


未采用任何混合模式示意图

“素材”准备好了,那么就可以开始“动刀”了,我们实例化一个PorterDuffXfermode对象,将其设置给刚才中的画笔:

duffXfermode = new PorterDuffXfermode(PorterDuff.Mode.ADD);
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //离屏绘制
    int layerID = canvas.saveLayer(0, 0, getWidth(), getHeight(), paint, Canvas.ALL_SAVE_FLAG);
    canvas.drawBitmap(dstBm, 0, 0, paint);
    paint.setXfermode(duffXfermode);
    canvas.drawBitmap(srcBm, 0, 0, paint);
    paint.setXfermode(null);
    canvas.restoreToCount(layerID);
}

这里onDraw中有几个注意的点,我们通过Canvas的saveLayer和restoreToCount将混合模式置于单独的层面中进行,以免混合模式相关的操作对画布的其他区域产生影响,通俗的讲,可以理解成从Canvas中抽取出一层单独的图层,一切绘制都不会影响到原本的画布,等调用restoreToCount之后,就会把我们的最终效果加回到Canvas上。(是不是有点类似Photoshop的图层概念)

接着经过以下几个步骤:

1.调用drawBitmap绘制了DST图
2.为画笔设置混合模式
3.用设置了混合模式的画笔绘制SRC图
4.清除画笔混合模式

这里采用了PorterDuff.Mode.ADD模式,呈现出来的效果如下:

PorterDuff.Mode.ADD 效果图

可以看到中间相交区域的不是蓝色也不是红色,而是它们的饱和度相加之后得到的结果。
我们只需要替换PorterDuffXfermode的构造方法参数,改成其他模式,即可看到其他模式的效果,依次如下:

PorterDuff.Mode.CLEAR

PorterDuff.Mode.CLEAR 效果图

CLEAR模式的公式是[0, 0],也就是透明度和色值均为0,由于我们是将混合模式设置给画笔之后才绘制红色方形,所以可以看到蓝色(DST)与红色(SRC)相交的区域,蓝色部分也一并被清除掉,也就是交集区域透明度和色值均为0,因此此时的SRC就类似是一块“橡皮擦”的效果。


PorterDuff.Mode.DARKEN

PorterDuff.Mode.DARKEN 效果图

相交的区域,看起来像是变成了另外一种颜色,其实这种模式就是比较深色的会覆盖浅色的,可以尝试把其中一个改成黑色或者白色,就更明显了~


PorterDuff.Mode.DST

PorterDuff.Mode.DST 效果图

DST模式的公式是[Da, Dc],也就是整个红色方形的区域,都取决于这两个值来显示,那么在红色方形范围内,也就只有两者相交的区域才有蓝色的透明度和色值,圆形是DST图,方形是SRC图,可以看到最终的效果只剩下DST图了,也就是交集区域只显示DST的透明度和色值。


PorterDuff.Mode.DST_ATOP

PorterDuff.Mode.DST_ATOP 效果图

DST_ATOP的公式为[Sa, Sa * Dc + Sc * (1 - Da)],透明度取决于SRC的透明度,在两者相交的区域,Da为1,所以其实色值只剩Sa*Dc,就是在交集的区域显示DST图的色值,不相交的区域只显示SRC的部分。


PorterDuff.Mode.DST_IN

PorterDuff.Mode.DST_IN 效果图.png

DST_IN的公式为[Sa * Da, Sa * Dc],可以看到,只在交集的区域有颜色和透明值的显示,且显示的是DST图的部分,这是因为在两者相交的区域,Sa不为0,所以Sa*Da显示DST的透明度,Sa*Dc显示DST的色值,在相交之外的区域,要么是Sa为0,要么是Da为0,所以这些区域计算出来的结果都是[0,0]。


PorterDuff.Mode.DST_OUT

PorterDuff.Mode.DST_OUT 效果图

DST_OUT的公式为[Da * (1 - Sa), Dc * (1 - Sa)],仔细看公式,它与DST_IN的计算方式区别仅仅在于(1-Sa),在两者不相交的区域,且Sa为0的地方,显示DST的部分,可以看到,它与DST_IN的效果恰恰就是互补。


PorterDuff.Mode.DST_OVER

PorterDuff.Mode.DST_OVER 效果图

呈现出来的效果,如同DST图盖住了SRC图,所以称之为DST_OVER~


PorterDuff.Mode.LIGHTEN

PorterDuff.Mode.LIGHTEN 效果图

与DARKEN相反,浅色覆盖深色,会呈现出变亮的效果。


PorterDuff.Mode.MULTIPLY

PorterDuff.Mode.MULTIPLY 效果图.png

MULTIPLY模式的公式为[Sa * Da, Sc * Dc],透明度为Sa*Da,说明只在相交的区域有透明度,其他区域均由于Sa=0或者Da=0导致透明度为0,色值为SRC和DST的色值相乘后的结果。


PorterDuff.Mode.OVERLAY

PorterDuff.Mode.OVERLAY 效果图

在这种模式下,虽然是覆盖,但是并不会完全遮挡,而是形成叠加的视觉效果。


PorterDuff.Mode.SCREEN

PorterDuff.Mode.SCREEN 效果图

两个形状的交集区域,呈现出了另一种颜色,但其实是取了两者较白的部分混合而成的效果。


PorterDuff.Mode.SRC

PorterDuff.Mode.SRC 效果图

SRC的公式为[Sa, Sc],在两者相交区域,Sa和Sc均不为0,在两者相交区域之外的地方,就取决于SRC的透明度和色值了,形成的效果就是混合后只显示SRC的部分。


PorterDuff.Mode.SRC_ATOP

PorterDuff.Mode.SRC_ATOP 效果图

SRC_ATOP的公式为[Da, Sc * Da + (1 - Sa) * Dc],透明度取决于DST的透明度,在两者相交的区域,Sa为1,所以其实色值只剩Sc*Da,就是在交集的区域显示SRC图的色值,不相交的区域只显示DST的部分。


PorterDuff.Mode.SRC_IN

PorterDuff.Mode.SRC_IN 效果图

SRC_IN的公式为[Sa * Da, Sc * Da],可以看到,只在交集的区域有颜色和透明值的显示,且显示的是SRC图的部分,这是因为在两者相交的区域,Da不为0,所以Sa*Da显示DST的透明度,Sc*Da显示SRC的色值,在相交之外的区域,要么是Sa为0,要么是Da为0,所以这些区域计算出来的结果都是[0,0]。


PorterDuff.Mode.SRC_OUT

PorterDuff.Mode.SRC_OUT 效果图

SRC_OUT的公式为[Sa * (1 - Da), Sc * (1 - Da)],可以看到,只在交集之外的区域有颜色和透明值的显示,且显示的是SRC图的部分,这是因为在两者相交的区域,Da为1,所以Sa*(1-Da)为0,Sc*(1 - Da)显示也为0,在相交之外的区域,只有SRC的区域Da才为0,所以这些区域计算出来的结果就是[Sa,Sc]。


PorterDuff.Mode.SRC_OVER

PorterDuff.Mode.SRC_OVER 效果图

呈现出来的效果,如同SRC图盖住了DST图,所以称之为SRC_OVER~


PorterDuff.Mode.SRC_XOR

PorterDuff.Mode.XOR 效果图

在SRC图像和DST图像相交的区域之外绘制它们,在相交的区域受到对应alpha和色值影响,如果完全不透明则相交处完全不绘制。

 

运用混合模式实现效果

裁剪任意形状图片

圆形图片View的实现方式有很多种,比如说继承ImageView在onDraw中ClipPath,混合模式也能实现这种效果,只需要将我们要加载的背景图设置为目标图像,然后绘制一个圆形形状的源图像,设置混合模式为DST_IN,那么就只会绘制出圆形范围内的背景图,从而实现圆形图片的效果,关键代码如下:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //离屏绘制
    int layerID = canvas.saveLayer(0, 0, width, height, paint, Canvas.ALL_SAVE_FLAG);
    canvas.drawBitmap(dstBm, 0, 0, paint);
    paint.setXfermode(duffXfermode);
    canvas.drawBitmap(srcBm, 0, 0, paint);
    paint.setXfermode(null);
    canvas.restoreToCount(layerID);
}

public Bitmap createDstBitmap(int width, int height) {
    return BitmapFactory.decodeResource(getResources(), R.drawable.bg_duffmode_test);
}

public Bitmap createSrcBitmap(int width, int height) {
    Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(bitmap);
    Paint dstPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    dstPaint.setColor(Color.parseColor("#ec6941"));
    canvas.drawCircle(width/2, height/2, height/2, dstPaint);
    return bitmap;
}

 
效果如下:


混合模式实现圆形图片

甚至我们可以自定义裁剪形状,只要修改上面代码中创建源图像的代码如下:

public Bitmap createSrcBitmap(int width, int height) {
    Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(bitmap);
    Paint srcPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    srcPaint.setStyle(Paint.Style.FILL);
    srcPaint.setColor(Color.parseColor("#ec6941"));
    Path path = new Path();
    path.moveTo(width/2, 0);
    path.lineTo(width/6, height);
    path.lineTo(width*5/6, height);
    path.close();
    canvas.drawPath(path, srcPaint);
    //canvas.drawCircle(width/2, height/2, height/2, srcPaint);
    return bitmap;
}

就能得到一个三角形的效果:


混合模式实现三角形图片

水波纹进度球效果

原理其实就是利用贝塞尔曲线绘制水波纹性状并且将其闭合起来,作为目标图像,然后再绘制一个圆形作为源图像,也就是用圆形来“裁剪”水波纹,利用混合模式,只显示出圆形范围内的水波纹部分,从而形成水波纹进度球效果:

纯色水波纹球

甚至可以利用图标去填充水波纹的区域,代码比较多就不贴了,详见我另一篇文章『Android自定义View实战』给我一个图标,还你一个水波纹进度球

 

结语

简单来讲,其实目标图像就类似于底片,源图像就类似于叠加在上面的图层,混合模式就相当于各种滤镜。混合模式虽然一共只有18种,但巧妙地利用这些模式已经足以帮助我们轻松实现一些特殊效果,本文只是列举一些利用混合模式实现的效果,如果你有更好的创意,欢迎一起讨论~ 文中Demo完整代码已上传到 一个集合酷炫效果的自定义组件库,欢迎Issue。
 

欢迎关注 Android小Y 的简书,更多Android精选自定义View

『Android自定义View实战』实现一个小清新的弹出式圆环菜单
『Android自定义View实战』玩转PathMeasure之自定义支付结果动画
『Android自定义View实战』自定义弧形旋转菜单栏——卫星菜单
『Android自定义View实战』自定义带入场动画的弧形百分比进度条

GitHubGitHub-ZJYWidget
CSDN博客IT_ZJYANG
简 书Android小Y
GitHub 上建了一个集合炫酷自定义View的项目,里面有很多实用的自定义View源码及demo,会长期维护,欢迎Star~ 如有不足之处或建议还望指正,相互学习,相互进步,如果觉得不错动动小手点个喜欢, 谢谢~

推荐阅读更多精彩内容