概述
在android canvas绘图中经常会使用到Paint.setXfermode()来给画笔设置一个Xfermode;Xfermode是一种将所绘制的图形的像素按照一定模式进行混合从而形成新的像素值。Xfermode有三个子类:AvoidXfermode、PixelXorXfermode、PorterDuffXfermode;前两个已经废弃了,我们使用的最多的就是PorterDuffXfermode。通过这些模式可以实现很多效果,比较常见的就是圆形头像等。
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。
在学习如何使用时一般都是参照SDK提供的Demo,也就是这张广为流传的图。这张图直观的展现了16种不同的PorterDuffXfermod模式的混合效果。但这张图也为使用者埋下了一些坑。
写个示例
我们按照API Demo这种样式自己来写个示例看看效果:
@Override
protected void onDraw(Canvas canvas) {
//背景色
canvas.drawARGB(255, 255, 156, 161);
//先绘制的是dst,后绘制的是src
drawDst(canvas, mPaint);
drawSrc(canvas, mPaint);
}
private void drawDst(Canvas canvas, Paint p) {
//画黄色圆形
p.setColor(0xFFFFCC44);
canvas.drawOval(new RectF(0, 0, W * 3 / 4, H * 3 / 4), p);
}
private void drawSrc(Canvas canvas, Paint p) {
//画蓝色矩形
p.setColor(0xFF66AAFF);
canvas.drawRect(W / 3, H / 3, W * 19 / 20, H * 19 / 20, p);
}
代码很简单,就是在onDraw()中先画了一个黄色圆形dst,然后再画了一个蓝色矩形src。结果显示后绘制的蓝色矩形叠加在黄色圆形上了,这是正常的混合模式。我们试试使用PorterDuffXfermod的CLEAR模式。修改onDraw方法如下:
@Override
protected void onDraw(Canvas canvas) {
//背景色
canvas.drawARGB(255, 255, 156, 161);
//先绘制的是dst,后绘制的是src
drawDst(canvas, mPaint);
//设置xfermode
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
drawSrc(canvas, mPaint);
//还原
mPaint.setXfermode(null);
}
我们在绘制蓝色矩形src时给paint设置了Xfermode为CLEAR模式;我们发现绘制的蓝色矩形src结果成了白色矩形,这是为什么呢?这里我们先不解释,我们现在将onDraw修改成如下:
@Override
protected void onDraw(Canvas canvas) {
//背景色
canvas.drawARGB(255, 255, 156, 161);
int sc = canvas.saveLayer(0, 0, canvas.getWidth(), canvas.getHeight(), null, Canvas.ALL_SAVE_FLAG);
//先绘制的是dst,后绘制的是src
drawDst(canvas, mPaint);
//设置xfermode
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
drawSrc(canvas, mPaint);
//还原
mPaint.setXfermode(null);
canvas.restoreToCount(sc);
}
这次我们发现绘制的这个蓝色矩形src好像“消失了”。并且发现dst与src两个图形的相交处显示的是背景色。比较这两处代码不同之处在于这次绘制图形是放在canvas.saveLayer与canvas.restoreToCount中实现的。关于savelayout的作用查一下可知:
- 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的绘制工作。
我们分析一下原因:
1. 我们先将画布设置背景色,此时画布的ARGB值就是背景色值。
2. 我们通过saveLayer新建一个layer,接下来的绘制都是在这个新建的layer上进行绘制的了。
3. 我们先绘制了一个黄色的圆形dst。
4. 设置画笔Xfermode为CLEAR模式画蓝色矩形src,由于CLEAR模式蓝色矩形区域(包括与圆形相交部分)的ARGB值被设置为(0,0,0,0)也就是透明。
5. 这样新建的layer上就只有一个3/4黄色圆形dst是不透明的(1/4与src叠加部分被置为透明),最后通过restoreToCount将layer绘制到canvas上,绘制的结果就是:透明部分显示canvas背景颜色,不透明部分显示黄色圆形dst部分。也就是上图这种现象。
“为什么第一次会出现白色矩形呢?”这个问题也就很好回答了,因为我们一开始是在canvas的默认layer上绘制的,当我们的矩形区域src被CLEAR模式绘制后,该区域变为透明。canvas该部分就成为透明了,但由于Activity背景本身是白色的所以最终显示就为白色了。
谁错了?
我们通过上面的实验和分析可以得出,CLEAR模式可以使两种图片相交处设置为透明。但我们把API Demo那张图拿出来后一看:“不对啊,不应该是全部透明么?”这里就是那张图给我们挖的坑了。我们仔细看下官方示例的代码是如何实现的:
// 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);
p.setColor(0xFFFFCC44);
c.drawOval(new RectF(0, 0, w*3/4, h*3/4), p);
return bm;
}
// 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;
}
这是其实现代码,我们可以看出这两个方法的目的就是创建两个图形用来演示PorterDuffXfermod的不同效果。但我们仔细一看就能发现有点问题了:
1. 创建的bitmap的宽高是w,h;但绘制的图形大小只是其中一部分(注意drawXX中的代码),这样bitmap其余部分是透明的。
2. 这就可以知道最终得到的dst与src是两张大小一致(都为w,h)的bitmap,只是可见区域(绘制区域)导致我们误解是两个不同尺寸的bitmap。
3. 这样的dst与src叠加就不再是一个黄色圆形与蓝色矩形叠加了,而是两张宽高一致的bitmap叠加了。
3. 这样的src(蓝色矩形)设置为CLEAR后,其实是绘制的整个bitmap设置为CLEAR。dst与src相交区域也就是整个bitmap区域都会被设置为透明,这也就是为什么我们的实验与官方demo不一致的地方了。
原来如此!所以在开发中万不可看着API Demo图无脑来选择一种自己想要的模式,这样会使你得不到自己想要的结果时摸不着头脑。接下来我们按照图形实际大小修改后可以得出另一张图:
最后几点
关于硬件加速
在sdkversion>=11时,需要关闭硬件加速,否则 Mode.CLEAR 、 Mode.DARKEN 、 Mode.LIGHTEN 三种模式下绘制效果不正常。if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB){ //View从API Level 11才加入setLayerType方法 //关闭硬件加速 setLayerType(View.LAYER_TYPE_SOFTWARE, null); }
- 关于saveLayer
savelayer不是必须的,我们已经知道使用他的目的了;其实我们可以新建一个bitmap并在这上面进行绘制,最后将bitmap绘制到canvas上,其实原理是一致的。