Canvas和PorterDuffXfermode的秘密

前要:此篇主要以Android举例,iOS可以参考CGBlendMode

Canvas

绘制四要素:
1个Bitmap用来承载像素信息,
1个绘图基元(例如,Rect,Path,text,Bitmap),
1个Canvas用来管理Draw相关方法(把绘图基元写入Bitmap中),
1个画笔(用于描绘图像的颜色和风格)。
这和我们日常理解的绘画异曲同工,Bitmap作为画布,Canvas管理着绘画的手法,绘图基元代表着要绘制的目标,Paint就是你手里的画笔和颜料。

获取Canvas
1.onDraw方法的入口参数就是Canvas,直接可以使用,而我们操作这个Canvas最终的效果会反应在这个View上。
2.新建Canvas。1个Canvas对象一定要结合1个Bitmap对象,所以一定要为新建的Canvas对象设置1个Bitmap对象。但是要注意,该bitmap一定要是mutable(可变的)

Bitmap.createBitmap(mWidth, mHeight, Config.ARGB_8888) ;
或者
BimtapFactory.decodeResource().copy(configu_argb_8888, true);
(BimapFactory.decodeResource() 得到的mutable 为false)

Canvas绘制
Canvas内部维持了一个mutable Bitmap,所以我们有一系列方法:
drawRGB(int r, int g, int b)
drawARGB(int a, int r, int g, int b)
drawColor(int color)
drawColor(int color, PorterDuff.Mode mode)
drawPaint(Paint paint)
绘制图形
canvas.drawArc (扇形)
canvas.drawCircle(圆)
canvas.drawOval(椭圆)
canvas.drawLine(线)
canvas.drawPoint(点)
canvas.drawRect(矩形)
canvas.drawRoundRect(圆角矩形)
canvas.drawVertices(顶点)
cnavas.drawPath(路径)
绘制图片
canvas.drawBitmap(位图)
canvas.drawPicture(图片)
文本
canvas.drawText

Canvas变换
Canvas不仅仅可以draw一些图形、图片,其本身也提供了可操作的方法:rorate(旋转)、scale(压缩)、translate(平移)、skew(扭曲)等。

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

    canvas.drawRect(new Rect(0, 0, 200, 200), new Paint());
    canvas.scale(0.5f, 0.5f);//缩放了
    canvas.drawRect(new Rect(400, 400, 600, 600), new Paint());

    canvas.translate(600, 600);//平移了
    canvas.rotate(45);//旋转了
    canvas.drawRect(new Rect(0, 0, 200, 200), new Paint());

    canvas.translate(200, 200);
    canvas.skew(.5f, .5f);//扭曲了
    canvas.drawRect(new Rect(0, 0, 200, 200), new Paint());
}

Canvas保存和回滚
如绘制表盘:

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

 for (int i = 0; i < 360; i = i + 6) {
     canvas.save();
     canvas.rotate(i, 100, 100);
     canvas.drawLine(100, 0, 100, 10, new Paint());
     canvas.restore();
 }
}

PorterDuffXfermode

概述

PorterDuffXfermode extends Xfermode。它将所绘制的图形的像素与Canvas中对应位置的像素按照一定规则进行混合,形成新的像素值,从而更新Canvas中最终的像素颜色值。

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

我们下面会分析几个代码片段研究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对上面的代码进行一下修改,修改后的代码如下所示:

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

效果如下所示:

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

在canvas.drawRect()之前setXfermode,那么所绘制的矩形中的像素称作源像素(Src),所绘制的矩形在Canvas中对应位置的矩形内的像素称作目标像素(Dst)。
即根据Xfermode的规则,Src是绘制内容像素,Dst是Canvas像素。Src和Dst的ARGB值会重新计算。
本例中Xfermode是PorterDuff.Mode.CLEAR,直接将目标像素的ARGB四个分量全置为0,即(0,0,0,0),即透明色,所以实际上绘制了一个透明的矩形。但是效果图为什么是白色的呢,因为屏幕本身是白色的。

示例三

我们在对示例二中的代码进行一下修改,将绘制圆形和绘制矩形相关的代码放到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);
    }

效果如下所示:

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

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

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

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

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

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

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

在将一个新建的layer绘制到Canvas上去时,Android会用整个layer上面的像素颜色去更新Canvas对应位置上像素的颜色,并不是简单的替换,而是Canvas和新layer进行Alpha混合,可参见此处链接

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

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

这张图是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,然后再绘制蓝色矩形的效果。

但是上图是错误的,它实现这个效果是在该代码中对所绘制的黄色图形和蓝色图形大小都做了手脚。

不同混合模式的计算规则

为了方便观察对比,整个View的背景设置为绿色,最终运行效果应该是如下所示:

上面的例子演示了了16种混合模式的效果,并且关键代码都放在了canvas.saveLayer()与canvas.restoreToCount()之间。DST是黄色圆,SRC是蓝色矩形,PorterDuffXfermode公式应用于该Rectangle(canvas.drawRect())区域。

我们知道一个像素的颜色由四个分量组成,即ARGB,第一个分量A表示的是Alpha值,后面三个分量RGB表示了颜色。我们用S代表源像素,源像素的颜色值可表示为[Sa, Sc],Sa中的a是alpha的缩写,Sa表示源像素的Alpha值,Sc中的c是颜色color的缩写,Sc表示源像素的RGB。我们用D代表目标像素,目标像素的颜色值可表示为[Da, Dc],Da表示目标像素的Alpha值,Dc表示目标像素的RGB。

源像素与目标像素在不同混合模式下计算颜色的规则如下所示:

CLEAR:[0, 0]所绘制不会提交到画布上

SRC:[Sa, Sc] 显示上层绘制图片

DST:[Da, Dc]显示下层绘制图片

SRC_OVER:[Sa + (1 – Sa)Da, Rc = Sc + (1 – Sa)Dc]正常绘制显示,上下层绘制叠盖。

DST_OVER:[Sa + (1 – Sa)Da, Rc = Dc + (1 – Da)Sc]上下层都显示。下层居上显示。

SRC_IN:[Sa * Da, Sc * Da] 取两层绘制交集。显示上层。

DST_IN:[Sa * Da, Sa * Dc] 取两层绘制交集。显示下层。

SRC_OUT:[Sa * (1 – Da), Sc * (1 – Da)] 取上层绘制非交集部分。

DST_OUT:[Da * (1 – Sa), Dc * (1 – Sa)] 取下层绘制非交集部分。

SRC_ATOP:[Da, Sc * Da + (1 – Sa) * Dc] 取下层非交集部分与上层交集部分

DST_ATOP:[Sa, Sa * Dc + Sc * (1 – Da)] 取上层非交集部分与下层交集部分

XOR:[Sa + Da – 2 * Sa * Da, Sc * (1 – Da) + (1 – Sa) * Dc]

DARKEN:[Sa + Da – SaDa, Sc(1 – Da) + Dc*(1 – Sa) + min(Sc, Dc)]

LIGHTEN:[Sa + Da – SaDa, Sc(1 – Da) + Dc*(1 – Sa) + max(Sc, Dc)]

MULTIPLY:[Sa * Da, Sc * Dc]

SCREEN:[Sa + Da – Sa * Da, Sc + Dc – Sc * Dc]

ADD:Saturate(S + D)

OVERLAY:Saturate(S + D)

最后需要说明一下,DARKEN、LIGHTEN、OVERLAY等几种混合规则在GPU硬件加速下不起效,如果你觉得混合模式没有正确使用,可以让调用View.setLayerType(View.LAYER_TYPE_SOFTWARE, null)方法,把我们的View禁用掉GPU硬件加速,切换到软件渲染模式,这样所有的混合模式都能正常使用了,具体可参见博文《Android中GPU硬件加速控制及其在2D图形绘制上的局限》

最后总结一下,PorterDuffXfermode用于实现新绘制的像素与Canvas上对应位置已有的像素按照混合规则进行颜色混合。

参考:
Android中Canvas绘图之PorterDuffXfermode使用及工作原理详解
Android中Canvas绘图基础详解(附源码下载)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,911评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,014评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,129评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,283评论 1 264
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,159评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,161评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,565评论 3 382
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,251评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,531评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,619评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,383评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,255评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,624评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,916评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,199评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,553评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,756评论 2 335

推荐阅读更多精彩内容