详解Paint的setXfermode(Xfermode xfermode)
一、setXfermode(Xfermode xfermode)
Xfermode国外有大神称之为过渡模式,这种翻译比较贴切但恐怕不易理解,大家也可以直接称之为图像混合模式,因为所谓的“过渡”其实就是图像混合的一种,这个方法跟我们上面讲到的setColorFilter蛮相似的。查看API文档发现其果然有三个子类:AvoidXfermode, PixelXorXfermode和PorterDuffXfermode,这三个子类实现的功能要比setColorFilter的三个子类复杂得多。
二、AvoidXfermode
2.1 关闭硬件加速
这个API因为不支持硬件加速在API 16已经过时了(大家可以在HardwareAccel查看那些方法不支持硬件加速)如果想在高于API 16的机子上进行测试,必须现在应用或手机设置中关闭硬件加速,在应用中我们可以通过在AndroidManifest.xml文件中设置application节点下的android:hardwareAccelerated属性为false来关闭硬件加速:
<application android:allowBackup="true" android:hardwareAccelerated="false" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > <activity android:name=".MainActivity" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application>
注意:如果你在设置中开启了强制硬件加速(强制使用GPU进行绘制),在代码中配置也是没用的。
2.2 构造方法
AvoidXfermode只有一个含参的构造方法:AvoidXfermode(int opColor, int tolerance, AvoidXfermode.Mode mode)
AvoidXfermode有三个参数,第一个opColor表示一个16进制的可以带透明通道的颜色值例如0x12345678;第二个参数tolerance表示容差值,那么什么是容差呢?你可以理解为一个可以标识“精确”或“模糊”的东西,表示当前颜色和opColor的差异程度;最后一个参数表示AvoidXfermode的具体模式,其可选值只有两个:AvoidXfermode.Mode.AVOID或者AvoidXfermode.Mode.TARGET,两者的意思也非常简单,我们逐一来看。
2.3 AvoidXfermode.Mode.TARGET
在该模式下Android会判断画布上的颜色是否会有跟opColor不一样的颜色,opColor可以理解为基准色,仅仅用来做判断。比如我opColor是红色,那么在TARGET模式下就会去判断我们的画布上是否有存在红色(opColor)的地方,如果有,则把该区域“染”上一层我们”画笔的颜色“,否则不“染”色,而tolerance容差值则表示画布上的像素和我们定义的红色之间的差别是多少的时候才去“染”的,比如当前画布有一个像素的色值是(200, 20, 13),而我们的红色值为(255, 0, 0),当tolerance容差值为255时,即便(200, 20, 13)并不等于红色值也会被“染”色,容差值越大“染”色范围越广反之则反。
如果我们想要让图片中白色像素被染上紫色:
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 先画一个图片 Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.kale); canvas.drawBitmap(bitmap, 240, 600, mPaint); /* * 当画布中有跟0XFFFFFFFF色不一样的地方时候才“染”色 * 第一个参数颜色是基本色,绘制的时候会判断有没有和基本色相似的颜色(相似程度用容差值表示),有的话就用画笔当前的颜色去绘制那个像素点。 */ AvoidXfermode avoidXfermode = new AvoidXfermode(0XFFFFFFFF, 255, AvoidXfermode.Mode.TARGET); // “染”什么色是由我们自己决定的 mPaint.setARGB(255, 211, 53, 243); // 设置模式 mPaint.setXfermode(avoidXfermode); // 在图片同等位置上,画一个和位图大小一样的矩形 canvas.drawRect(240, 600, 240 + bitmap.getWidth(), 600 + bitmap.getHeight(), mPaint); }
得到下面的效果:
如果提高容差值,那么接近白色的区域也会被染上紫色。
注意:如果你没关闭硬件加速,那么就会得到像下图这样的一个色块。
2.4 AvoidXfermode.Mode.AVOID
则与TARGET恰恰相反,TARGET是我们指定的颜色是否与画布的颜色一样,而AVOID是我们指定的颜色是否与画布不一样,其他的都与TARGET类似
AvoidXfermode(0XFFFFFFFF, 0, AvoidXfermode.Mode.AVOID):
当模式为AVOID容差值为0时,只有当图片中像素颜色值与0XFFFFFFFF完全不一样的地方才会被染色
AvoidXfermode(0XFFFFFFFF, 255, AvoidXfermode.Mode.AVOID):
当容差值为255时,只要与0XFFFFFFFF稍微有点不一样的地方就会被染色
那么这玩意究竟有什么用呢?比如说当我们只想在白色的区域画点东西或者想把白色区域的地方替换为另一张图片的时候就可以采取这种方式!
三、PixelXorXfermode
与AvoidXfermode一样也在API 16过时了,该类也提供了一个含参的构造方法PixelXorXfermode(int opColor),该类的计算实现很简单,从官方给出的计算公式来看就是:op ^ src ^ dst,像素色值的按位异或运算,如果大家感兴趣,可以自己用一个纯色去尝试,并自己计算异或运算的值是否与得出的颜色值一样,这里我就不讲了,Because it was deprecated and useless。
四、PorterDuffXfermode
Xfermode的最后一个子类也是惟一一个没有过时且沿用至今的子类,这个类是否强大,值得我们好好的学习学习。该类同样有且只有一个含参的构造方法PorterDuffXfermode(PorterDuff.Mode mode),这个PorterDuff.Mode大家看后是否会有些面熟,它跟上面我们讲ColorFilter时候用到的PorterDuff.Mode是一样的!PorterDuffXfermode就是图形混合模式的意思。
4.1 多种混合效果
这张图片从一定程度上形象地说明了图形混合的作用,两个图形一圆一方通过一定的计算产生不同的组合效果,在API中Android为我们提供了18种(比上图多了两种ADD和OVERLAY)模式:
这18种模式Android还为我们提供了它们的计算方式比如LIGHTEN的计算方式为[Sa + Da - Sa*Da, 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 - Sa*Da”这一部分的值代表计算后的Alpha通道,而“,”后的部分为“Sc*(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)”这一部分的值代表计算后的颜色值,图形混合后的图片依靠这个矢量来计算ARGB的值,如果大家感兴趣可以查看维基百科中对Alpha合成的解释:http://en.wikipedia.org/wiki/Alpha_compositing。
当大家看到上面API DEMO给出的效果时一定会觉得PorterDuffXfermode其实就是简单的图形交并集计算,比如重叠的部分删掉或者叠加等等,事实上呢!PorterDuffXfermode的计算绝非是根据于此!
4.2 准备好做测试的图片和代码框架
我们将在不同的模式下混合这两个Bitmap来看看这两个渐变色的颜色值在不同的混合模式下究竟发生了什么?图片中的黑色部分是完全透明的。
先看看我们的测试代码:
@TargetApi(Build.VERSION_CODES.HONEYCOMB) public class PorterDuffView extends View { /* * PorterDuff模式常量 * 可以在此更改不同的模式测试 */ private static final PorterDuff.Mode MODE = PorterDuff.Mode.ADD; private static final int RECT_SIZE_SMALL = 400;// 左右上方示例渐变正方形的尺寸大小 private static final int RECT_SIZE_BIG = 800;// 中间测试渐变正方形的尺寸大小 private Paint mPaint;// 画笔 private PorterDuffBO porterDuffBO;// PorterDuffView类的业务对象 private PorterDuffXfermode porterDuffXfermode;// 图形混合模式 private int screenW, screenH;// 屏幕尺寸 private int s_l, s_t;// 左上方正方形的原点坐标 private int d_l, d_t;// 右上方正方形的原点坐标 private int rectX, rectY;// 中间正方形的原点坐标 public PorterDuffView(Context context, AttributeSet attrs) { super(context, attrs); // 实例化画笔并设置抗锯齿 mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); // 实例化业务对象 porterDuffBO = new PorterDuffBO(); // 实例化混合模式 porterDuffXfermode = new PorterDuffXfermode(MODE); // 计算坐标 calu(context); } /** * 计算坐标 * * @param context * 上下文环境引用 */ private void calu(Context context) { // 获取包含屏幕尺寸的数组 int[] screenSize = MeasureUtil.getScreenSize((Activity) context); // 获取屏幕尺寸 screenW = screenSize[0]; screenH = screenSize[1]; // 计算左上方正方形原点坐标 s_l = 0; s_t = 0; // 计算右上方正方形原点坐标 d_l = screenW - RECT_SIZE_SMALL; d_t = 0; // 计算中间方正方形原点坐标 rectX = screenW / 2 - RECT_SIZE_BIG / 2; rectY = RECT_SIZE_SMALL + (screenH - RECT_SIZE_SMALL) / 2 - RECT_SIZE_BIG / 2; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 设置画布颜色为黑色以便我们更好地观察 canvas.drawColor(Color.BLACK); // 设置业务对象尺寸值计算生成左右上方的渐变方形 porterDuffBO.setSize(RECT_SIZE_SMALL); /* * 画出左右上方两个正方形 * 其中左边的的为src右边的为dis */ canvas.drawBitmap(porterDuffBO.initSrcBitmap(), s_l, s_t, mPaint); canvas.drawBitmap(porterDuffBO.initDisBitmap(), d_l, d_t, mPaint); /* * 将绘制操作保存到新的图层(更官方的说法应该是离屏缓存)我们将在1/3中学习到Canvas的全部用法这里就先follow me */ int sc = canvas.saveLayer(0, 0, screenW, screenH, null, Canvas.ALL_SAVE_FLAG); // 重新设置业务对象尺寸值计算生成中间的渐变方形 porterDuffBO.setSize(RECT_SIZE_BIG); // 先绘制dis目标图 canvas.drawBitmap(porterDuffBO.initDisBitmap(), rectX, rectY, mPaint); // 设置混合模式 mPaint.setXfermode(porterDuffXfermode); // 再绘制src源图 canvas.drawBitmap(porterDuffBO.initSrcBitmap(), rectX, rectY, mPaint); // 还原混合模式 mPaint.setXfermode(null); // 还原画布 canvas.restoreToCount(sc); } }
代码中我们使用到了View的离屏缓冲,也通俗地称之为层,这个概念很简单,我们在绘图的时候新建一个“层”,所有的绘制操作都在该层上而不影响该层以外的图像,比如代码中我们在绘制了画布颜色和左右上方两个方形后就新建了一个图层来绘制中间的大正方形,这个方形和左右上方的方形是在两个不同的层上的。很类似PS中的层。
演示说明:我们上方会绘制两个图像,左边是原始图像,右边是目标图像,中间的大正方形是最终的结果。所有图片黑色部分都是透明的,之所以是黑色的,是因为背景是黑色的。
4.3 详解各种模式
PorterDuff.Mode.ADD
计算方式:Saturate(S + D);
说明:饱和相加
从计算方式和显示的结果我们可以看到,ADD模式简单来说就是对图像饱和度进行相加,这个模式在应用中不常用,我唯一一次使用它是通过代码控制RGB通道的融合生成图片。
PorterDuff.Mode.CLEAR
计算方式:[0, 0];
说明:清除
清除图像,很好理解。
PorterDuff.Mode.DARKEN
计算方式:[Sa + Da - Sa*Da, Sc*(1 - Da) + Dc*(1 - Sa) + min(Sc, Dc)];
说明:变暗
这个模式计算方式目测很复杂,其实效果很好理解,两个图像混合,较深的颜色总是会覆盖较浅的颜色,如果两者深浅相同则混合。
如图,黄色覆盖了红色,蓝色和青色因为是跟透明混合所以不变。细心的朋友会发现青色和黄色之间有一层类似橙色的过渡色,这就是混合的结果。在实际的测试中源图和目标图的DARKEN混合偶尔会有相反的结果比如红色覆盖了黄色,这源于Android对颜色值“深浅”的定义,我暂时没有在官方查到有关资料不知道是否与图形图像学一致。DARKEN模式的应用在图像色彩方面比较广泛我们可以利用其特性来获得不同的成像效果,这点与之前介绍的ColorFilter有点类似。
PorterDuff.Mode.DST
计算方式:[Da, Dc];说明:只绘制目标图像
很好理解,就是不管原始的图片,之是绘制目标图像。这里的目标图像是右上方的图像,所以中间的结果区域和右上方的图像完全一致。
PorterDuff.Mode.DST_ATOP
计算方式:[Sa, Sa * Dc + Sc * (1 - Da)];
说明:在源图像和目标图像相交的地方绘制目标图像而在不相交的地方绘制源图像
因为黑色区域是透明区域,所以原图和目标图片不会有相交的地方,故最终的结果图片下半部分也是黑色的。原图上面是不透明,目标图片右边是不透明,所以相交之处是左上方。结果也显示,在不相交的地方仅仅绘制了原始图像。
PorterDuff.Mode.DST_IN
计算方式:[Sa * Da, Sa * Dc];
说明:只在源图像和目标图像相交的地方绘制目标图像
之前分析过了,原图和目标图片相交的地方只有左上区域,结果也显示在左上区域仅仅绘制了目标图像。我想最常见的应用就是蒙板绘制,利用源图作为蒙板“抠出”目标图上的图像。
例子:
我们把原图先画到手机屏幕上:
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas);// 先画一个图片 Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.src); canvas.drawBitmap(bitmap, 180, 200, null); }
我们想仅仅留下人物,取消上面的文字和白色背景该咋办呢?这就要用到了遮罩图片了。
用到的图片:
实现代码:
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 原始图片 Bitmap src = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.src); // 图片的遮罩 Bitmap mask = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.mask); /** * 分析: * 当前的模式是相交处绘制目标图像,那么我们的src就应该是目标图像了。mask是遮罩,反正是不会被绘制的,理解来看它应该做原始图像。 * 结论: * 原始图像:mask * 目标图像:src */ /* * 将绘制操作保存到新的图层(更官方的说法应该是离屏缓存) */ int sc = canvas.saveLayer(0, 0, 720, 1080, null, Canvas.ALL_SAVE_FLAG); // 先绘制dis目标图 canvas.drawBitmap(src, 180, 200, mPaint); // 设置混合模式 (只在源图像和目标图像相交的地方绘制目标图像) mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN)); // 再绘制src源图 canvas.drawBitmap(mask, 180, 200, mPaint); // 还原混合模式 mPaint.setXfermode(null); // 还原画布 canvas.restoreToCount(sc); }
最终结果:
PorterDuff.Mode.DST_OUT
说明:只在源图像和目标图像相交的地方绘制源图像
上面我们做了PorterDuff.Mode.DST_IN的例子,不妨也来做做PorterDuff.Mode.DST_OUT的例子吧。这个例子是用遮罩图片和来做的,目的是扣出剪影。
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 图片的遮罩 Bitmap mask = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.mask); /* * 将绘制操作保存到新的图层(更官方的说法应该是离屏缓存) */ int sc = canvas.saveLayer(0, 0, 720, 1080, null, Canvas.ALL_SAVE_FLAG); // 先绘制一层颜色 canvas.drawColor(0xFF8f66DA); // 设置混合模式 mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); // 再绘制src源图 canvas.drawBitmap(mask, 180, 200, mPaint); // 还原混合模式 mPaint.setXfermode(null); // 还原画布 canvas.restoreToCount(sc); }
结果:
PorterDuff.Mode.DST_OVER
计算方式:[Sa + (1 - Sa)*Da, Rc = Dc + (1 - Da)*Sc];
说明:在源图像的上方绘制目标图像
就是把目标图像放在了原图上方,产生简单的叠加效果。
PorterDuff.Mode.LIGHTEN
计算方式:[Sa + Da - Sa*Da, Sc*(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)];
说明:变亮
与DARKEN相反,把原图和目标图中的颜色进行比较,找到颜色较明亮的,区域进行绘制。比如,黄色比红色亮,所以绘制了黄色,没有绘制红色。
PorterDuff.Mode.MULTIPLY
计算方式:[Sa * Da, Sc * Dc];
说明:正片叠底
该模式通俗的计算方式很简单,源图像素颜色值乘以目标图像素颜色值除以255即得混合后图像像素的颜色值,该模式在设计领域应用广泛,因为其特性黑色与任何颜色混合都会得黑色,在手绘的上色、三维动画的UV贴图绘制都有应用,具体效果大家自己尝试我就不说了。
PorterDuff.Mode.OVERLAY
计算方式:未给出;
说明:叠加
这个模式没有在官方的API DEMO中给出,谷歌也没有给出其计算方式,在实际效果中其对亮色和暗色不起作用,也就是说对黑白色无效,它会将源色与目标色混合产生一种中间色,这种中间色生成的规律也很简单,如果源色比目标色暗,那么让目标色的颜色倍增,否则颜色递减。
PorterDuff.Mode.SCREEN
计算方式:[Sa + Da - Sa * Da, Sc + Dc - Sc * Dc];
说明:滤色
滤色产生的效果我认为是Android提供的几个色彩混合模式中最好的,它可以让图像焦媃幻化,有一种色调均和的感觉。
用代码来看看效果:
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 原始图片 Bitmap src = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.src); /* * 将绘制操作保存到新的图层(更官方的说法应该是离屏缓存) */ int sc = canvas.saveLayer(0, 0, 720, 1080, null, Canvas.ALL_SAVE_FLAG); // 先绘制dis目标图 //canvas.drawBitmap(src, 180, 200, mPaint); // 先绘制一层带透明度的颜色 canvas.drawColor(0xcc1c093e); // 设置混合模式 (只在源图像和目标图像相交的地方绘制目标图像) mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SCREEN)); // 再绘制src源图 canvas.drawBitmap(src, 180, 200, mPaint); // 还原混合模式 mPaint.setXfermode(null); // 还原画布 canvas.restoreToCount(sc); }
代码中的紫色是有透明度的,让它和原图进行混合,给原图添加了一点点的淡紫色。
PorterDuff.Mode.SRC
计算方式:[Sa, Sc];
说明:只显示源图
只绘制源图,SRC类的模式跟DIS正好相反。
PorterDuff.Mode.SRC_ATOP
计算方式:[Da, Sc * Da + (1 - Sa) * Dc];
说明:在源图像和目标图像相交的地方绘制源图像,在不相交的地方绘制目标图像
原图和目标图相交的地方是左上角,所以左上角是原图,其余的地方都是目标图像。
PorterDuff.Mode.SRC_IN
计算方式:[Sa * Da, Sc * Da];
说明:只在源图像和目标图像相交的地方绘制源图像
和上面分析的类似,左上角是相交的地方,所以只有愿图像,其余的地方没有绘制。
PorterDuff.Mode.SRC_OUT
计算方式:[Sa * (1 - Da), Sc * (1 - Da)];
说明:只在源图像和目标图像不相交的地方绘制源图像
我们知道不相交的地方就是除了左上角之外的地方,这些地方都是目标图像。
PorterDuff.Mode.SRC_OVER
计算方式:[Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc];
说明:在目标图像的顶部绘制源图像
这也是个谁上谁下的关系,这里的原图是放在目标图像上方的,产生了叠加效果。
PorterDuff.Mode.XOR
计算方式:[Sa + Da - 2 * Sa * Da, Sc * (1 - Da) + (1 - Sa) * Dc];
说明:在源图像和目标图像重叠之外的任何地方绘制他们,而在不重叠的地方不绘制任何内容
XOR这个模式我们在将PixelXorXfermode的时候提到过,不了解的话上去看看。
五、总结
大家一定要有这样的思维,当你想要去画一个View的时候一定要想想看这个View的图形是不是可以通过基本的几何图形混合来生成,如果可以,那么恭喜你,使用PorterDuffXfermode的混合模式你可以事半功倍!
说明:本文大部分来自:http://blog.csdn.net/aigestudio/article/details/41316141,我对原文有细小的删减,记录在此作为学习笔记之用。
From AigeStudio(http://blog.csdn.net/aigestudio)Power by Aige 尊重原作者,感谢作者的分享!