专栏名称: 安卓开发精选
伯乐在线旗下账号,分享安卓应用相关内容,包括:安卓应用开发、设计和动态等。
目录
相关文章推荐
鸿洋  ·  关于 2025 副业探索,DeepSeek ... ·  昨天  
51好读  ›  专栏  ›  安卓开发精选

Android 中 Canvas 绘图之 PorterDuffXfermode 使用及工作原理详解

安卓开发精选  · 公众号  · android  · 2017-02-25 21:57

正文

(点击 上方公众号,可快速关注)

来源:孙群 

blog.csdn.net/iispring/article/details/50472485

如有好文章投稿,请点击 → 这里了解详情


概述


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


PorterDuffXfermode这个类中的Porter和Duff是两个人名,这两个人在1984年一起写了一篇名为《Compositing Digital Images》的论文。


我们知道,一个像素是由RGBA四个分量组成的,该论文就论述了如何实现不同数字图像的像素之间是如何进行混合的,该论文提出了多种像素混合的模式。如果做过图像处理开发,会对其比较了解,该技术也和OpenGL中的Alpha混合技术异曲同工。


PorterDuffXfermode支持以下十几种像素颜色的混合模式,分别为:CLEAR、SRC、DST、SRC_OVER、DST_OVER、SRC_IN、DST_IN、SRC_OUT、DST_OUT、SRC_ATOP、DST_ATOP、XOR、DARKEN、LIGHTEN、MULTIPLY、SCREEN。


我们下面会分析几个代码片段研究PorterDuffXfermode使用及工作原理详解。




示例一


我们在演示如何使用PorterDuffXfermode之前,先看一下下面这个例子,代码如下所示:


@Override

    protected void onDraw(Canvas canvas) {

        super.onDraw(canvas);

        //设置背景色

         canvas.drawARGB(255, 139, 197, 186);

 

        int canvasWidth = canvas.getWidth();

        int r = canvasWidth / 3;

        //绘制黄色的圆形

        paint.setColor(0xFFFFCC44);

        canvas.drawCircle(r, r, r,  paint);

        //绘制蓝色的矩形

        paint.setColor(0xFF66AAFF);

        canvas.drawRect(r, r, r * 2.7f, r * 2.7f, paint);

    }


我们重写了View的onDraw方法,首先将View的背景色设置为绿色,然后绘制了一个黄色的圆形,然后再绘制一个蓝色的矩形,效果如下所示:


上面演示就是Canvas正常的绘图流程,没有使用PorterDuffXfermode。我们简单分析一下上面这段代码:


  1. 首先,我们调用了canvas.drawARGB(255, 139, 197, 186)方法将整个Canvas都绘制成一个颜色,在执行完这句代码后,canvas上所有像素的颜色值的ARGB颜色都是(255,139,197,186),由于像素的alpha分量是255而不是0,所以此时所有像素都不透明。

  2. 当我们执行了canvas.drawCircle(r, r, r, paint)之后,Android会在所画圆的位置用黄颜色的画笔绘制一个黄色的圆形,此时整个圆形内部所有的像素颜色值的ARGB颜色都是0xFFFFCC44,然后用这些黄色的像素替换掉Canvas中对应的同一位置中颜色值ARGB为(255,139,197,186)的像素,这样就将黄色圆形绘制到Canvas上了。

  3. 当我们执行了canvas.drawRect(r, r, r * 2.7f, r * 2.7f, paint)之后,Android会在所画矩形的位置用蓝色的画笔绘制一个蓝色的矩形,此时整个矩形内部所有的像素颜色值的ARGB颜色都是0xFF66AAFF,然后用这些蓝色的像素替换掉Canvas中对应的同一位置中的像素,这样黄色的圆中的右下角部分的像素与其他一些背景色像素就被蓝色像素替换了,这样就将蓝色矩形绘制到Canvas上了。


上述过程虽然简单,但是了解Canvas绘图时具体的像素更新过程是真正理解PorterDuffXfermode的工作原理的基础。




示例二


下面我们使用PorterDuffXfermode对上面的代码进行一下修改,修改后的代码如下所示:


@Override

    protected void onDraw(Canvas canvas) {

        super.onDraw(canvas);

        //设置背景色

        canvas.drawARGB(255, 139, 197, 186);

 

        int canvasWidth = canvas.getWidth();

        int r = canvasWidth / 3;

         //正常绘制黄色的圆形

        paint.setColor(0xFFFFCC44);

        canvas.drawCircle(r, r, r, paint);

        //使用CLEAR作为PorterDuffXfermode绘制蓝色的矩形

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

        paint.setColor(0xFF66AAFF);

        canvas.drawRect(r, r,  r * 2.7f, r * 2.7f, paint);

        //最后将画笔去除Xfermode

        paint.setXfermode(null);

    }


效果如下所示:


下面我们对以上代码进行一下分析:


  1. 首先,我们调用了canvas.drawARGB(255, 139, 197, 186)方法将整个Canvas都绘制成一个颜色,此时所有像素都不透明。

  2. 然后我们通过调用canvas.drawCircle(r, r, r, paint)绘制了一个黄色的圆形到Canvas上面。

  3. 然后我们执行代码paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)),将画笔的PorterDuff模式设置为CLEAR。

  4. 然后调用canvas.drawRect(r, r, r * 2.7f, r * 2.7f, paint)方法绘制蓝色的矩形,但是最终界面上出现了一个白色的矩形。

  5. 在绘制完成后,我们调用paint.setXfermode(null)将画笔去除Xfermode。


我们具体分析一下白色矩形出现的原因。一般我们在调用canvas.drawXXX()方法时都会传入一个画笔Paint对象,Android在绘图时会先检查该画笔Paint对象有没有设置Xfermode,如果没有设置Xfermode,那么直接将绘制的图形覆盖Canvas对应位置原有的像素;如果设置了Xfermode,那么会按照Xfermode具体的规则来更新Canvas中对应位置的像素颜色。就本例来说,在执行canvas.drawCirlce()方法时,画笔Paint没有设置Xfermode对象,所以绘制的黄色圆形直接覆盖了Canvas上的像素。当我们调用canvas.drawRect()绘制矩形时,画笔Paint已经设置Xfermode的值为PorterDuff.Mode.CLEAR,此时Android首先是在内存中绘制了这么一个矩形,所绘制的图形中的像素称作源像素(source,简称src),所绘制的矩形在Canvas中对应位置的矩形内的像素称作目标像素(destination,简称dst)。源像素的ARGB四个分量会和Canvas上同一位置处的目标像素的ARGB四个分量按照Xfermode定义的规则进行计算,形成最终的ARGB值,然后用该最终的ARGB值更新目标像素的ARGB值。本例中的Xfermode是PorterDuff.Mode.CLEAR,该规则比较简单粗暴,直接要求目标像素的ARGB四个分量全置为0,即(0,0,0,0),即透明色,所以我们通过canvas.drawRect()在Canvas上绘制了一个透明的矩形,由于Activity本身屏幕的背景时白色的,所以此处就显示了一个白色的矩形。




示例三


我们在对示例二中的代码进行一下修改,将绘制圆形和绘制矩形相关的代码放到canvas.saveLayer()和canvas.restoreToCount()之间,代码如下所示:


@Override

    protected void onDraw(Canvas canvas ) {

        super.onDraw(canvas);

        //设置背景色

        canvas.drawARGB(255, 139, 197, 186);

 

        int canvasWidth = canvas.getWidth();

        int canvasHeight = canvas.getHeight();

        int layerId = canvas.saveLayer(0,  0, canvasWidth, canvasHeight, null, Canvas.ALL_SAVE_FLAG);

            int r = canvasWidth / 3;

            //正常绘制黄色的圆形

            paint.setColor(0xFFFFCC44);

            canvas.drawCircle(r, r, r, paint);

            //使用CLEAR作为PorterDuffXfermode绘制蓝色的矩形

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

            paint.setColor(0xFF66AAFF);

            canvas.drawRect(r, r, r * 2.7f, r * 2.7f, paint);

            //最后将画笔去除Xfermode

            paint.setXfermode(null);

        canvas.restoreToCount(layerId);

     }


效果如下所示:


下面对上述代码进行一下分析:


  1. 首先,我们调用了canvas.drawARGB(255, 139, 197, 186)方法将整个Canvas都绘制成一个颜色,此时所有像素都不透明。

  2. 然后我们将主要的代码都放到了canvas.saveLayer()以及canvas.restoreToCount()之间,并且我将代码缩进了一下。当我们把代码写在canvas.saveXXX()与canvas.restoreXXX()之间时,建议把里面的代码缩进,这样的写法便于代码可读,当然代码缩进与否不是强制性的,也不会影响运行效果。


关于canvas绘图中的layer有以下几点需要说明:


  • canvas是支持图层layer渲染这种技术的,canvas默认就有一个layer,当我们平时调用canvas的各种drawXXX()方法时,其实是把所有的东西都绘制到canvas这个默认的layer上面。

  • 我们还可以通过canvas.saveLayer()新建一个layer,新建的layer放置在canvas默认layer的上部,当我们执行了canvas.saveLayer()之后,我们所有的绘制操作都绘制到了我们新建的layer上,而不是canvas默认的layer。

  • 用canvas.saveLayer()方法产生的layer所有像素的ARGB值都是(0,0,0,0),即canvas.saveLayer()方法产生的layer初始时时完全透明的。

  • canvas.saveLayer()方法会返回一个int值,用于表示layer的ID,在我们对这个新layer绘制完成后可以通过调用canvas.restoreToCount(layer)或者canvas.restore()把这个layer绘制到canvas默认的layer上去,这样就完成了一个layer的绘制工作。


那你可能感觉到很奇怪,我们只是将绘制圆形与矩形的代码放到了canvas.saveLayer()和canvas.restoreToCount()之间,为什么不再像示例二那样显示白色的矩形了?


我们在分析示例二代码时知道了最终矩形区域的目标颜色都被重置为透明色(0,0,0,0)了,最后只是由于Activity背景色为白色,所以才最终显示成白色矩形。在本例中,我们在新建的layer上面绘制完成后,其实矩形区域的目标颜色也还是被重置为透明色(0,0,0,0)了,这样整个新建layer只有圆的3/4不是透明的,其余像素全是透明的,然后我们调用canvas.restoreToCount()将该layer又绘制到了Canvas上面去了。在将一个新建的layer绘制到Canvas上去时,Android会用整个layer上面的像素颜色去更新Canvas对应位置上像素的颜色,并不是简单的替换,而是Canvas和新layer进行Alpha混合,可参见此处链接。由于我们的layer中只有两种像素:完全透明的和完全不透明的,不存在部分透明的像素,并且完全透明的像素的颜色值的四个分量都为0,所以本例就将Canvas和新layer进行Alpha混合的规则简化了,具体来说:


  • 如果新建layer上面某个像素的Alpha分量为255,即该像素完全不透明,那么Android会直接用该像素的ARGB值作为Canvas对应位置上像素的颜色值。

  • 如果新建layer上面某个像素的Alpha分量为0,即该像素完全透明,在本例中Alpha分量为0的像素,其RGB分量也都为0,那么Android会保留Canvas对应位置上像素的颜色值。


这样当将新layer绘制到Canvas上时,完全不透明的3/4黄色圆中的像素会完全覆盖Canvas对应位置的像素,而由于在新layer上面绘制的矩形区域的像素ARGB都为(0,0,0,0),所以最终Canvas上对应矩形区域还是保持之前的背景色,这样就不会出现白色的矩形了。


大部分情况下,我们想要本例中实现的效果,而不是想要示例二中形成的白色矩形,所以大部分情况下在使用PorterDuffXfermode时都是结合canvas.saveLayer()、canvas.restoreToCount()的,将关键代码写在这两个方法之间。




一张被不经大脑疯传的神图


如果大家Google或百度PorterDuffXfermode相关的博文,大家肯定会看到下面这张神图,如下所示:


这张图是Android的sdk下自带的API的Demo示例中的一个,其源码对应的物理路径是C:\Users\iSpring\AppData\Local\Android\sdk\samples\android-23\legacy\ApiDemos\src\com\example\android\apis\graphics\Xfermodes.java。


这张图演示了先绘制黄色的圆形,然后将画笔paint设置为16种不同的PorterDuffXfermode,然后再绘制蓝色矩形的效果。


上面的效果看起来貌似很正常,但是我想说的是这张被疯传了的上图对开发者极具误导性,该图的出发点是好的,其想直观表达多种PorterDuffXfermode的效果,为了实现这个目的其在该代码中对所绘制的黄色图形和蓝色图形都做了手脚。


其代码中创建黄色圆形的代码如下所示:


// 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);





请到「今天看啥」查看全文