此文已由作者游葳授权网易云社区发布。

欢迎访问网易云社区,了解更多网易技术产品运营经验。

写在开头

随着应用开发的深入,视觉同学在完成了页面的基本设计后,再也按耐不住心中的寂寞,开始对各种细节不满意,于是乎就会提出各种视觉优化的方案。作为开发人员,啥也别说了,你懂的,有困难要上,没困难,制造困难也要上。既然是优化提升的方案,那很多时候只使用系统提供的各种控件,或者只是简单的用Paint去进行图形颜色的绘制,已经满足不了视觉同志的胃口了,这就要求我们必须掌握Paint的进阶技巧,比如本文介绍的图像混合技术 - PorterDuffXfermode。

PorterDuffXfermode 简介

相信很多android开发同学和我一样,第一次看到这个ProterDuff单词都会觉得奇怪,这是个啥子意思呢。作为一个猪场员工,我当然是立刻马上用有道词典翻译了一下,结果啥也没搜出来。后来上网查了才知道,ProterDuff是两个人名的组合: Tomas Proter和 Tom Duff. 这两个人在1984年一起写了一篇名为《Compositing Digital Images》的论文。我们知道,一个像素是由ARGB四个分量组成的,该论文就论述了如何实现不同数字图像的像素之间是如何进行混合的,并提出了多种像素混合的模式。PorterDuffXfermode支持以下十几种像素颜色的混合模式,分别为:

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];说明:混合

ADD      计算方式:Saturate(S + D);说明:饱和度相加

S代表源像素,源像素的颜色值表示为[Sa, Sc],Sa中的a是alpha的缩写,Sa表示源像素的Alpha值,Sc中的c是颜色color的缩写,Sc表示源像素的RGB。D代表目标像素,目标像素的颜色值表示为[Da, Dc],Da表示目标像素的Alpha值,Dc表示目标像素的RGB。

合成后[]逗号前面的这一部分的值代表计算后的Alpha通道,而逗号后的这一部分的值代表计算后的颜色值,图形混合后的图片依靠这个矢量来计算ARGB的值。

一张容易被误解的神图

相信很多人在用到PorterDuffXfermode的时候都有看过这张图吧,这张图是Android的sdk下自带的API的Demo示例。但是如果按照这张图的示例进行开发的话,有时可能会达不到预期效果。比如第一种的CLEAR效果,乍一看该图,CLEAR达到的效果应该是把dst和src的图片全部都清空了,但这个其实是不对的,因为PorterDuffXfermode 的机制就是src与dst进行各种混合变化,在超出src范围内的区域是不起作用的,所以CLEAR只是把src所包含部分清除了,但是在图上看了,却是整个图层上啥都没有了,这个又是为什么呢?

这个秘密就藏在Demo的源码中,打开位于/Users/netease/Library/Android/sdk/samples/android-19/legacy/ApiDemos/src/com/example/android/apis/graphics目录下的Xfermodes.java文件,示例中创建dst和src图片的源码如下:

  1.     // create a bitmap with a circle, used for the "dst" image
  2.     static Bitmap makeDst(int w, int h) {
  3.         Bitmap bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
  4.         Canvas c = new Canvas(bm);
  5.         Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
  6.  
  7.         p.setColor(0xFFFFCC44);
  8.         c.drawOval(new RectF(0, 0, w*3/4, h*3/4), p);        return bm;
  9.     }
  1.     // create a bitmap with a rect, used for the "src" image
  2.     static Bitmap makeSrc(int w, int h) {
  3.         Bitmap bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
  4.         Canvas c = new Canvas(bm);
  5.         Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
  6.  
  7.         p.setColor(0xFF66AAFF);
  8.         c.drawRect(w/3, h/3, w*19/20, h*19/20, p);        return bm;
  9.     }

发现了吗?原来在示例中创建的dst和src的大小,不只是我们从图中看到的只是那两个圆圈和方块而已,而是整个图的范围,黄色和蓝色的区域其实只是dst和src的一部分而已,只是其他部分是透明的,让人容易误以为dst和src就那么大而已。所以,当都是w * h范围的src和dst用CLEAR模式进行混合后,才会出现全部都没有的效果。

假如就以实际看到的两个区域作为dst和src的大小来进行演示,效果又会是怎么样的呢?修改后的混合效果如下( 为了方便观察比较,我将填满整个页面充当背景的View的颜色设置成白色,每一个显示效果的小View的背景色设置为绿色):

    

可以看到,当dst和src都只是圆圈和方块大小是,CLEAR模式下就仅仅只是把方块区域给清除了,这个才是CLEAR的真实效果。那为什么上下两张图的清除效果又会有所差异呢,一个是显示当前View的绿色背景,一个则直接显示了底层View的白色背景呢?

嘿嘿,原因就在于对canvas图层(layer)的使用。第一个图的流程是先绘制绿色背景,然后再调用saveLayer新生成一个图层进行dst和src的CLEAR操作,操作完后调用restore退出该图层,将该图层合成到原图层上。第二个图的流程是首先生成一个新图层,然后在图层上绘制绿色背景,进行dst和src的操作,这个时候由于绿色背景和dst,src是处于同一layer,因此CLEAR操作会将该layer上方块区域的RGB色值全部设置为0,再将该layer合成到原图层上后,就把底下的白色背景显示出来了(详细代码可见附件)。很多时候我们应该需要的是第一个图的效果,因此在操作的时候就要使用saveLayer、restoreToCount的方法把混合操作放在新图层上进行了。

一个食栗

有一天,做视觉的胖大叔(是的,负责给我们做视觉的是个大叔,原来的妹纸被拉去做官网视觉了。。。)跑过来给我说,诶,这个意见反馈发送图片的效果要改啊,要改的和微信的效果一样(微信聊天界面发送图片是什么效果,我觉得我就不用上图了吧)。额,是不是做聊天的都要向微信学(抄)啊,嘿嘿。好吧,原来那种简单的直接给ImageView添加一个背景的方式是用不了咯,想想怎么搞吧。

一开始的想法是利用剪裁的方式,把图片按照背景泡泡图片的尺寸进行裁剪,但是这个计算就比较累,而且那个尖角是什么鬼?这个要怎么计算。在纸上画了半天了,突然脑子里闪过“ PorterDuffXfermode”(好吧,其实当时肯定拼错了)几个字,哈哈,终于找到了解决问题的那把key了。于是乎,赶紧上网复习了一下PorterDuffXfermode的相关资料,确定了应该要使用的是SRC_IN的模式,将泡泡图片作为dst,需要显示的图片作为src,保证这两者长宽一致,那合成后就是具有泡泡形状的图片了。想清楚了就直接开始实践,最终完美的达到了胖大叔的要求:

具体实现流程如下:

1 自定义一个继承自ImageView的控件OverlapImageView,因为ImageView里面正好可以配置背景图片(dst)和前景图片(src)。由于我们使用的背景泡泡图是一张.9图片,因此初始化的时候如果背景是.9图,还需要进行一下处理:

  1.         final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.OverlapImageView,defStyleAttr,0);        boolean isNinePatch = a.getBoolean(R.styleable.OverlapImageView_isNinePatch,false);        final int srcResId = a.getResourceId(R.styleable.OverlapImageView_dst, 0);        final TypedValue value = new TypedValue();        final Resources r = a.getResources();        try {            final InputStream is = r.openRawResource(srcResId, value);            final BitmapFactory.Options options = new BitmapFactory.Options();
  2.             options.inScreenDensity = (int) r.getDisplayMetrics().scaledDensity;            final Rect padding = new Rect();
  3.             dstBp = BitmapFactory.decodeResourceStream(r, value, is, padding, options);
  4.             is.close();            if (isNinePatch && dstBp != null){
  5.                 mNinePatch = new NinePatch(dstBp,dstBp.getNinePatchChunk(),null);
  6.             }
  7.         } catch (IOException e) {            // Ignore
  8.             e.printStackTrace();
  9.             Drawable d = a.getDrawable(R.styleable.OverlapImageView_dst);
  10.             dstBp = drawableToBitmap(d);
  11.         }

2 复写onDraw()方法:

  1.     @Override
  2.     protected void onDraw(Canvas canvas) {        if(srcBp != null && (dstBp != null || mNinePatch != null)){            int width = getRight() - getLeft() > 0 ? getRight() - getLeft() : srcBp.getWidth();            int height = getBottom() - getTop() > 0 ? getBottom() - getTop() :srcBp.getHeight();            //1.创建一个新图层Layer进行效果合成
  3.             int sc = canvas.saveLayer(0,0,width,height,null,Canvas.ALL_SAVE_FLAG);
  4.             Rect r = new Rect(0,0,width,height);            //2.绘制DST
  5.             if (mNinePatch != null){
  6.                 mNinePatch.draw(canvas,r,mPaint);
  7.             }else {
  8.                 canvas.drawBitmap(dstBp,null,r,mPaint);
  9.             }            //3.设置混合模式,一旦调用该方法,当前Layer上的内容会被作为DST
  10.             mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));            //4.绘制SRC
  11.             canvas.drawBitmap(srcBp,null,r,mPaint);
  12.             mPaint.setXfermode(null);            //5.当前Layer退栈,将其内容保存到canvas默认的Layer上
  13.             canvas.restoreToCount(sc);
  14.         }else {            super.onDraw(canvas);
  15.         }
  16.     }

需要注意的一点就是一旦调用setXfermode()方法后,当前Layer上的内容就会被当做dst的内容进行处理。canvas默认是自带了一个Layer,因此如果没有调用saveLayer(),那当前canvas上的所有内容都是dst了。因此必须搞清楚哪些内容是dst,不然的话合成出来的就可能达不到预期效果了。

另一个栗子

又过了几天,胖大叔又呼哧呼哧的跑过来找我,说是那个粉丝榜的进度条效果不好看,要改!然后他就给我发了一张具有指导性意见的图片,就按这个效果改啊:

有了上次的经验,这个我略微思索,掐指一算,嗯,你小子不就是XOR模式吗,当然,还需要对progressBar进行一下改造,默认的进度条是无法显示文字的。大致的实现思路就是先新建一个Layer,绘制ProgressBar的边框,绘制进度条,然后把这两个图像作为dst,再绘制文字作为src,最后使用XOR模式进行合成,就可以达到上面的效果啦,具体代码如下:

1 新建进度条背景的xml文件:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
  3.     <item android:id="@android:id/background">
  4.         <shape android:shape="rectangle">
  5.             <corners android:radius="3dp"></corners>
  6.             <stroke android:width="1px"
  7.             android:color="@color/bg_color_edc550"></stroke>
  8.         </shape>
  9.     </item>
  10.     <item android:id="@android:id/progress">
  11.         <clip>
  12.             <shape android:shape="rectangle">
  13.                 <corners android:radius="3dp"></corners>
  14.                 <solid android:color="@color/bg_color_ffd53a"/>
  15.             </shape>
  16.         </clip>
  17.     </item>
  18. </layer-list>

2 自定义一个控件继承ProgressBar,然后复写onDraw()方法:

  1.     @Override
  2.     protected synchronized void onDraw(Canvas canvas) {        if (mText.length() == 0){            super.onDraw(canvas);            return;
  3.         }else if (!mRevertMode){            super.onDraw(canvas);
  4.             drawText(canvas);            return;
  5.         }        //1.新建一个图层Layer
  6.         int sc = canvas.saveLayer(0,0,getMeasuredWidth(),getMeasuredHeight(),null,Canvas.ALL_SAVE_FLAG);        //2. 绘制背景边框
  7.         final Drawable backgound = getBackground();        if (backgound != null){
  8.             backgound.draw(canvas);
  9.         }        //3. 绘制进度条
  10.         final Drawable d = getProgressDrawable();        if(!= null){            final int saveCount = canvas.save();
  11.             d.draw(canvas);
  12.             canvas.restoreToCount(saveCount);            if (mText.length() > 0){                //4. 设置混合模式XOR,
  13.                 mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.XOR));                //5. 绘制进度文字
  14.                 drawText(canvas);
  15.                 mPaint.setXfermode(null);
  16.             }
  17.         }        //6.退栈,将效果合成到canvas中
  18.         canvas.restoreToCount(sc);
  19.     }

最终的效果如下:

TIPS:

假如你设置的混合模式没有生效,试着关闭一下硬件加速功能。

免费体验云安全(易盾)内容安全、验证码等服务

更多网易技术、产品、运营经验分享请点击

相关文章:
【推荐】 PaaS服务之路漫谈(三)
【推荐】 云计算交互设计师的正确出装姿势

PorterDuffXfermode 图像混合技术在漫画APP中的应用的更多相关文章

  1. 客户端相关知识学习(一)之混合开发,为什么要在App中使用H5页面以及应用场景、注意事项

    混合开发 随着移动互联网的高速发展,常规的开发速度已经渐渐不能满足市场需求.原生H5混合开发应运而生,目前,市场上许多主流应用都有用到混合开发,例如支付宝.美团等.下面,结合我本人的开发经验,简单谈一 ...

  2. 学习 opencv---(3) ROI 区域图像叠加&初级图像混合

    在这篇文章里,我们一起学习了在OpenCV中如何定义感兴趣区域ROI,如何使用addWeighted函数进行图像混合操作,以及将ROI和addWeighted函数结合起来使用,对指定区域进行图像混合操 ...

  3. Atitti 图像处理 图像混合 图像叠加 blend 原理与实现

    Atitti 图像处理 图像混合 图像叠加 blend 原理与实现 混合模式 编辑 本词条缺少信息栏,补充相关内容使词条更完整,还能快速升级,赶紧来编辑吧! 混合模式是图像处理技术中的一个技术名词,不 ...

  4. 关于APP,原生和H5开发技术的争论 APP开发技术选型判断依据

    关于APP,原生和H5开发技术的争论 App的开发技术,目前流行的两种方式,原生和Html5.原生分了安卓平台和ios平台(还有小众的黑莓.死去的塞班就不说了),H5就是Html5. 目前争论不休的问 ...

  5. Hybrid App中原生页面 VS H5页面(分享)

    本文部分转自  http://www.jianshu.com/p/00ff5664e000 现有3类主流APP,分别为:Web App.Hybrid App(混合模式移动应用,Hybrid有“混合的” ...

  6. 《逐梦旅程 WINDOWS游戏编程之从零开始》笔记8——载入三维模型&Alpha混合技术&深度测试与Z缓存

    第17章 三维游戏模型的载入 主要是如何从3ds max中导出.X文件,以及如何从X文件加载三维模型到DirextX游戏程序里.因为复杂的3D物体,要用代码去实现,那太反人类了,所以我们需要一些建模软 ...

  7. OpenGL核心技术之混合技术

    笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者.国家专利发明人;已出版书籍:<手把手教你架构3D游戏引擎>电子工业出版社和<Unity3D ...

  8. android应用市场、社区客户端、漫画App、TensorFlow Demo、歌词显示、动画效果等源码

    Android精选源码 MVP架构Android应用市场项目 android刻度盘控件源码 Android实现一个社区客户端 android商品详情页上拉查看详情 基于RxJava+Retrofit2 ...

  9. android采用MVP完整漫画APP、钉钉地图效果、功能完善的音乐播放器、仿QQ动态登录效果、触手app主页等源码

    Android精选源码 一个可以上拉下滑的Ui效果,觉得好看可以学学 APP登陆页面适配 一款采用MVP的的完整漫画APP源码 android实现钉钉地图效果源码 一个使用单个文字生成壁纸图片的app ...

随机推荐

  1. java 定义一个同步map内存去重法

    实例:

  2. Scrapy爬虫入门系列1 安装

    安装python2.7 参见CentOS升级python 2.6到2.7 安装pip 参见CentOS安装python setuptools and pip‎ 依赖 https://docs.scra ...

  3. poj1125--Floyd

    题解: 有N个股票经济人能够互相传递消息.他们之间存在一些单向的通信路径.如今有一个消息要由某个人開始传递给其它全部人.问应该由哪一个人来传递,才干在最短时间内让全部人都接收到消息. 显然,用Floy ...

  4. codeforces Looksery Cup 2015 H Degenerate Matrix

    The determinant of a matrix 2 × 2 is defined as follows: A matrix is called degenerate if its determ ...

  5. TCP 同步传输:客户端发送,服务器段接收

    1.服务器端程序 可以在TcpClient上调用GetStream()方法来获的链接到远程计算机的网络流NetworkStream.当在客户端调用时,他获的链接服务器端的流:当在服务器端调用时,他获得 ...

  6. 网站web.cofig配置用户的权限

    访问被拒绝. 说明: 访问服务此请求所需的资源时出错.服务器可能未配置为访问所请求的 URL. 错误消息 401.2.: 未经授权: 服务器配置导致登录失败.请验证您是否有权基于您提供的凭据和 Web ...

  7. 2016/07/07 apmserv5.2.6 Apache启动失败,请检查相关配置。MySQL5.1已启动。

    因为要用PHP做一个程序,在本机上配PHP环境,下了个APMServ5.26,安装很简单,不再多说,装好后,启动,提示错误,具体是:“Apache启动失败,请检查相关配置.√MySQL5.1已启动”, ...

  8. EasyPusher手机直播推送是如何实现后台直播推送的

    本文由EasyDarwin开源团队成员John提供:http://blog.csdn.net/jyt0551/article/details/52276062 EasyPusher Android是使 ...

  9. Open Source Streaming Server--EasyDarwin

    Welcome to EasyDarwin Streaming Server, which is an open source Streaming Server Based On Appple's D ...

  10. JAVA with Cassandra

    maven项目,在pom.xml里加入依赖.不是的话下载相应的jar包放到lib目录下.这里驱动包的版本要和你cassandra的大版本一致. <dependency> <group ...