Android自定义系列——6.PorterDuffXfermode

2024-06-24 05:18

本文主要是介绍Android自定义系列——6.PorterDuffXfermode,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

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

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。

@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.drawARGB(255, 139, 197, 186)方法将整个Canvas都绘制成一个颜色,在执行完这句代码后,canvas上所有像素的颜色值的ARGB颜色都是(255,139,197,186),由于像素的alpha分量是255而不是0,所以此时所有像素都不透明。

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

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

在这里插入图片描述对以上代码进行一下分析:
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有以下几点需要说明:
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()之间,为什么不再像示例二那样显示白色的矩形了?

我们在分析示例二代码时知道了最终矩形区域的目标颜色都被重置为透明色(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上对应矩形区域还是保持之前的背景色,这样就不会出现白色的矩形了。

在这里插入图片描述
图是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" imagestatic 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;}

上面的代码首先通过指定尺寸,new了一个Bitmap对象,这样的Bitmap对象默认所有的像素的颜色都是(0,0,0,0),由于每个像素的alpha分量都默认为0,所以整个Bitmap都是透明的。然后用该Bitmap构造了一个Canvas对象,这样当执行c.drawOval()时,会将黄色的圆形绘制到Canvas上,并自动将Canvas上的图形更新到Canvas所绑定的之前创建的Bitmap上。这样,Bitmap中的像素有两种,一种是位于圆形范围内的像素,其像素值为0xFFFFCC44,另一种是位于圆形范围外的像素,其像素值为0x00000000,也就是说该Bitmap中的黄色圆形区域是不透明的,其余范围是透明的。最后将这个Bitmap对象返回,这样可以在onDraw()方法中通过canvas.drawBitmap()以绘制黄色的圆形。大家注意看c.drawOval()中的RecF参数,right是w3/4,而不是w,bottom是h3/4,而不是h。这说明什么呢?这说明该Bitmap实际的大小要比你在上图中看到的黄色圆形区域要大,两者尺寸不一致。

创建蓝色矩形的代码如下所示:

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

创建蓝色矩形的代码与创建黄色圆形的代码很相似。大家注意观察c.drawRect()中的参数,left的值为w/3,而不是0,top的值是h/3,而不是0,right的值为w19/20,而不是w,bottom的值是h19/20,而不是h。这说明什么呢?这说明该Bitmap实际的大小要比你在上图中看到的蓝色矩形区域要大,两者尺寸不一致。

经过上面的分析我们知道,API Demo中所绘制的Bitmap的实际大小都比我们肉眼看到的实际大小要大,这导致了上图结果会给开发者带来很大困惑。我举个例子,比如当设置的PorterDuffXfermode中的值为CLEAR时,API Demo中肉眼看到的结果是整个圆形都不可见了,其实这是不对的,因为如果makeDst()、makeSrc()方法所得到的Bitmap的实际大小与所画的圆、矩形实际大小相同,那么效果应该是只有圆与矩形相交的圆的右下角的部分被裁剪成透明的了,圆的其他3/4的部分都应该还可见;再比如当设置的PorterDuffXfermode中的值为SRC时,API Demo中肉眼看到的结果是绘制的黄色的圆形完全不可见,绘制的蓝色的矩形完全可见,其实这是不对的,因为如果makeDst()、makeSrc()方法所得到的Bitmap的实际大小与所画的圆、矩形实际大小相同,那么效果应该是所绘制的黄色的圆形可见,所绘制的蓝色的矩形也可见,只不过圆形和矩形相交的区域是蓝色的,即正确的效果应该是蓝色矩形压盖了黄色圆形。

正确的图应该是:
在这里插入图片描述
上面的例子演示了了16种混合模式的效果,并且关键代码都放在了canvas.saveLayer()与canvas.restoreToCount()之间

我们知道一个像素的颜色由四个分量组成,即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 - Sa*Da, Sc*(1 - Da) + Dc*(1 - Sa) + min(Sc, Dc)]LIGHTEN:[Sa + Da - Sa*Da, 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自定义系列——6.PorterDuffXfermode的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1089260

相关文章

ROS话题通信流程自定义数据格式

ROS话题通信流程自定义数据格式 需求流程实现步骤定义msg文件编辑配置文件编译 在 ROS 通信协议中,数据载体是一个较为重要组成部分,ROS 中通过 std_msgs 封装了一些原生的数据类型,比如:String、Int32、Int64、Char、Bool、Empty… 但是,这些数据一般只包含一个 data 字段,结构的单一意味着功能上的局限性,当传输一些复杂的数据,比如:

Eclipse+ADT与Android Studio开发的区别

下文的EA指Eclipse+ADT,AS就是指Android Studio。 就编写界面布局来说AS可以边开发边预览(所见即所得,以及多个屏幕预览),这个优势比较大。AS运行时占的内存比EA的要小。AS创建项目时要创建gradle项目框架,so,创建项目时AS比较慢。android studio基于gradle构建项目,你无法同时集中管理和维护多个项目的源码,而eclipse ADT可以同时打开

android 免费短信验证功能

没有太复杂的使用的话,功能实现比较简单粗暴。 在www.mob.com网站中可以申请使用免费短信验证功能。 步骤: 1.注册登录。 2.选择“短信验证码SDK” 3.下载对应的sdk包,我这是选studio的。 4.从头像那进入后台并创建短信验证应用,获取到key跟secret 5.根据技术文档操作(initSDK方法写在setContentView上面) 6.关键:在有用到的Mo

android一键分享功能部分实现

为什么叫做部分实现呢,其实是我只实现一部分的分享。如新浪微博,那还有没去实现的是微信分享。还有一部分奇怪的问题:我QQ分享跟QQ空间的分享功能,我都没配置key那些都是原本集成就有的key也可以实现分享,谁清楚的麻烦详解下。 实现分享功能我们可以去www.mob.com这个网站集成。免费的,而且还有短信验证功能。等这分享研究完后就研究下短信验证功能。 开始实现步骤(新浪分享,以下是本人自己实现

Android我的二维码扫描功能发展史(完整)

最近在研究下二维码扫描功能,跟据从网上查阅的资料到自己勉强已实现扫描功能来一一介绍我的二维码扫描功能实现的发展历程: 首页通过网络搜索发现做android二维码扫描功能看去都是基于google的ZXing项目开发。 2、搜索怎么使用ZXing实现自己的二维码扫描:从网上下载ZXing-2.2.zip以及core-2.2-source.jar文件,分别解压两个文件。然后把.jar解压出来的整个c

android 带与不带logo的二维码生成

该代码基于ZXing项目,这个网上能下载得到。 定义的控件以及属性: public static final int SCAN_CODE = 1;private ImageView iv;private EditText et;private Button qr_btn,add_logo;private Bitmap logo,bitmap,bmp; //logo图标private st

Android多线程下载见解

通过for循环开启N个线程,这是多线程,但每次循环都new一个线程肯定很耗内存的。那可以改用线程池来。 就以我个人对多线程下载的理解是开启一个线程后: 1.通过HttpUrlConnection对象获取要下载文件的总长度 2.通过RandomAccessFile流对象在本地创建一个跟远程文件长度一样大小的空文件。 3.通过文件总长度/线程个数=得到每个线程大概要下载的量(线程块大小)。

时间服务器中,适用于国内的 NTP 服务器地址,可用于时间同步或 Android 加速 GPS 定位

NTP 是什么?   NTP 是网络时间协议(Network Time Protocol),它用来同步网络设备【如计算机、手机】的时间的协议。 NTP 实现什么目的?   目的很简单,就是为了提供准确时间。因为我们的手表、设备等,经常会时间跑着跑着就有误差,或快或慢的少几秒,时间长了甚至误差过分钟。 NTP 服务器列表 最常见、熟知的就是 www.pool.ntp.org/zo

JavaWeb系列二十: jQuery的DOM操作 下

jQuery的DOM操作 CSS-DOM操作多选框案例页面加载完毕触发方法作业布置jQuery获取选中复选框的值jQuery控制checkbox被选中jQuery控制(全选/全不选/反选)jQuery动态添加删除用户 CSS-DOM操作 获取和设置元素的样式属性: css()获取和设置元素透明度: opacity属性获取和设置元素高度, 宽度: height(), widt

高仿精仿愤怒的小鸟android版游戏源码

这是一款很完美的高仿精仿愤怒的小鸟android版游戏源码,大家可以研究一下吧、 为了报复偷走鸟蛋的肥猪们,鸟儿以自己的身体为武器,仿佛炮弹一样去攻击肥猪们的堡垒。游戏是十分卡通的2D画面,看着愤怒的红色小鸟,奋不顾身的往绿色的肥猪的堡垒砸去,那种奇妙的感觉还真是令人感到很欢乐。而游戏的配乐同样充满了欢乐的感觉,轻松的节奏,欢快的风格。 源码下载