Paint PorterDuffXfermode详解
多年以来自己都有一个毛病,知识或者说技术储备看到过或者知道在哪里就觉得自己掌握了,但实际上并没有,当自己开始做,两眼抓瞎。最好的例子在于ImageView scaleType 相关的。以前一直记不住,直到我写demo 去对比区别才很清楚明白,所以对于PorterDuffXfermode 也一样。
以下理论解释来自于Xfermode in android
什么是Xfermode
有三个实现类: AvoidXfermode,PixelXorXfermode,PorterDuffXfermode
AvoidXfermode
AvoidXfermode xfermode will draw the src everywhere except on top of the
opColor or, depending on the Mode, draw only on top of the opColor.
官方解释,按照我的理解是如果想把原来图像进行处理,比如绿色换成红色,可以使用。这里有个容差值的概念,比如红色是0xff0000,但在一定范围内都是红色,如果设置一个容差,在范围内的 各种符合要求的红色 都会被处理。
PixelXofXermode
没设么用,不支持硬件加速
接下来说说重点,也就是最常用的
PorterDuffXfermode
Porter-Duff 来由
Porter-Duff 操作是 1 组 12 项用于描述数字图像合成的基本手法,包括
Clear、Source Only、Destination Only、Source Over、Source In、Source
Out、Source Atop、Destination Over、Destination In、Destination
Out、Destination Atop、XOR。通过组合使用 Porter-Duff 操作,可完成任意 2D
图像的合成。
Thomas Porter 和 Tom Duff 发表于 1984年原始论文的扫描版本
可以支持任何2D图像的合成。理论支撑
PorterDuffXfermode 各种模式之间的区别
有哪些种类?
android 中共有18种不同模式,分别是:
- CLEAR
- SRC
- DST
- SRC_OVER
- DST_OVER
- SRC_IN
- DST_IN
- SRC_OUT
- DST_OUT
- SRC_ATOP
- DST_ATOP
- XOR
- DARKEN
- LIGHTEN
- MULTIPLY
- SCREEN
- ADD
- OVERLAY
文档解释
public enum Mode {
/** [0, 0] */
CLEAR (0),
/** [Sa, Sc] */
SRC (1),
/** [Da, Dc] */
DST (2),
/** [Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc] */
SRC_OVER (3),
/** [Sa + (1 - Sa)*Da, Rc = Dc + (1 - Da)*Sc] */
DST_OVER (4),
/** [Sa * Da, Sc * Da] */
SRC_IN (5),
/** [Sa * Da, Sa * Dc] */
DST_IN (6),
// ...以下省略
结合Paint 如何使用
1.声明Paint
private val paint by lazy {
Paint().apply {
isAntiAlias = true
color = ContextCompat.getColor(ctx, R.color.app_color_blue_2_pressed)
style = Paint.Style.FILL
}
}
2.对Paint 设置
paint.xfermode = PorterDuffXfermode(modes[i])
3.canvas 绘制
// draw dst
canvas.drawBitmap(makeDst()
,0f,0f,paint)
paint.xfermode = PorterDuffXfermode(modes[i])
// draw src
canvas.drawBitmap(makeSrc(),0f,0f,paint)
paint.xfermode = null
XfermodeSampleView 分析
先上图为了仔细对比以及理解各种模式之间的区别,以及使用中遇到的问题,还有疑惑,接下来仔细的分析各种出现的情况。
- 首先xfermode 绘图需要两部分,DST,SRC 两种。可以理解为DST 在下边,SEC在上面。也就是说DST先绘制,SRC 后绘制。看一下代码生成
fun makeSrc() : Bitmap{
val radius = rectSize.div(3f)
val bitmap = Bitmap.createBitmap(rectSize
,rectSize,Bitmap.Config.ARGB_8888)
val c = Canvas(bitmap)
val p = Paint().apply {
style = Paint.Style.FILL
color = ContextCompat.getColor(context, R.color.app_color_theme_3)
}
c.drawRect(radius,radius,rectSize.times(0.75f),rectSize.times(0.75f),p)
return bitmap
}
fun makeDst() : Bitmap{
val radius = rectSize.div(3f)
val bitmap = Bitmap.createBitmap(radius.times(2).toInt()
,radius.times(2).toInt(),Bitmap.Config.ARGB_8888)
val c = Canvas(bitmap)
val p = Paint().apply {
style = Paint.Style.FILL
color = ContextCompat.getColor(context, R.color.app_color_blue_2_pressed)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
c.drawOval(0f,0f,radius.times(2),radius.times(2),p)
}
return bitmap
}
这一段代码需要理解的地方在于,canvas.drawRect,canvas.drawOval,为什么是传递这些参数。比如说 c.drawOval(0f,0f,radius.times(2),radius.times(2),p),为什么传的left = 0f,top = 0f,right = 2 * radius,bottom = 2 * radius。首先我们创建宽高为 2 * radius。 所以画布总的大小为固定2 * radius 大小。canvas 坐标还是以左上有起点。
- 在对DST,SRC进行绘制的时候,为什么是需要传入bitmap 呢?
例如 canvas.drawBitmap(makeSrc(),0f,0f,paint)
在之前的测试中也写过不是bitmap的情况。直接drawOval ,drawRect。但是情况跟现在的情况完全不一样。各种模式之间的混合不如预期,也只有现在通过bitmap 之间的混合才会生效。
一下错误的用法(具体原因不是很明确)
canvas.drawCircle(cx, cy, radius, paint)
paint.xfermode = PorterDuffXfermode(modes[i])
paint.color = ContextCompat.getColor(context, R.color.app_color_theme_3)
// draw src
canvas.drawRect(cx, cy
, cx + radius.times(2), cy + radius.times(2), paint)
paint.xfermode = null
可以看到这两种绘制的方式,一种通过生成biamp,为它创建canvas 并且绘图。另一种直接使用canvas 去绘制。本来我认为这两种没有区别,也不会有问题。可实际上出现了问题。没有达到预期的混合。具体的原因目前还没有明确,我猜测可能是因为通过生成bitmap,这是本来已经在新的画布上绘制的。而且xfermode 本来就是图像的绘图混合。drawRect,drawCircle本身不是图像。所以会有根本的差异。也就是说使用xfermode 要在于场景,在drawBitmap()中去使用。
采用drawXXX 的方式,混合错误,如下
错误的使用
- 还有一个问题也是混合中很常见的。混合之后会出现混合处会有黑色占位的情况。对于这样的情况,很多次没有搞明白的时候我都是拒绝的。这到底是什么情况? 现在我可以理解为是因为dst,src混合的窗口是透明的,其实对于这种解释,也很疑惑,因为调用过canvas.drawColor(Color.WHITE)使canvas 背景为白色,可惜这样也会有问题。在混合之后,进行裁剪了。就黑色了。这样子我还没有找到根本确切的解释。但是解决方法是有的。在进行混合之前需要保存画布
val sc = canvas.saveLayer(posX.toFloat() * rectSize
, posY.toFloat() * rectSize
, (posX.toFloat() + 1) * rectSize, (posY.toFloat() + 1) * rectSize, null,
Canvas.ALL_SAVE_FLAG )
...
canvas.restoreToCount(sc)
可以看到调用结束之后,恢复了画布。关于canvas 的saveLayer,canvas.restoreToCount的分析理解,请看另一篇文章。还有为什么是saveLayouer 里面是一部分保存。而不是canvas.save() 它们有什么区别?
可以看到通过restoreToCount 的处理,并没有黑色部分了。结果也很符合预期
- 对于背景的添加,这里用到了bitmapShader。也是Paint 另外一个很值得好好理解的方法paint.setShader(),shader 也就是着色器
- 创建bitmapShadow
// make a ckeckerboard pattern
val bm = Bitmap.createBitmap(intArrayOf(-0x1, -0x333334, -0x333334, -0x1), 2, 2,
Bitmap.Config.RGB_565)
mBG = BitmapShader(bm,
Shader.TileMode.REPEAT,
Shader.TileMode.REPEAT)
val m = Matrix()
m.setScale(6f, 6f)
mBG.setLocalMatrix(m)`
具体的参数意义,以及mBg.setLocalMatrix 以后在好好写一下。
- 使用
paint.setShader(mBG)
// draw bg
canvas.drawRect(x,y, x + rectSize.toFloat() - 25,y + rectSize.toFloat(),paint)
paint.setShader(null)
在这里,paint.setShadow() 设置好shadow,在drawRect中会把创建bitmapShader 传入的bitmap 绘制上去。至于绘制的顺序,比如是绘制的shader 的bitmap 和 canvas.drawCircle 的图形,谁在上谁在下。可以认为shader 是在最下的。
- 清除shader
paint.setShadow(null)
设置为Null 即可去除