混合模式的作用就是将两张图片进行无缝结合,类似于PS中的图片融合。可以通过Paint.setXfermode()设置混合模式,使用图片的缓存需要注意两点:
- 1、关闭硬件加速
setLayerType(LAYER_TYPE_SOFTWARE, null)
- 2、使用离屏缓存(使用canvas.save()可能无法实现效果)
val layerId = canvas.saveLayer(0f,0f,width.toFloat(),height.toFloat(),null,Canvas.ALL_SAVE_FLAG)
//图片混合代码
canvas.restoreToCount(layerId)
函数声明如下:
public Xfermode setXfermode(Xfermode xfermode)
Xfermode的派生类有AvoidXfermode、PixelXorXfermode、PorterDuffXfermode,AvoidXfermode和PixelXorXfermode已经被弃用,这里主要看下PorterDuffXfermode。
1、PorterDuffXfermode
PorterDuffXfermode的构造函数如下
public PorterDuffXfermode(PorterDuff.Mode mode)
PorterDuff.Mode的取值有18个,如下
image.png
比如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通道,后部分表示计算后的颜色。图像混合后的图片就是依据这个公式来对DST和SRC两张图片中的每个像素进行计算,得到最终结果的。
上面公式中都涉及到源图像和目标图像,下面举例说明下什么是源图像,什么是目标图像。
这里创建两张图片,分别为一个圆形和一个矩形,矩形的左上角开始位置在圆形中心。如下图
image.png
在这个例子中,圆形是目标图像,方形是源图像,区域1为源图像和目标图像相交的区域,区域2是源图像和空白像素相交的区域。
1.1、Mode.SRC_IN
首先需要自定义控件并且初始化
class PorterDuffXfermodeView(context: Context, attrs: AttributeSet) : View(context, attrs) {
private val mWidth = 200
private val mHeight = 200
private var dstBitmap: Bitmap? = null
private var srcBitmap: Bitmap? = null
private val paint = Paint()
init {
//禁用硬件加速
setLayerType(LAYER_TYPE_SOFTWARE,null)
dstBitmap=makeDst()
srcBitmap=makeSrc()
}
}
然后创建源图像和目标图像
private fun makeDst(): Bitmap? {
val bitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
paint.color = 0xFFFFCC44.toInt()
canvas.drawCircle(mWidth * 1.0f / 2, mHeight * 1.0f / 2, mWidth * 1.0f / 2, paint)
return bitmap
}
这里创建了一个空白的图片,然后在图片上画了一个黄色的圆形,所以此时图片中心有一个圆形,除圆形外的位置都是空白像素。
private fun makeSrc(): Bitmap? {
val bitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
paint.color = 0xFF66AAFF.toInt()
canvas.drawRect(
0f,
0f,
mWidth.toFloat(),
mHeight.toFloat(),
paint
)
return bitmap
}
这里创建了一个大小相同的位图,并将其填充为蓝色。
然后将两张图片进行融合,代码如下
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
val layerId= canvas?.saveLayer(0f,0f,width.toFloat(),height.toFloat(),null,Canvas.ALL_SAVE_FLAGS)
canvas?.drawBitmap(dstBitmap!!,0f,0f,paint)
paint.setXfermode(PorterDuffXfermode(PorterDuff.Mode.SRC_IN))
canvas?.drawBitmap(srcBitmap!!,mWidth*1.0f/2,mHeight*1.0f/2,paint)
paint.setXfermode(null)
canvas?.restoreToCount(layerId)
}
我们首先在(0,0)位置画出圆形图像,然后设置PorterDuffXfermode的模式为SRC_IN,之后在圆形图像的中心为左上角,画出方形。
在设置PorterDuffXfermode前画出的图像叫做目标图像,在设置之后画出的图像成为源图像。
该示例的效果如下
对于Mode.SRC_IN模式,它的计算公式为[Sa * Da,Sc * Da]。可以看出结果值的透明度和颜色值都是由Sa、Sc分别乘以目标图像的Da来计算的。当目标图像为空白像素时,计算的结果也为空白像素;当目标图像不透明时,相交区域显示源图像像素。所以从结果看到,源图像和目标图像相交部分显示源图像;而对于不想交区域,因为目标图像的透明度为0,所以不显示源图像。
从上面的例子可以看出,我们要关注两个区域:区域一(两图像相交的部分),区域二(源图像的非相交部分)
1.2、颜色叠加相关模式
颜色叠加相关的模式有如下几种:Mode.ADD(饱和度叠加)、Mode.LIGHTEN(变亮)、Mode.DARKEN(变暗)、Mode.MULTIPLY(正片叠底)、Mode.OVERLAY(叠加)、Mode.SCREEN(滤色)
1.2.1、Mode.ADD(饱和度叠加)
对应的算法如下:
Saturate(S+D)
ADD模式其实是对SRC和DST两张图片相交的区域的饱和度进行相加。
将上面例子的混合模式改为Mode.ADD,效果图如下
image.png
从效果中可以看出源图像和目标图像不相交的部分饱和度未发生变化,因为不想交的部分只有一方的饱和度为100,另一方为0,所以不会发生变化。
1.2.2、Mode.LIGHTEN(变亮)
对应的算法如下:
[Sa+ Da - Sa*Da,Sc*(l - Da) + Dc*(l - Sa) + max(Sc , Dc)]
变亮模式的效果图如下
image.png
从上面公式可知,只有两张图片相交的部分才会有颜色的变化,其他区域不变。
1.2.3、Mode.DARKERN(变暗)
对应的公式如下
[Sa+ Da - Sa*Da , Sc*(l - Da) + Dc* (l - Sa) + min(Sc , Dc)]
效果如下
image.png
从上面公式可知,只有两张图片相交的部分才会有颜色的变化,其他区域不变。
1.2.4、Mode.MULTIPLY(正片叠底)
对应的公式如下
[Sa * Da , Sc * Dc]
效果如下
image.png
源图像和目标图像非相交的部分的目标图像的alpha为0,所以这个区域为透明。
1.2.4、Mode.OVERLAY(叠加)
Google没有给出对应的公式,效果图如下
image.png
虽然没有给出公式,但从效果图中可以看到,源图像的相交区域有效果,非相交区域依然是存在的。这就可以肯定 点:当目标图像透明时,在这种模式下,源图像的色值不会受到影响。
1.2.5、Mode.SCREEN(滤色)
对应的算法如下:
[Sa + Da - Sa * Da , Sc + Dc - Sc * Dc)
效果如下图所示。
image.png
同样,只是源图像与目标图像的相交区域有效果,源图像的非相交区域保持原样
到这里 ,这6种混合模式就讲完了,下面总结一下。
- 1、除了 Mode.MULTIPLY (正片叠底)会在目标图像透明时将结果对应区域置为透明。其他模式图像都不受目标图像透明像素的影响,即源图像的非相交区域保持原样。
- 2、在考虑混合模式时,一般只考虑两种:第一 ,像 区域一 一样的两个不透明区域的混合;第二,像区域二一样的与完全透明区域的混合。对于与半透明区域的混合 在实战中般是用不到的。
示例:实现Twitter的描边效果
效果图如下:
image.png
从上图可以看出,只有使用正片叠底的混合模式,两张图片中一方透明,结果像素才是透明的,所以这里使用正片叠底来实现。
private val dstBitmap by lazy {
BitmapFactory.decodeResource(context.resources, R.mipmap.twiter_bg)
}
private val srcBitmap by lazy {
BitmapFactory.decodeResource(context.resources, R.mipmap.twiter_light)
}
private val multiply by lazy {
PorterDuffXfermode(PorterDuff.Mode.MULTIPLY)
}
private val paint = Paint()
init {
setLayerType(LAYER_TYPE_SOFTWARE, null)
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
val layerId= canvas?.saveLayer(0f,0f,width.toFloat(),height.toFloat(),null,Canvas.ALL_SAVE_FLAGS)
canvas?.drawBitmap(dstBitmap, 0f, 0f, paint)
paint.setXfermode(multiply)
canvas?.drawBitmap(srcBitmap, 0f, 0f, paint)
paint.setXfermode(null)
canvas?.restoreToCount(layerId)
}
1.3、PorterDuffXfermode之源图像模式
当在处理图像相交时,需要显示源图像,就需要从如下几种源图像模式中选取相应的模式了。源图像模式有Mode.SRC、Mode.SRC_IN、 Mode.SRC_OUT、Mode.SRC _OVER、Mode.SRC_ATOP。
1.3.1、Mode.SRC
对应算法如下
[Sa , Sc]
效果图如下
image.png
1.3.2、Mode.SRC_IN
对应的算法如下
[Sa * Da , Sc * Da]
有公式可知源图像和目标图像不相交的区域,源图像的alpha为0,所以不显示。
效果如下
image.png
SRC_IN是利用目标图像的透明度来改变源图像的透明度和饱和度的,当目标图像的透明度为0时,源图像就不显示。
示例一:实现图片的圆角效果
先来看下效果图
image.png
代码如下:
private val dstBitmap by lazy {
BitmapFactory.decodeResource(context.resources, R.mipmap.dog_shade)
}
private val srcBitmap by lazy {
BitmapFactory.decodeResource(context.resources, R.mipmap.dog)
}
private val srcIn by lazy {
PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
}
private val paint = Paint()
init {
setLayerType(LAYER_TYPE_SOFTWARE, null)
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
val layerId= canvas?.saveLayer(0f,0f,width.toFloat(),height.toFloat(),null,Canvas.ALL_SAVE_FLAGS)
canvas?.drawBitmap(dstBitmap, 0f, 0f, paint)
paint.setXfermode(srcIn)
canvas?.drawBitmap(srcBitmap, Rect(0,0,srcBitmap.width,srcBitmap.height),
Rect(0,0,dstBitmap.width,dstBitmap.height), paint)
paint.setXfermode(null)
canvas?.restoreToCount(layerId)
}
示例二:实现图片倒影效果
先来看下效果图
image.png
由于要显示小狗,所以小狗图是源图像,遮罩是目标图像,遮罩是一个透明度从49%-0%的图像。实现起来也很简单,首先绘制出小狗的图像,然后向下平移画布,使用SRC_IN的混合模式,融合旋转180°后的小狗图像和遮罩图即可,代码如下:
private val bakBitmap by lazy {
BitmapFactory.decodeResource(context.resources, R.mipmap.dog_invert_shade)
}
private val dogBitmap by lazy {
BitmapFactory.decodeResource(context.resources, R.mipmap.dog)
}
private val invertBitmap by lazy {
val matrix = Matrix()
matrix.setScale(1f, -1f)
Bitmap.createBitmap(dogBitmap, 0, 0, dogBitmap.width, dogBitmap.height, matrix, true)
}
private val srcIn by lazy {
PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
}
private val paint = Paint()
init {
setLayerType(LAYER_TYPE_SOFTWARE, null)
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
val mWidth = width / 2
val mHeight = mWidth * bakBitmap.height / bakBitmap.width
val layerId= canvas?.saveLayer(0f,0f,width.toFloat(),height.toFloat(),null,Canvas.ALL_SAVE_FLAGS)
canvas?.drawBitmap(dogBitmap, null, Rect(0, 0, mWidth, mHeight), paint)
canvas?.translate(0f,mHeight.toFloat())
canvas?.drawBitmap(bakBitmap,null,Rect(0,0,mWidth,mHeight),paint)
paint.setXfermode(srcIn)
canvas?.drawBitmap(invertBitmap,null,Rect(0,0,mWidth,mHeight),paint)
paint.setXfermode(null)
canvas?.restoreToCount(layerId)
}
1.3.3、Mode.SRC_OUT
对应的算法如下:
[Sa* (1 - Da), Sc* (1 - Da)]
效果图如下
image.png
SRC_OUT的特性:使用目标图像的透明度的补值来调节源图像的透明度和饱和度的,当目标图像为空白像素时就显示源图像;当目标图像为不透明时,相交区域就为空白像素。
示例:橡皮擦效果
先来看效果图
橡皮擦.gif
从效果图可以看出,手指滑动到哪里,小狗图片上指定指定位置的图像就隐藏了,我们可以使用混合模式SRC_OUT,该混合模式是使用目标图像透明度的补值来控制源图像的透明度的。这里可以将小狗图像作为源图像,手势轨迹为目标图像,当手指滑动绘制Path时,此处的目标图像的透明度为则为1,所以此处的源图像隐藏。具体代码如下:
class EraserView(context: Context, attrs: AttributeSet) : View(context, attrs) {
private val path = Path()
private val paint = Paint()
private val bitmap = BitmapFactory.decodeResource(context.resources, R.mipmap.scenery)
private var preX = -1f
private var preY = -1f
private var dstBitmap=Bitmap.createBitmap(bitmap.width,bitmap.height,Bitmap.Config.ARGB_8888)
init {
paint.style = Paint.Style.STROKE
paint.strokeWidth = 50f
paint.setColor(Color.RED)
setLayerType(LAYER_TYPE_SOFTWARE,null)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
event?.let {
when (it.action) {
MotionEvent.ACTION_DOWN -> {
path.moveTo(event.x, event.y)
preX = it.x
preY = it.y
invalidate()
return true
}
MotionEvent.ACTION_MOVE -> {
val endX = (preX + event.x) / 2
val endY = (preY + event.y) / 2
path.quadTo(preX, preY, endX, endY)
preX = event.x
preY = event.y
invalidate()
}
}
}
return super.onTouchEvent(event)
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
val layerId= canvas?.saveLayer(0f,0f,width.toFloat(),height.toFloat(),null,Canvas.ALL_SAVE_FLAGS)
val mCanvas = Canvas(dstBitmap)
mCanvas.drawPath(path, paint)
canvas?.drawBitmap(dstBitmap, 0f, 0f, paint)
paint.setXfermode(PorterDuffXfermode(PorterDuff.Mode.SRC_OUT))
canvas?.drawBitmap(bitmap, 0f,0f, paint)
paint.setXfermode(null)
canvas?.restoreToCount(layerId)
}
}
示例:刮刮卡效果
效果图如下
对上面橡皮擦示例代码的基础上,先绘制文字图片,在擦除图片后,文字就展示出来了。有两点需要注意:
- 1、在离屏缓存在绘制文字图片。
- 2、使用Canvas.saveLayer()新建图层,使用Canvas.save()无效。
具体代码如下:
class EraserView(context: Context, attrs: AttributeSet) : View(context, attrs) {
private val path = Path()
private val paint = Paint()
private val bitmap = BitmapFactory.decodeResource(context.resources, R.mipmap.scenery)
private var preX = -1f
private var preY = -1f
private var dstBitmap =
Bitmap.createBitmap(bitmap.width, bitmap.height, Bitmap.Config.ARGB_8888)
private val textBitmap=BitmapFactory.decodeResource(context.resources,R.mipmap.guaguaka_text)
init {
paint.style = Paint.Style.STROKE
paint.strokeWidth = 50f
paint.setColor(Color.RED)
setLayerType(LAYER_TYPE_SOFTWARE, null)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
event?.let {
when (it.action) {
MotionEvent.ACTION_DOWN -> {
path.moveTo(event.x, event.y)
preX = it.x
preY = it.y
invalidate()
return true
}
MotionEvent.ACTION_MOVE -> {
val endX = (preX + event.x) / 2
val endY = (preY + event.y) / 2
path.quadTo(preX, preY, endX, endY)
preX = event.x
preY = event.y
invalidate()
}
}
}
return super.onTouchEvent(event)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawBitmap(textBitmap, 0f, 0f, paint)
val layerId = canvas.saveLayer(0f,0f,width.toFloat(),height.toFloat(),paint,Canvas.ALL_SAVE_FLAG)
val mCanvas = Canvas(dstBitmap)
mCanvas.drawPath(path, paint)
canvas.drawBitmap(dstBitmap, 0f, 0f, paint)
paint.setXfermode(PorterDuffXfermode(PorterDuff.Mode.SRC_OUT))
canvas.drawBitmap(bitmap, 0f, 0f, paint)
paint.setXfermode(null)
canvas.restoreToCount(layerId)
}
}
1.3.4、Mode.SRC_OVER
对应的算法如下:
[Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc]
效果如下
image.png
结果图像的透明度为Sa + (1 - Sa)*Da,所以当源图像的透明度为100%时,源图像透明度不变原样显示,颜色计算也是一样。
1.3.5、Mode.SRC_ATOP
对应公式如下
[Da, Sc * Da + (1 - Sa) * Dc]
效果如下图
image.png
结果图像的透明度以目标图像透明度作为参考,当目标图像透明度为0,源图像透明度为0。
1.4、目标图像模式
我们知道,在与 SRC 相关的模式中,在处理相交区域时,优先以源图像显示为主;而在DST 相关的模式中,在处理相交区域时,优先以目标图像显示为主。这部分所涉及的模式Mode.DST 、Mode.DST_IN、 Mode.DST_ OUT 、Mode.DST_OVER、 Mode.DST_ ATOP
可以明显看出,源图像模式与目标图像模式所具有的模式是相同的,但一个以显示源图像为主,另一个以显示目标图像为主 所以,在能通过源图像模式实现的例子中,只需将源图像和目标图像对调下,就可以使用对应的目标图像模式来实现了。具体不再赘述。
1.5、Mode.CLEAR
对应算法如下
[0,0]
image.png
从效果图可以看到,计算结果是空白像素,源图像所在区域都会变成空白区域,这样就起到了清空源图像元素的效果。
2、总结
- 1、如果两张图片融合,需要生成颜色叠加特效,就需要从Mode.ADD (饱和度相 )、 Mode.DARKEN (变暗) Mode.LIGHTEN
(变亮〉、 Mode.MULTIPLY (正片叠底〉、 Mode.OVERLAY (叠加)、 Mode.SCREEN (滤色)模式中选择。 - 2、如果两张图片融合需要根据透明度来决定另一张图的显示还是隐藏,就需要从源图像模式或目标图像模式中选择。
- 3、当需要清空图像时,就需要使用Mode.CLEAR来完成。
网友评论