因为以前 UI 相关的东西写的不多,今天看了下 Android 中图像合成的部分。顺便写了几个 demo 踩坑。内容比较简单,简单总结一下,分享给大家。
porterduff 并不是Android 特有的概念,我们看一下 porterduff 的 wikipedua, 它是 1984 年由 ThomasPorter 和 TomDuff 创建的数字图像合成算法,并且成为了计算机图形学基础的一部分。
这个算法的意思是,当2个图像进行合成的时候,必须知道合成对单个像素有什么影响。除了颜色通道(RGB),还有透明度(A),从而定义每个像素在和基础像素重叠的时候应该如何显示,或者下面一个图像的像素的颜色强度如何照到覆盖像素上。简单点说就是,2张图,叠在一起,不同的mode下,根据ARGB,会显示出不同的样子。
根据 Android 文档的介绍,合成算法大概分成下面几个模式:
我们分别定义一个源图像 src 和 目标图像 dest

【src】

【dest】
按照绘制顺序的话, dest先绘制,在下面。 src后绘制,在上方。
合成的模式大致有透明度合成模式和混合模式,这些在官网上有具体的介绍。在官方文档中,每个模式都会附带 2 个公式来计算输出部分的透明度和颜色。

其中 α 表示 alpha,C 表示 color。
至于每种模式,可以参考下面这张广为流传的图:

效果是下面的,dest是蓝色的圆形,宽高是100*100, src是红色的矩形,宽高是 100 * 30。
直接放代码:
class XferModeView : View {
constructor(context: Context?) : super(context)
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
)
var paint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG)
private var mode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
var srcRect: RectF = RectF(0f, 70f, 100f, 100f)
val bitmapCanvas = Canvas()
var bitmap: Bitmap? = null
override fun onDraw(canvas: Canvas?) {
canvas?.let {
val saveCount = it.saveLayer(
0f,
0f,
canvas.width.toFloat(),
canvas.height.toFloat(),
null,
Canvas.ALL_SAVE_FLAG
)
paint.color = Color.BLUE
paint.style = Paint.Style.FILL
it.drawCircle(
50f, 50f,
50f, paint
) // dst
paint.color = Color.RED
if (bitmap == null) {
bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)
}
bitmapCanvas.setBitmap(bitmap)
bitmapCanvas.drawRect(srcRect, paint)
paint.xfermode = mode
canvas.drawBitmap(bitmap!!, 0f, 0f, paint)
paint.xfermode = null
it.restoreToCount(saveCount)
}
}
}这里给这段demo做一下总结:
canvas.saveLayer,结束的时候调用 restoreToCount , saveLayer的意思是在下一步绘制的之前把所在的图层状态进行保存,后面的内容绘制在一个新的图层,这样旧图层的内容也不会影响最后的绘制效果。saveLayer,在文档介绍中,这是一个非常昂贵的操作,我们可能需要花费2倍以上的时间去绘制。在 xfermode的场景下,其实可以不调用 savelayer,直接在view初始化的时候开启硬件加速就可以实现同样的效果,并且这也是比较推荐的做法。init {
setLayerType(LAYER_TYPE_HARDWARE, null)
}porterDuff的时候,很想当然的直接切换mode, draw 2个图像,发现效果和文档上的示图完全不一致。最后看了很多其他的demo,发现都是先把 dest和 src 写到 bitmap里面的。尝试了一下,发现实际上至少需要把 src 先画到 bitmap 上,然后再切换 mode,绘制到 canvas 上面。至于最后的bitmap的坐标(大小)如何控制呢,2个bitmap一样就可以了。仔细想想, porterduff 的算法其实是操作了像素点,那肯定是需要 bitmap 来操作的。类似场景还是都使用 srcbitmap和destbitmap来处理比较稳妥。
Bitmap 创建的时候:Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)这里的width和right和 dest 的 width,height一样就可以,像上面的demo示例,直接设置成view的 width, height 就行。
在 draw 的时候,需要调用:
canvas.drawBitmap(bitmap!!, 0f, 0f, paint)当宽高和 view 一致的时候,left、top的坐标值也很简单,直接设置成 (0, 0) 即可。