文章目录
1 PorterDuff.Mode的作用和背景
PorterDuff.Mode
是用来指定两个图像共同绘制时的颜色策略的。它是一个枚举,不同的Mode可以指定不同的策略。颜色策略的意思,就是说把源图像绘制到目标图像处时应该怎样确定二者结合后的颜色。
PorterDuff.Mode
一共有17个,可以分为两类:
-
Alpha合成(Alpha Compositing)
-
混合(Blending)
第一类Alpha合成,其实就是 PorterDuff
这个词所指代的算法。PorterDuff
并不是一个具有实际意义的词组,而是两个人的名字。这两个人当年共同发表了一篇论文,描述了12种将两个图像共同绘制的操作(即算法)。而这篇论文所论述的操作,都是关于Alpha通道(也就是我们通俗理解的透明度)的计算的,后来人们就把这类计算称为Alpha合成(Alpha Composiing)。
第二类混合,也就是Photoshop等制图软件里都有的那些混合模式(multiply darken lighten之类的)。这一类操作的是颜色本身而不是Alpha通道,并不属于Alpha合成,所以和Porter与Duff这两个人也没什么关系,不过为了使用方便,它们同样也被Google加进了 PorterDuff.Mode
里。
2 PorterDuff.Mode的颜色策略
2.1 Alpha合成
上面Google的官方文档展示的Alpha合成是DST在下层先绘制,SRC在上层后绘制。
DST
就是 Destination
意思是目标,在使用Xfermode前我们应该要有一个目标,就是 DST
;而 SRC
就是 Source
意思是源,就是我要往 DST
上贴使用Xfermode的图像。所以就有DST在下层,SRC在上层。
根据上面的Alpha合成效果,我这边再细分方便理解:
-
OUT:取指定层的非交集部分。比如
DST_OUT
表示取下层非交集部分 -
IN:取指定层的交集部分。比如
DST_IN
表示取下层交集部分 -
OVER:指定层在上层显示。比如
DST_OVER
表示取下层显示在上面 -
ATOP:取与指定层的交集部分和相反层的非交集部分。比如
DST_ATOP
表示取下层的交集部分和上层的非交集部分 -
XOR:取上层和下层的非交集部分
那么各个Alpha合成的描述如下:
-
PorterDuff.Mode.CLEAR:绘制不会提交到画布上
-
PorterDuff.Mode.SRC:显示上层绘制图片
-
PorterDuff.Mode.SRC_OVER:正常显示,指定上层覆盖在下层上面
-
PorterDuff.Mode.SRC_IN:取上层交集部分
-
PorterDuff.Mode.SRC_OUT:取上层非交集部分
-
PorterDuff.Mode.SRC_ATOP:取上层交集部分和下层非交集部分
-
PorterDuff.Mode.DST:显示下层绘制图片
-
PorterDuff.Mode.DST_OVER:下层覆盖在上层上面
-
PorterDuff.Mode.DST_IN:取下层交集部分
-
PorterDuff.Mode.DST_OUT:取下层非交集部分
-
PorterDuff.Mode.DST_ATOP:取下层交集部分和上层非交集部分
-
PorterDuff.Mode.CLEAR:所绘制不会提交到画布上
-
PorterDuff.Mode.XOR:取上层和下层的非交集部分
2.2 Xfermode使用注意事项
2.2.1 控制好透明区域
你或许会认为Google官方文档中,SRC和DST就是只绘制颜色区域一个矩形和一个圆形然后两个颜色区域叠加,你的理解是这样的:
SRC:
// 假设矩形的绘制区域是在left=150, top=150, right=400, bottom=400
// 不包含外部透明区域
canvas.drawRect(150, 150, 400, 400, paint);
DST:
// 假设圆形的绘制区域是在圆心(400, 150),radius=150区域
// 不包含外部透明区域
canvas.drawCircle(400, 150, 150, paint);
// 只考虑颜色区域的叠加效果
public class XfermodeView extends View {
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private Xfermode mXfermode;
public XfermodeView(Context context) {
super(context);
}
public XfermodeView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public XfermodeView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
{
// 如果有效果没有生效(比如混合的几种模式),需要关闭硬件加速
setLayerType(LAYER_TYPE_SOFTWARE, null);
mXfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int saved = canvas.saveLayer(null, null, Canvas.ALL_SAVE_FLAG);
// 绘制DST位于下层,DST为圆形
canvas.drawCircle(400, 150, 150, paint);
mPaint.setXfermode(mXfermode);
// 绘制SRC位于上层,SRC为矩形
canvas.drawRect(150, 150, 400, 400, paint);
mPaint.setXfermode(null);
canvas.restoreToCount(saved);
}
}
如果按上面只绘制颜色区域的叠加,你会发现实际绘制效果和Google效果图展示的不同,好像有些颜色策略显示正常,有些显示不正确。这其实是透明区域控制不正确导致。
Google官方文档展示所绘制SRC和DST是Bitmap,Bitmap有透明区域和颜色区域,Bitmap绘制的蓝色矩形和粉色圆形是用 Canvas
绘制的颜色区域宽高。SRC和DST的Bitmap是大小控制了透明区域重叠的,不仅仅包含颜色区域,还有外部的透明区域也要计算进去。描述如下图所示:
透明区域要足够覆盖到要和它结合绘制的内容,否则得到的结果很可能不是你想要的:
由上图所示,由于透明区域过小而覆盖不到的地方,将不会收到Xfermode的影响。
为了能够实现官方文档上的效果,我们需要的是把要合成效果的图像抠出来排除被空白区域影响,那要怎么抠出来?其实就是使用离屏缓冲 canvas.saveLayer()
和 canvas.restoreToCount()
。
我们按照Google官方文档的效果将上面的分析写成项目示例代码:
public class XfermodeView extends View {
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private Bitmap mSrcBitmap;
private Bitmap mDstBitmap;
private Xfermode mXfermode;
public XfermodeView(Context context) {
super(context);
}
public XfermodeView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public XfermodeView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
{
// 如果有效果没有生效(比如混合的几种模式),需要关闭硬件加速
setLayerType(LAYER_TYPE_SOFTWARE, null);
mXfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mSrcBitmap == null) {
mSrcBitmap = createSrc(getWidth(), getHeight());
}
if (mDstBitmap == null) {
mDstBitmap = createDst(getWidth(), getHeight());
}
int saved = canvas.saveLayer(null, null, Canvas.ALL_SAVE_FLAG);
// 绘制DST位于下层,DST为圆形
canvas.drawBitmap(mDstBitmap, 0, 0, mPaint);
mPaint.setXfermode(mXfermode);
// 绘制SRC位于上层,SRC为矩形
canvas.drawBitmap(mSrcBitmap, 0, 0, mPaint);
mPaint.setXfermode(null);
canvas.restoreToCount(saved);
}
private Bitmap createSrc(int width, int height) {
// Bitmap大小是View的宽高,和DST是重叠的
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.parseColor("#3C92F5"));
canvas.drawRect(150, 150, 400, 400, paint);
return bitmap;
}
private Bitmap createDst(int width, int height) {
// Bitmap大小是View的宽高,和SRC是重叠的
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.parseColor("#ED1462"));
canvas.drawCircle(400, 150, 150, paint);
return bitmap;
}
}
2.2.2 使用离屏缓冲(Off-screen Buffer)
如果在使用Xfermode时没有开启离屏缓冲,会出现绘制效果与期望不符。同样按照上面SRC和DST的Bitmap都是同样大小来绘制,没有开启离屏缓冲效果的 PorterDuff.Mode.DST_IN
的效果如下:
按照逻辑我们会认为,在第二步绘制SRC画圆的时候,跟它共同计算的是第一步绘制的DST矩形。但实际上,却是整个View的显示区域都在画圆的时候参与计算,并且View自身的底色并不是默认的透明色,导致不仅绘制的是整个圆的范围,而且在范围之外都变成了黑色。
要想使用Xfermode正常绘制,必须使用离屏缓冲(Off-screen Buffer)把内容绘制在额外的层上,再把绘制好的内容贴回View中。通过使用离屏缓冲,把要绘制的内容单独绘制在缓冲层,Xfermode的使用就不会出现奇怪的结果了。使用离屏缓冲有两种方式:
- Canvas.saveLayer():可以做短时的离屏缓冲
int saved = canvas.saveLayer(null, null, Canvas.ALL_SAVE_FLAG);
...
canvas.restoreToCount(saved);
-
View.setLayerType():直接把整个View都绘制在离屏缓冲中
-
setLayerType(LAYER_TYPE_HARDWARE):使用GPU来缓冲
-
setLayerType(LAYER_TYPE_SOFTWARE):直接用一个Bitmap来缓冲
-
2.3 混合
混合的使用也可以使用上面的Demo来完成,但要注意关闭硬件加速否则存在无法生效的问题。一般实际开发用得不多了解即可,具体效果还是直接拿的Google官方文档的效果图:
参考: