Android高级进阶——绘图篇(五)setXfermode 设置混合模式

一、GPU硬件加速

  • 1、概述

GPU英文全称Graphic Processing Unit,中文翻译为“图形处理器”。与CPU不同,GPU是专门为处理图形任务而产生的芯片。
在GPU出现之前,CPU一直负责着所有的运算工作,CPU的架构是有利于X86指令集的串行架构,CPU从设计思路上适合尽可能快的完成一个任务。但当面对类似多媒体、图形图像处理类型的任务时,就显得力不从心。因为在多媒体计算中通常要求更高的运算密度、多并发线程和频繁地存储器访问;显然当你打游戏时,屏幕上的动画是需要实时刷新的,这些都需要频繁的计算、存取动作;如果CPU不能及时响应,那么屏幕就会显得很卡……你的队友可能会发一句……我等的花都谢了,你咋还不动呢……
为了专门处理多媒体的计算、存储任务,GPU就应运而生了,GPU中自带处理器和存储器,以用来专门计算和存储多媒体任务。
对于Andorid来讲,在API 11之前是没有GPU的概念的,在API 11之后,在程序集中加入了对GPU加速的支持,在API 14之后,硬件加速是默认开启的!我们可以显式地强制图像计算时使用GPU而不使用CPU.

在CPU绘制和GPU绘制时,在流程上是有区别的:

  • 在基于软件的绘制模型下,CPU主导绘图,视图按照两个步骤绘制:
  • 让View层次结构失效
  • 绘制View层次结构
  • 在基于硬件加速的绘制模式下,GPU主导绘图,绘制按照三个步骤绘制:
  • 让View层次结构失效
  • 记录、更新显示列表
  • 绘制显示列表

可以看到在GPU加速时,流程中多了一项“记录、更新显示列表”,它表示在第一步View层次结构失效后,并不是直接开始逐层绘制,而是首先把这些View的绘制函数作为绘制指令记录一个显示列表中,然后再读取显示列表中的绘制指令调用OpenGL相关函数完成实际绘制。所以在GPU加速时,实际是使用OpenGL的函数来完成绘制的。

所以使用GPU加速的优点显而易见:硬件加速提高了Android系统显示和刷新的速度;

  • 它有缺点也显而易见:
  • 1、 兼容性问题:由于是将绘制函数转换成OpenGL命令来绘制,定然会存在OpenGL并不能完全支持原始绘制函数的问题,所以这就会造成在打开GPU加速时,效果会失效的问题。
  • 2、内存消耗问题:由于需要OpenGL的指令,所以需要把系统中的OpenGL相关的包加载到内存中来,所以单纯OpenGL API调用就会占用8MB,而实际上会占用更多内存;
  • 3、电量消耗问题:多使用了一个部件,当然会更耗电……

下图显示了一些特殊函数硬件加速开始支持的平台等级:(红叉表示任何平台都不支持,不在列表中的默认在API 11就开始支持)

《Android高级进阶——绘图篇(五)setXfermode 设置混合模式》
《Android高级进阶——绘图篇(五)setXfermode 设置混合模式》
《Android高级进阶——绘图篇(五)setXfermode 设置混合模式》

图片摘自《google官方文档:硬件加速》
我再重复一遍,上面我们涉及了两个API等级,在API 11以后,在程序集中加入了对GPU加速的支持,在API 14之后,硬件加速是默认开启的!也就是说在API 11——API 13虽然是支持硬件加速的,但是默认是关闭的。

  • 2、禁用GPU硬件加速方法
    那么问题就来了,如果你的APP跑在API 14版本以后,而你洽好要用那些不支持硬件加速的函数要怎么办?
    那就只好禁用硬件加速喽,针对不同类型的东东,Android给我们提供了不同的禁用方法:
    硬件加速分全局(Application)、Activity、Window、View 四个层级

  • 1.在AndroidManifest.xml文件为application标签添加如下的属性即可为整个应用程序开启/关闭硬件加速:

<application android:hardwareAccelerated=”true” …>

  • 2.在Activity 标签下使用 hardwareAccelerated 属性开启或关闭硬件加速:

<activity android:hardwareAccelerated=”false” />

    1. 在Window 层级使用如下代码开启硬件加速:(Window层级不支持关闭硬件加速)

getWindow().setFlags(
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);

  • 4.View 级别如下关闭硬件加速:(view 层级上不支持开启硬件加速)

setLayerType(View.LAYER_TYPE_SOFTWARE, null);

或者使用android:layerType=”software”来关闭硬件加速:比如

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    android:layout_width="fill_parent"  
    android:layout_height="fill_parent"  
    android:orientation="vertical"  
    android:paddingLeft="2dp"  
    android:layerType="software"  
    android:paddingRight="2dp" >  

二、setXfermode(Xfermode xfermode)之PorterDuffXfermode

这个函数是图像混合里最难的一个了,它的功能也是相当强大的,这个模式叫做图形混合模式。
与setColorFilter一样,派生自Xfermode的有三个类:

《Android高级进阶——绘图篇(五)setXfermode 设置混合模式》 image.png

  • 1、概述——基本流程
    从上面可以看出,派生自Xfermode的有AvoidXfermode,PixelXorXfermode,PorterDuffXfermode;
    从硬件加速不支持的函数列表中,我们可以看到AvoidXfermode,PixelXorXfermode是完全不支持的,而PorterDuffXfermode是部分不支持的。

《Android高级进阶——绘图篇(五)setXfermode 设置混合模式》 image.png

所以在使用Xfermode时,为了保险起见,我们需要做两件事:

  • 1、禁用硬件加速:

setLayerType(View.LAYER_TYPE_SOFTWARE, null);

  • 2、使用离屏绘制
//新建图层  
int layerID = canvas.saveLayer(0,0,width,height,mPaint,Canvas.ALL_SAVE_FLAG);  
  
//TODO 核心绘制代码  
  
//还原图层  
canvas.restoreToCount(layerID);  

有关离屏绘制的原因,这节就先不给大家引申了,后面会单独拉出来一篇文章讲离屏绘制(Canvas图层相关),大家只需要知道,我们需要把绘制的核心代码放在saveLayer()和restoreToCount()之间即可。

下面我们先简单讲解PorterDuffXfermode的用法,然后写个例子,看下SetXfermode()的使用方法和效果

PorterDuffXfermode的声明如下:

public PorterDuffXfermode(PorterDuff.Mode mode)

它只有一个参数PorterDuff.Mode,它的可取值有如下几个:

Mode.CLEAR  
Mode.SRC  
Mode.DST  
Mode.SRC_OVER  
Mode.DST_OVER  
Mode.SRC_IN  
Mode.DST_IN  
Mode.SRC_OUT  
Mode.DST_OUT  
Mode.SRC_ATOP  
Mode.DST_ATOP  
Mode.XOR  
Mode.DARKEN  
Mode.LIGHTEN  
Mode.MULTIPLY  
Mode.SCREEN  
Mode.OVERLAY  
Mode.ADD  

上面每一个模式都对应着一个算法:

《Android高级进阶——绘图篇(五)setXfermode 设置混合模式》 image.png

摘自《google:PorterDuff.Mode》
比如LIGHTEN的计算方式为[Sa + Da – SaDa, Sc(1 – Da) + Dc(1 – Sa) + max(Sc, Dc)],其中Sa全称为Source alpha表示源图的Alpha通道;Sc全称为Source color表示源图的颜色;Da全称为Destination alpha表示目标图的Alpha通道;Dc全称为Destination color表示目标图的颜色,在每个公式中,都会被分为两部分[……,……],其中“,”前的部分为“Sa + Da – SaDa”这一部分的值代表计算后的Alpha通道而“,”后的部分为“Sc(1 – Da) + Dc(1 – Sa) + max(Sc, Dc)”这一部分的值代表计算后的颜色值,图形混合后的图片就是依据这个公式来对DST和SRC两张图像中每一个像素进行计算,得到最终的结果的。

Google给我们了一张图,显示的是两个图形一圆一方通过一定的计算产生不同的组合效果,其中圆形是底部的目标图像,方形是上方的源图像。

《Android高级进阶——绘图篇(五)setXfermode 设置混合模式》

在上面的公式中涉及到一个概念,目标图DST,源图SRC。那什么是源图,什么是目标图呢?我们简单举例子来说明一下:

    private void init() {
        //初始化画笔
        paint = new Paint();
        paint.setColor(Color.RED);
        paint.setStyle(Paint.Style.FILL_AND_STROKE);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //禁用硬件加速
        setLayerType(View.LAYER_TYPE_SOFTWARE, null);
        //使用离屏绘制
        int layerID = canvas.saveLayer(0, 0, getWidth(), getHeight(), paint, Canvas.ALL_SAVE_FLAG);

        canvas.drawBitmap(createDstBigmap(width, height), 0, 0, paint);
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
        canvas.drawBitmap(createSrcBigmap(width, height), width / 2, height / 2, paint);
        paint.setXfermode(null);

        canvas.restoreToCount(layerID);

    }

    public Bitmap createDstBigmap(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(0xFFFFCC44);
        canvas.drawCircle(width / 2, height / 2, width / 2, scrPaint);
        return bitmap;
    }

    public Bitmap createSrcBigmap(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(0xFF66AAFF);
        canvas.drawRect(new Rect(0, 0, width, height), dstPaint);
        return bitmap;
    }

《Android高级进阶——绘图篇(五)setXfermode 设置混合模式》 image.png

它会在源图像所在区域与目标图像运算,在得到结果以后,将结果覆盖到目标图像上。整个过程如下:
首先在两个矩形的所在位置

《Android高级进阶——绘图篇(五)setXfermode 设置混合模式》 image.png

然后是源图像计算结果的覆盖过程:

《Android高级进阶——绘图篇(五)setXfermode 设置混合模式》 image.png

其中蓝色小块是源图像所在区域与目标图像经过运算的结果(有关这个结果为什么是一小块蓝色,下面会具体讲),在得到结果以后,把结果对应区域的图像先清空,然后把结果覆盖上去。

这里还需要强调一点,源图像在运算时,只是在源图像所在区域与对应区域的目标图像做运算。所以目标图像与源图像不相交的地方是不会参与运算的!这一点非常重要!不相交的地方不会参与运算,所以不相交的地方的图像也不会是脏数据,也不会被更新,所以不相交地方的图像也永远显示的是目标图像。

  • 2、Google的误导
    大家仔细看我们示例代码的结果,同样是SRC_IN模式,为什么我们的结果与google的图像不一样呢?
    google给出的SRC_IN的结果是这样的

《Android高级进阶——绘图篇(五)setXfermode 设置混合模式》 image.png

而我们的运算结果确是这样的:

《Android高级进阶——绘图篇(五)setXfermode 设置混合模式》 image.png

在Android\sdk\samples\android-XX\legacy\ApiDemos\src\com\example\android\apis\graphics\Xfermodes.java中可以找到google所给图像的源码:(我仿照上面的示例,对源码进行更改,只演示SRC_IN的合成样式,具体源码大家可以从上面路径查找)

public class Xfermodes extends View {  
  
        // create a bitmap with a circle, used for the "dst" image  
        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;  
    }  
    private int width = 400;  
    private int height = 400;  
    private Bitmap dstBmp;  
    private Bitmap srcBmp;  
    private Paint mPaint;  
  
    public Xfermodes(Context context,AttributeSet attrs) {  
        super(context,attrs);  
  
        srcBmp = makeSrc(width, height);  
        dstBmp = makeDst(width, height);  
        mPaint = new Paint();  
    }  
  
    @Override   
    protected void onDraw(Canvas canvas) {  
        canvas.drawColor(Color.WHITE);  
  
        int layerID = canvas.saveLayer(0,0,width*2,height*2,mPaint,Canvas.ALL_SAVE_FLAG);  
  
        canvas.drawBitmap(dstBmp, 0, 0, mPaint);  
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));  
        canvas.drawBitmap(srcBmp, 0, 0, mPaint);  
        mPaint.setXfermode(null);  
  
        canvas.restoreToCount(layerID);  
    }  
}  

效果图如下:

《Android高级进阶——绘图篇(五)setXfermode 设置混合模式》 image.png

我们从代码中分析下,google是如何来得到这个图像的。
看源码中,他也是通过makeDst(int w, int h)和makeSrc(int w, int h)生成两个bitmap,一圆一方,但仔细看他的代码,就会发现问题
先看makeDst(int w, int h)

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;  
}

这段函数是用来生成圆形的目标图像的,它首先生成一个宽度为w,高度为h的空白图像:

Bitmap bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);

然后在这个空白图像上画圆形:

c.drawOval(new RectF(0, 0, w3/4, h3/4), p);

现在问题来了,它这里bitmap的宽和高是w和h,但画的圆形的大小却是new RectF(0, 0, w3/4, h3/4)!并没有完全填满bitmap,画出来的效果图是这样的:

《Android高级进阶——绘图篇(五)setXfermode 设置混合模式》 image.png

其中红色矩形区域是整个bitmap的大小。

然后再看makeSrc(int w, int h)

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;  
}  

这里同样是新建一个宽度为w和高度为h的空白图像:

Bitmap bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);

然后再在这个空白bitmap上画一个矩形:

c.drawRect(w/3, h/3, w19/20, h19/20, p);

大家注意到了没,它同样是比空白bitmap小的,它竟然是从(w/3, h/3)开始画,right和bottom的坐标在( w19/20, h19/20)!这个比bitmap小太多了,我们来看下它的图像

《Android高级进阶——绘图篇(五)setXfermode 设置混合模式》 image.png

同样,红色框表示bitmap的位置。从中可以看到明显矩形框只占整个bitmap的其中小部分

最后我们来看看是如何将这两个bitmap绘出来的

protected void onDraw(Canvas canvas) {  
    canvas.drawColor(Color.WHITE);  
  
    int layerID = canvas.saveLayer(0,0,width*2,height*2,mPaint,Canvas.ALL_SAVE_FLAG);  
  
    canvas.drawBitmap(dstBmp, 0, 0, mPaint);  
    mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));  
    canvas.drawBitmap(srcBmp, 0, 0, mPaint);  
    mPaint.setXfermode(null);  
  
    canvas.restoreToCount(layerID);  
}  

最最关键的是看这里:

canvas.drawBitmap(dstBmp, 0, 0, mPaint);  
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));  
canvas.drawBitmap(srcBmp, 0, 0, mPaint);  

注意,这两个bitmap的大小都是width和height,而且都是从(0,0)位置开始绘制的!!!!这说明这两个bitmap是完全重合的!!!!
所以它的合成过程是这样的:

《Android高级进阶——绘图篇(五)setXfermode 设置混合模式》 image.png

即两个同样大小的bitmap合并。正是由于这两个bitmap所在位置和大小是完全一样的,所以在以源图像所在区域与目标图像做计算时,是将两个图像完全重合计算的,而不是像我们前面示例中那样,只有一部分相交区域。所以google这么做是对开发者的误导,利用两个完全不同的完全相同大小的图片,只在其中一部分画矩形和圆形,但在做计算时却是以整个bitmap大小来做计算的,这显然是不正确的。
最后,仿照示例代码,在(0,0,width,height)画一个圆形,然后在(width/2,height/2,3width/2,3height/2)的位置画一个矩形,然后应用各个Mode样式结果如下:

《Android高级进阶——绘图篇(五)setXfermode 设置混合模式》 image.png

模式的具体使用

下面将逐个讲解每个模式的意义,在开始讲解之前,我们随便拿一个效果图来看一下,我们在这个效果图中需要关注哪两点

《Android高级进阶——绘图篇(五)setXfermode 设置混合模式》 image.png

对应代码:

canvas.drawBitmap(dstBmp, 0, 0, mPaint);  
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.LIGHTEN));  
canvas.drawBitmap(srcBmp,width/2,height/2, mPaint);  

其实在最后一句计算效果图像时,是以源图像所在区域为计算目标的,把计算后的源图像更新到对应区域内。

所以如上图所示,我们在计算源图像所在区域效果图时,需要着重关注两个区域:
1、如图标示区域一:区域一是源图像和目标图像的相交区域,由于在这个区域源图像和目标图像像素都不是空白像素,所以可以明显看出颜色的计算效果。
2、如图标示区域二:在区域二中,源图像所在区域的目标图像是空白像素,所以这块区域所表示的意义就是,当某一方区域是空白像素时,此时的计算结果。
总而言之:我们在下面的各个模式计算时,只需要关注图示中的区域一和区域二;其中区域一表示当源图像和目标图像像素都不是空白像素时的计算结果,而区域二则表示当某一方区域是空白像素时,此时的计算结果。

一、颜色叠加相关模式

这部分涉及到的几个模式有Mode.ADD(饱和度相加)、Mode.DARKEN(变暗),Mode.LIGHTEN(变亮)、Mode.MULTIPLY(正片叠底)、Mode.OVERLAY(叠加),Mode.SCREEN(滤色)

  • 1 、Mode.ADD(饱和度相加)
    它的公式是Saturate(S + D);ADD模式简单来说就是对SRC与DST两张图片相交区域的饱和度进行相加
    同样使用上篇中的示例,一个矩形,一个圆形来做相加 (代码就不给了,代码还是一样的代码就是改了个模式)

效果图如下:

《Android高级进阶——绘图篇(五)setXfermode 设置混合模式》 image.png

从效果图中可以看出,只有源图与目标图像相交的部分的图像的饱和度产生了变化,没相交的部分是没有变的,因为对方的饱和度是0,当然不相交的位置饱和度是不会变的。
这个模式的应用范围比较少,暂时想不到哪里会用到;

  • 2、Mode.LIGHTEN(变亮)
    它的算法是: [Sa + Da – SaDa,Sc(1 – Da) + Dc*(1 – Sa) + max(Sc, Dc)]
    圆形和矩形的效果图为:

《Android高级进阶——绘图篇(五)setXfermode 设置混合模式》 image.png

这个效果比较容易理解,两个图像重合的区域才会有颜色值变化,所以只有重合区域才有变亮的效果,源图像非重合的区域,由于对应区域的目标图像是空白像素,所以直接显示源图像。

我们在实际应用中,会有下面的这个情况,当选中一本书时,给这本书加上灯光效果

《Android高级进阶——绘图篇(五)setXfermode 设置混合模式》

其实它是两张图合成的:
DST:目标图像

《Android高级进阶——绘图篇(五)setXfermode 设置混合模式》 image.png

SRC:源图像

《Android高级进阶——绘图篇(五)setXfermode 设置混合模式》 image.png

可以看到,在这张图片的最上方中间的位置有些白色半透明的填充,其它位置都是透明的。

  • 3、Mode.DARKEN(变暗)

对应公式是: [Sa + Da – SaDa,Sc(1 – Da) + Dc*(1 – Sa) + max(Sc, Dc)]
示例图像为:

《Android高级进阶——绘图篇(五)setXfermode 设置混合模式》

  • 4、Mode.MULTIPLY(正片叠底)
    公式是:[Sa * Da, Sc * Dc]
    示例图像为:

《Android高级进阶——绘图篇(五)setXfermode 设置混合模式》 image.png

有些同学会奇怪了,Photoshop中也有正片叠底啊,相交区域正片叠底后的颜色确实是绿色的,但源图像的非相交区域怎么没了?
我们来看下他的计算公式:[Sa * Da, Sc * Dc],在计算alpha值时的公式是Sa * Da,是用源图像的alpha值乘以目标图像的alpha值;由于源图像的非相交区域所对应的目标图像像素的alpha是0,所以结果像素的alpha值仍是0,所以源图像的非相交区域在计算后是透明的。
在两个图像的相交区域的混合方式是与photoshop中的正片叠底效果是一致的。

  • 5、Mode.OVERLAY(叠加)
    这个没有给出公式……
    示例图像为:

《Android高级进阶——绘图篇(五)setXfermode 设置混合模式》 image.png

虽然没有给出公式,但从效果图中可以看到,源图像交合部分有效果,非交合部分依然是存在的,这就可以肯定一点,当目标图像透明时,在这个模式下源图像的色值不会受到影响;

  • 6、Mode.SCREEN(滤色)
    对应公式是:[Sa + Da – Sa * Da, Sc + Dc – Sc * Dc]
    示例图像为:

    《Android高级进阶——绘图篇(五)setXfermode 设置混合模式》 image.png

同样,只是源图像与目标图像交合部分有效果,源图像非交合部分保持原样。

  • 7、示例——twitter标识的描边效果
    由于这些模式在photoshop中都存在,直接拿目标图像和源图像在photoshop中就可以演示出来,就没有多举例子,其实,在实现时实现两图像混合时,也经常会用到这些模式的,比如这里twitter的暗光效果
    我们这里有两张源图:
    图一:

《Android高级进阶——绘图篇(五)setXfermode 设置混合模式》 image.png

图二:

《Android高级进阶——绘图篇(五)setXfermode 设置混合模式》 image.png

合成图如下:

《Android高级进阶——绘图篇(五)setXfermode 设置混合模式》 image.png

我们先想想这个要实现的效果有哪些特性:
首先,
在图一中,小鸟整个都是蓝色的
在图二中,只有小鸟的边缘部分是白色的,中间部分是透明的。
在最终的合成图中:图一和图二中小鸟与边缘的是显示的,而且还有某种效果,但小鸟中间的区域变透明了!显示的是底部Activity的背景色。
想到我们前面学到的几种样式中,Mode.MULTIPLY(正片叠底)、SRC_IN 和 DST_IN 会在两个图像的一方透明时,结果像素就是透明的。所以这里使用的模式就是Mode.MULTIPLY
对应代码如下:

    @Override  
    protected void onDraw(Canvas canvas) {  
        super.onDraw(canvas);  
  
        int layerId = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);  
  
        BmpDST = BitmapFactory.decodeResource(getResources(),R.drawable.twiter_bg,null);  
        BmpSRC = BitmapFactory.decodeResource(getResources(),R.drawable.twiter_light,null);  

        canvas.drawBitmap(BmpDST,0,0,mBitPaint);  
        mBitPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY));  
        canvas.drawBitmap(BmpSRC,0,0,mBitPaint);  
  
        mBitPaint.setXfermode(null);  
        canvas.restoreToCount(layerId);  
    }  
}  

二、SRC_IN 和 DST_IN 模式

  • 1、Mode.SRC_IN
    示例图像为:

《Android高级进阶——绘图篇(五)setXfermode 设置混合模式》 image.png

  • 2、MODE.DST_IN

《Android高级进阶——绘图篇(五)setXfermode 设置混合模式》 image.png

共同点:

当模式为 SRC_IN 和 DST_IN 时,不管是 目标图像 还是 源图像 只要是和空白像素(完全透明)相交的像素,计算结果也将为空白像素(完全透明)
不同点:
相交局域视情况显示(SRC_IN 时,相交区域显示为 源图像,相反显示 目标图像)

我们来验证一下我们上面得出的结论:

  • 验证一:
    首先把 目标图像 的画笔设为 Color.TRANSPARENT(完全透明)我们来试一下当 模式 为 PorterDuff.Mode.SRC_IN 时 效果是怎样的?

《Android高级进阶——绘图篇(五)setXfermode 设置混合模式》

什么都没显示是对的,因为 目标图像 是空白像素,本身就显示不出来,而 源图像 与 目标图像 的空白像素相交的像素,计算结果也将为空白像素(完全透明),就是这么个情况

  • 验证二:
    首先把 源图像 的画笔设为 Color.TRANSPARENT(完全透明)我们来试一下当 模式 为 PorterDuff.Mode.DST_IN 时 效果是怎样的?

效果是一样的,必将规则就是 与 空白像素相交变空白像素,理所当然

三、SRC_OUT 和 DST_OUT 模式

  • 1、SRC_OUT 模式
    示例图像为:

    《Android高级进阶——绘图篇(五)setXfermode 设置混合模式》 image.png

可以发现,当模式为 SRC_OUT 时,当前 目标图像 和 源图像 均不是空白像素,相交区域变为空白像素(完全透明),而未相交区域则正常显示原本像素

  • 假设一下,如果 目标像素为空白像素(完全透明)时,会变成什么样子呢?来试一下,把目标元素的画笔颜色改为 Color.TRANSPARENT(完全透明):
    效果图如下:

《Android高级进阶——绘图篇(五)setXfermode 设置混合模式》 image.png

发现,源图像 和 目标图像 的相交区域并没有变透明

  • 那如果 源图像 为空表像素(完全透明)呢?会是什么效果,来看一下:
    效果图如下:

    《Android高级进阶——绘图篇(五)setXfermode 设置混合模式》 image.png

结论:当模式为 SRC_OUT 时, 如果 目标图像 不是空白像素时,相交区域会变成空白像素,未相交区域正常显示,如果 目标图像 为空白像素(完全透明)时,则显示 源图像。

  • 2、DST_OUT 模式
    示例图像为:

    《Android高级进阶——绘图篇(五)setXfermode 设置混合模式》 image.png

可以发现,这个显示效果和 模式为 SRC_OUT 同时 源图像 为空白像素时的显示效果一样,是因为 目标像素 和 源像素 都不是空白像素,所以相交区域变成了空白像素,那么 源图像 的其他区域为什么也变成了空白像素呢?是因为 源图像 的其他区域的相交区域是空白像素导致的。

应用场景:(橡皮擦效果实现)
效果图如下:

《Android高级进阶——绘图篇(五)setXfermode 设置混合模式》 Jietu20180423-211846.gif

代码:

   private void init() {
        //初始化画笔
        paint = new Paint();
        //设置画笔颜色(不能为完全透明)
        paint.setColor(Color.RED);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(100);
        // 源图像
        srcBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.dog, null);
        //目标图像
        dstBitmap = Bitmap.createBitmap(srcBitmap.getWidth(), srcBitmap.getHeight(), Bitmap.Config.ARGB_8888);
        //路径(贝塞尔曲线)
        path = new Path();
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //禁用硬件加速
        setLayerType(View.LAYER_TYPE_SOFTWARE, null);
        //使用离屏绘制
        int layerID = canvas.saveLayer(0, 0, getWidth(), getHeight(), paint, Canvas.ALL_SAVE_FLAG);

        //先将路径绘制到 bitmap上
        Canvas dstCanvas = new Canvas(dstBitmap);
        dstCanvas.drawPath(path, paint);

        //绘制 目标图像
        canvas.drawBitmap(dstBitmap, 100, 100, paint);
        //设置 模式 为 SRC_OUT
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT));
        //绘制源图像
        canvas.drawBitmap(srcBitmap, 100, 100, paint);
        paint.setXfermode(null);

        canvas.restoreToCount(layerID);

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                eventX = event.getX();
                eventY = event.getY();
                path.moveTo(eventX, eventY);
                break;
            case MotionEvent.ACTION_MOVE:
                float endX = (event.getX() - eventX) / 2 + eventX;
                float endY = (event.getY() - eventY) / 2 + eventY;
                path.quadTo(eventX, eventY, endX, endY);
                eventX = event.getX();
                eventY = event.getY();
                break;
        }
                invalidate();
        return true;
    }

代码非常简单,利用的原理就是使用 SRC_OUT 模式时,当 目标图像 和 源图像 均不为 空白像素时,相交局域会变成空白像素(完全透明),所以在绘制途径时,使用的画笔颜色不能采用 Color.TRANSPARENT,必须要有颜色值。

首先这里涉及到使用贝赛尔曲线构造手指轨迹的知识,请参考《Android高级绘制——绘图篇(三)路径Path绘制以及贝塞尔曲线使用技巧

  • 2、刮刮卡效果

《Android高级进阶——绘图篇(五)setXfermode 设置混合模式》 Jietu20180423-222757.gif

代码:

    private void init() {
        //初始化画笔
        paint = new Paint();
        //设置画笔颜色(不能为完全透明)
        paint.setColor(Color.RED);
        paint.setStrokeWidth(100);
        paint.setStyle(Paint.Style.STROKE);
        // 源图像
        srcBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.guaguaka, null);
        //目标图像
        dstBitmap = Bitmap.createBitmap(srcBitmap.getWidth(), srcBitmap.getHeight(), Bitmap.Config.ARGB_8888);
        //中奖信息
        bitmap = Bitmap.createBitmap(srcBitmap.getWidth(), srcBitmap.getHeight(), Bitmap.Config.ARGB_8888);
        //路径(贝塞尔曲线)
        path = new Path();
        //绘制中奖信息文字的画笔
        textPaint = new Paint();
        textPaint.setColor(Color.RED);
        textPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        textPaint.setTextSize(50);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //禁用硬件加速
        setLayerType(View.LAYER_TYPE_SOFTWARE, null);

        Canvas canvas1 = new Canvas(bitmap);

        String text = "别傻了,宝贝,洗洗睡去吧!!!”;
        //获取文字宽度
        float textWidth = textPaint.measureText(text);
        //居中绘制文字,这里没有考虑高度居中
        canvas1.drawText(text, (bitmap.getWidth() - textWidth) / 2, bitmap.getHeight() / 2, textPaint);

        canvas.drawBitmap(bitmap, 100, 100, paint);

        //使用离屏绘制
        int layerID = canvas.saveLayer(0, 0, getWidth(), getHeight(), paint, Canvas.ALL_SAVE_FLAG);


        //先将路径绘制到 bitmap上
        Canvas dstCanvas = new Canvas(dstBitmap);
        dstCanvas.drawPath(path, paint);

        //绘制 目标图像
        canvas.drawBitmap(dstBitmap, 100, 100, paint);
        //设置 模式 为 SRC_OUT
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT));

        canvas.drawBitmap(srcBitmap, 100, 100, paint);

        //绘制源图像
        paint.setXfermode(null);

        canvas.restoreToCount(layerID);

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                eventX = event.getX();
                eventY = event.getY();
                path.moveTo(eventX, eventY);
                break;
            case MotionEvent.ACTION_MOVE:
                float endX = (event.getX() - eventX) / 2 + eventX;
                float endY = (event.getY() - eventY) / 2 + eventY;
                path.quadTo(eventX, eventY, endX, endY);
                eventX = event.getX();
                eventY = event.getY();
                break;

        }
        invalidate();
        return true;
    }

这里需要注意的地方有那么一个:

        Canvas canvas1 = new Canvas(bitmap);

        String text = "别傻了,宝贝,洗洗睡去吧!!!”;
        //获取文字宽度
        float textWidth = textPaint.measureText(text);
        //居中绘制文字,这里没有考虑高度居中
        canvas1.drawText(text, (bitmap.getWidth() - textWidth) / 2, bitmap.getHeight() / 2, textPaint);

        canvas.drawBitmap(bitmap, 100, 100, paint);

在绘制中奖信息的时候,是在离屏绘制之外绘制的,为什么要在离屏绘制之前绘制呢?自己先想想,这个问题放到后面介绍 Canvas 图层时会详细说,这里由于篇幅问题就先放放……

最后一个:

Mode.CLEAR

因为这个模式下篇博客会用到,就介绍下
计算公式:[0, 0]
示例图像:

《Android高级进阶——绘图篇(五)setXfermode 设置混合模式》 image.png

后面我们做清空图像的时候会用到这个模式,从公式中可以看到,计算结果直接就是[0,0]即空像素。也就是说,源图像所在区域都会变成空像素!
这样就起到了清空源图像所在区域图像的功能了。

实用经验:
在实际应用中,我们可以从下面三个方面来决定使用哪一个模式:
1、首先,目标图像和源图像混合,需不需要生成颜色的叠加特效,如果需要叠加特效则从颜色叠加相关模式中选择,有Mode.ADD(饱和度相加)、Mode.DARKEN(变暗),Mode.LIGHTEN(变亮)、Mode.MULTIPLY(正片叠底)、Mode.OVERLAY(叠加),Mode.SCREEN(滤色)
2、当不需要特效,而需要根据某一张图像的透明像素来裁剪时,就需要使用SRC相关模式或DST相关模式了。由于SRC相关模式与DST相关模式是相通的,唯一不同的是决定当前哪个是目标图像和源图像;
3、当需要清空图像时,使用Mode.CLEAR

其他模式在实际应用中并不是很常见,等用到了在说吧,就这样,下篇会在结合 这几个模式在测试几个例子,后面开始图层,在后面就开始写实际自定义 View 以及 自定义绘制View 大概还会有个五六篇的样子,争取在月底之前搞定,加油!!!

    原文作者:aKaiC
    原文地址: https://www.jianshu.com/p/78c36742d50f
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞