ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
[TOC] # 1. 前言 混合模式能够将两张图片无缝结合,实现类似 Photoshop中的两张图片融合效果。通过 Paint 类中的 setXfermode(Xfermode xfermode)函数实现的,它 的参数 Xfermode 是一个空类,主要靠它的子类来实现不同的功能。主要子类有:AvoidXfermode、PixelXorXfermode、PorterDuffxfermode。 ## 1.1 注意点 AvoidXfermode、PixelXorXfermode 是完全不 支持硬件加速的,PorterDuffXfermode 是部分不支持硬件加速。所以,在使用 Xfermode 时,为了保险需要做到: * 禁用硬件加速,即:setLayerType(View.LAYER\_TYPE\_SOFTWARE, null); * 使用离屏绘制;即:把绘制的核心代码放在 canvas.save()和 canvas.restore()函数之间即可。 对于离屏绘制,也就是使用: ```java //新建图层 int layerId = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas. ALL\_SAVE\_FLAG); //核心绘制代码 ... //还原图层 canvas.restoreToCount(layerId); ``` 这里以PorterDuffXfermode为案例来学习。 # 2. PorterDuffXfermode 继承关系图为: ![](https://img.kancloud.cn/33/9c/339ce6a0d2ef9c4d5b1d02266b29d5a5_401x163.png) 简单追踪其源码,可以看到仅提供了一个公开构造方法: ~~~ public class PorterDuffXfermode extends Xfermode { public PorterDuffXfermode(PorterDuff.Mode mode) { porterDuffMode = mode.nativeInt; } } ~~~ 而其父类Xfermode也是异常简单: ~~~ public class Xfermode { static final int DEFAULT = PorterDuff.Mode.SRC_OVER.nativeInt; @UnsupportedAppUsage int porterDuffMode = DEFAULT; } ~~~ 也就是其实重点在于[PorterDuff.Mode](https://developer.android.google.cn/reference/kotlin/android/graphics/PorterDuff.Mode)这个枚举类型。这里直接查阅官方文档,很有意思的是,PorterDuffXfermode混合模式的命名就来源于作者Thomas Porter和Tom Duff,在1984年他们发表的论文《Compositing Digital Images》(《数字合成图像》)中研究了12中合成操作来控制源图像和目标图像的颜色混合结果,也被叫做Aplha通道合成模式。当然,在这个类中还提供了几种混合模式,且不限于alpha通道。分类如下: ![](https://img.kancloud.cn/b1/b6/b1b6f1ee93c7a6b0992c194a58d3228b_868x440.png) 比如在论文中作者展示了一张图片: ![](https://img.kancloud.cn/33/fb/33fbeba58df53037e40dd5eea6fd4052_819x732.png) 很难想象作者这个效果完成在1984年。确实让人大为惊叹。 为了便于理解其计算过程,这里按照文档说明进行。标记![](https://img.kancloud.cn/6b/20/6b20445748eb159a7192fd70f66890b3_45x27.png)为alpha通道输出,![Color_out](https://img.kancloud.cn/4e/97/4e977f2474515d55b44743978bd3c6db_46x32.png)为颜色值输出。下面是枚举类型中所有的模式: ## 2.1 ADD 两个部分相加: ![](https://img.kancloud.cn/db/48/db488a31224c243c5a00a47571605115_416x88.png) 简单来说就是对 SRC 与 DST 两张图片相交区域的饱和度进行相加。 ## 2.2 CLEAR 直接归零: ![](https://img.kancloud.cn/c5/eb/c5eb46ca4c70b6518f558e7df07c1aa5_144x85.png) ## 2.3 DARKEN 两个图片的重合区域有变暗的效果: ![](https://img.kancloud.cn/47/86/478640fb6f1f8a90e23cc251d9fc9d14_661x106.png) ## 2.4 DST 丢弃源图像,保留目标内: ![](https://img.kancloud.cn/fc/06/fc0662be4737106d9d4b4ebf0c1d70aa_188x93.png) * DST_ATOP; * DST_IN; * DST_OUT; * DST_OVER; ## 2.5 LIGHTEN 有重合 区域才有变亮的效果: ![](https://img.kancloud.cn/72/64/7264cd966415b956d3f667644e52f15b_680x101.png) ## 2.6 MULTIPLY 两部分的乘积,也就是是用源图像的 Alpha 值乘以目标 图像的 Alpha 值。由于源图像的非相交区域所对应的目标图像像素的 Alpha 值是 0,所以结果 像素的 Alpha 值仍是 0,源图像的非相交区域在计算后是透明的。与 Photoshop 中的**正片叠底**效果是一致的。 ![](https://img.kancloud.cn/0f/a4/0fa4b2b740ff5d1b394b70c1ba29db99_221x81.png) ## 2.7 SRC 在处理源图像所在区域的相交问题时,全部以源图像显示: ![](https://img.kancloud.cn/95/cd/95cdb116f48c1ffbc626c68436aef9a7_193x95.png) ### 2.7.1 SRC_ATOP; ### 2.7.2 SRC_IN 在这个公式中,目标值的透明度也是乘积形式计算,故而遇到空白像素的时候还是0: ![](https://img.kancloud.cn/a7/82/a78228aa2a50c4c0658bd0caf6bd0368_216x83.png) 故而我们可以用来做图片固定图形的裁切。比如下图: ![](https://img.kancloud.cn/7c/4c/7c4cfc3519af433e85053f0120df1940_842x257.png) 且由于SRC\_IN 模式是在相交时利用目标图像的透明度来改变源图像的透明度和饱和度的。故而,当目标图像的透明度在 0~255 之间时,就会把源图像的透明度和颜色值都变小。利用这 个特性,可以实现倒影效果。 ### 2.7.3 SRC_OUT; ### 2.7.4 SRC_OVER; ## 2.8 XOR 丢弃二者重叠部分像素,绘制剩余像素: ![](https://img.kancloud.cn/5a/cd/5acde2b4e965a124f799bd0478741c7e_480x104.png) ## 2.9 OVERLAY 根据目标颜色值来屏蔽源或者目标 ![](https://img.kancloud.cn/9a/2c/9a2c9988ca8b85c8065adba459957776_722x117.png) ## 2.10 SCREEN 将源像素和目标像素相加,然后将源像素与目标像素相减。可以达到**滤色**的目的。 ![](https://img.kancloud.cn/ee/77/ee770636b710ed17b79e8d5af282865a_360x85.png) # 3. 案例 比如首先准备两个图像,两个图像有重叠: ![](https://img.kancloud.cn/21/a0/21a00c1538bdf252032944e639ff1e47_185x126.png) 也就是简单的绘制矩形和平移、旋转画布: ~~~ override fun onDraw(canvas: Canvas?) { super.onDraw(canvas) canvas?.apply { // 绘制两个矩形 drawRect(mRect, mPaint) // 移动一下画布 val saveLayerId = saveLayer(0f, 0f, width.toFloat(), height.toFloat(), mPaint) translate(100f, 0f) rotate(45f, 300f, 300f) // 绘制另一个矩形 drawRect(mRect, mOtherPaint) // 恢复图层 restoreToCount(saveLayerId) } } ~~~ 下面开始测试,测试的时候,首先关闭硬件加速,即: ~~~ // 关闭硬件加速 setLayerType(View.LAYER_TYPE_SOFTWARE, null) ~~~ 但是,很不幸的是,由于这里的混合模式是两个图像之间的关系,而在上面的预备工作中我将之放置在了两个图层中,故而会导致在设置了画笔的Xfermode之后达不到预想的效果的。因为两个图像之间的计算应该在同一个图层中进行,事实上,经过了测试也是如此。故而这里修改为一个正方形和一个圆形的图案,即: ~~~ private fun drawRect(canvas: Canvas){ canvas.drawRect(mRect, mRectPaint) } private fun drawCircle(canvas: Canvas){ canvas.drawCircle(300f, 300f, 100f, mCirclePaint) } override fun onDraw(canvas: Canvas?) { super.onDraw(canvas) canvas?.apply { // this == canvas // 绘制目标 drawCircle(this) // 设置混合模式 mRectPaint.setXfermode(mXfermodes[6]) // 绘制另一个矩形 drawRect(this) // 清空混合模式 mRectPaint.setXfermode(null) } } ~~~ 当然,在初始化方法中初始了画笔和对应的模式: ~~~ private lateinit var mCirclePaint: Paint private lateinit var mRectPaint: Paint private lateinit var mRect: Rect private lateinit var mXfermodes: List<PorterDuffXfermode> private fun init() { mRect = Rect(200, 200, 400, 400) mRectPaint = Paint() mRectPaint.isAntiAlias = true mRectPaint.color = Color.RED mRectPaint.isDither = true mRectPaint.strokeWidth = 5f mRectPaint.style = Paint.Style.FILL mCirclePaint = Paint() mCirclePaint.isAntiAlias = true mCirclePaint.color = Color.BLUE mCirclePaint.isDither = true mCirclePaint.strokeWidth = 5f mCirclePaint.style = Paint.Style.FILL // 关闭硬件加速 setLayerType(View.LAYER_TYPE_SOFTWARE, null) // 初始化混合模式 mXfermodes = listOf<PorterDuffXfermode>( PorterDuffXfermode(PorterDuff.Mode.ADD), PorterDuffXfermode(PorterDuff.Mode.CLEAR), PorterDuffXfermode(PorterDuff.Mode.DST), PorterDuffXfermode(PorterDuff.Mode.DARKEN), PorterDuffXfermode(PorterDuff.Mode.LIGHTEN), PorterDuffXfermode(PorterDuff.Mode.SRC), PorterDuffXfermode(PorterDuff.Mode.XOR), PorterDuffXfermode(PorterDuff.Mode.SCREEN) ) } ~~~ 效果: ![](https://img.kancloud.cn/50/5e/505e93e39d789def36c231811b907d23_138x81.png) ## 3.1 圆形图片效果 因为混合模式主要是针对图片的模式,故而也可以用来进行图片的裁剪工作。比如这里我将一个图片裁剪为圆形。这里需要注意的是,因为需要保留的是图片内容,故而这里将加载的Bitmap图片设置为源图像,也就是先加载。对应代码: ~~~ /** * 绘制目标图,也就是要显示的部分的图 * @param canvas 画布 * @return 返回正方形图像的宽度 */ private fun drawSrcBitmap(canvas: Canvas): Int{ val resBitmap = BitmapFactory.decodeResource(resources, R.drawable.logo) canvas.drawBitmap(resBitmap, 200f, 200f, mRectPaint) return resBitmap.width } /** * 创建的遮罩也的是一个Bitmap对象 * @param width 正方形图片的宽度 * @return 返回创建的遮罩的Bitmap实例 */ private fun getMaskSrcBitmap(width: Int): Bitmap{ val center = width / 2f val bm = Bitmap.createBitmap(width, width, Bitmap.Config.ARGB_8888); val canvas = Canvas(bm) val paint = Paint(Paint.ANTI_ALIAS_FLAG) paint.color = Color.BLACK canvas.drawCircle(center, center, (width/2).toFloat(), paint) return bm } override fun onDraw(canvas: Canvas?) { super.onDraw(canvas) canvas?.apply { // this == canvas val saveLayerId = saveLayer(0f, 0f, width.toFloat(), height.toFloat(), mRectPaint) // 绘制目标 val bitmapWidth = drawSrcBitmap(this) // 设置混合模式 mRectPaint.xfermode = mXfermodes[7] // PorterDuffXfermode(PorterDuff.Mode.DST_IN) // 绘制圆 drawBitmap(getMaskSrcBitmap(bitmapWidth), 200f, 200f, mRectPaint) // 清空混合模式 mRectPaint.xfermode = null restoreToCount(saveLayerId) } } ~~~ 效果: ![](https://img.kancloud.cn/5e/12/5e12eaf211ad921edaed0bf63ed4d575_287x268.png) 这里需要注意的是,在进行图片裁剪的时候,需要的的遮罩层也是一个Bitmap图片,而不是直接绘制的圆形,比如下面的代码就是不行的: ~~~ private fun getMaskSrcBitmap2(canvas: Canvas, width: Int){ val center = width / 2f canvas.drawCircle(center, center, (width/2).toFloat(), mRectPaint) } ~~~ 如果遮罩层为上面的函数,然后将设置混合模式的一行代码注释掉,结果为: ![](https://img.kancloud.cn/45/fd/45fddecb638d508b5c5a1707cf18c972_309x301.png) 如果加上设置混合模式,结果无圆形裁剪效果: ![](https://img.kancloud.cn/c9/ad/c9ad8fa09889285e45053e6acc0d876c_265x252.png) 所以这里粗略得出结论:当绘制的为直接图形的时候,源和目标保持一致,也就是可以使用两个直接绘制的图形;也可以使用两个Bitmap图像。 ## 3.1 图片倒影效果 这里还是使用MULTIPLY来实现,可以利用MULTIPLY中乘积的特性来进行计算一个虚化的渐变图效果。步骤为: * 生成一个渐变、半透明的画布; * 使用canvas来在画布上绘制对应大小的bitmap; * 使用画笔的设置Xfermode来设置图像混合模式; 比如: ~~~ class PorterDuffXfermodeDemo2 : View { constructor(context: Context?) : super(context) { init() } constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { init() } constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super( context, attrs, defStyleAttr ) { init() } private lateinit var mRectPaint: Paint private lateinit var mRect: Rect private fun init() { mRect = Rect(200, 200, 400, 400) mRectPaint = Paint() mRectPaint.isAntiAlias = true mRectPaint.color = Color.RED mRectPaint.isDither = true mRectPaint.strokeWidth = 5f mRectPaint.style = Paint.Style.FILL // 关闭硬件加速 setLayerType(View.LAYER_TYPE_SOFTWARE, null) } /** * 创建一个渐变的半透明图层,用作蒙版 * @param width 正方形蒙版宽高 * @return 绘制了这个半透明图像的Bitmap对象 */ private fun createMaskBitmap(width: Int): Bitmap{ val tempBitmap = Bitmap.createBitmap(width, width, Bitmap.Config.ARGB_8888) val canvas = Canvas(tempBitmap) val paint = Paint(Paint.ANTI_ALIAS_FLAG) paint.color = Color.BLUE // 设置渐变,从底部到顶部 val x = width / 2 val linearGradient = LinearGradient( x.toFloat(), width.toFloat(), x.toFloat(), 0f, 0x000000ff.toInt(), 0x880000ff.toInt(), Shader.TileMode.CLAMP ) paint.setShader(linearGradient) canvas.drawRoundRect(0f, 0f, width.toFloat(), width.toFloat(), 8f, 8f, paint) return tempBitmap } override fun onDraw(canvas: Canvas?) { super.onDraw(canvas) canvas?.apply { // this == canvas val saveLayerId = saveLayer(0f, 0f, width.toFloat(), height.toFloat(), mRectPaint) // 绘制目标 // val bitmapWidth = drawSrcBitmap(this) val bitmap = createMaskBitmap(500) drawBitmap(bitmap, 100f, 100f, mRectPaint) restoreToCount(saveLayerId) } } } ~~~ 就可以得到一个半透明的图像,如下: ![](https://img.kancloud.cn/94/49/944980fb9837cb5997486b10f9d6ba99_178x152.png) 当然实际上这里我调整渐变颜色为白色,且调整了不透明度: ~~~ val linearGradient = LinearGradient( x.toFloat(), width.toFloat(), x.toFloat(), 0f, 0x00ffffff.toInt(), 0x33ffffff.toInt(), Shader.TileMode.CLAMP ) ~~~ 然后,使用图像的混合模式,指定为MULTIPLY,进行累乘像素: ~~~ override fun onDraw(canvas: Canvas?) { super.onDraw(canvas) canvas?.apply { // this == canvas val saveLayerId = saveLayer(0f, 0f, width.toFloat(), height.toFloat(), mRectPaint) // 绘制目标 val bitmapWidth = drawSrcBitmap(this) // 设置图像混合模式 mRectPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.MULTIPLY) // 创建图层蒙版 val bitmap = createMaskBitmap(bitmapWidth) // 绘制 drawBitmap(bitmap, 100f, 100f, mRectPaint) // 清空图像混合模式 mRectPaint.xfermode = null restoreToCount(saveLayerId) } } ~~~ 效果: ![](https://img.kancloud.cn/9e/19/9e19f98c6e98a57f281bc96c916febf0_282x277.png) ~~~ /** * 绘制目标图,也就是要显示的部分的图 * @param canvas 画布 * @return 返回正方形图像的宽度 */ private fun drawSrcBitmap(canvas: Canvas): Int{ val resBitmap = BitmapFactory.decodeResource(resources, R.drawable.logo) canvas.drawBitmap(resBitmap, 100f, 100f, mRectPaint) return resBitmap.width } ~~~ 然后需要做的就是加载原图,然后下一这一部分的效果,对其即可。这里就可以使用新建图层,然后移动画布,并镜像翻转画布来解决。比如: ~~~ override fun onDraw(canvas: Canvas?) { super.onDraw(canvas) canvas?.apply { // this == canvas // 因为这里两个地方需要,所以加载图片的放入了公共部分 // 绘制原图 canvas.drawBitmap(resBitmap, 100f, 100f, mRectPaint) // 新建图层 val saveLayerId = saveLayer(0f, 0f, width.toFloat(), height.toFloat(), mRectPaint) // 移动画布 translate(0f, resBitmap.width.toFloat()) // 镜像翻转画布 scale(1f, -1f, 100f + resBitmap.width / 2, 100f + resBitmap.width / 2) // 绘制目标 canvas.drawBitmap(resBitmap, 100f, 100f, mRectPaint) // 设置图像混合模式 mRectPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.MULTIPLY) // 创建图层蒙版 val bitmap = createMaskBitmap(resBitmap.width) // 绘制 drawBitmap(bitmap, 100f, 100f, mRectPaint) // 清空图像混合模式 mRectPaint.xfermode = null restoreToCount(saveLayerId) } } ~~~ ![](https://img.kancloud.cn/3c/ab/3cabd2977cfd35cf028c15da60883234_294x487.png) 当然,这里镜像的效果好坏其实就取决于生成得Mask的图像的好坏。所以也可以直接使用PS来生成一个无色到白色的渐变,可以降低不透明度设置为50%之类的,然后进行正片叠底。完整代码: ~~~ class PorterDuffXfermodeDemo2 : View { constructor(context: Context?) : super(context) { init() } constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { init() } constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super( context, attrs, defStyleAttr ) { init() } private lateinit var mRectPaint: Paint private lateinit var mRect: Rect private val resBitmap = BitmapFactory.decodeResource(resources, R.drawable.logo) private fun init() { mRect = Rect(200, 200, 400, 400) mRectPaint = Paint() mRectPaint.isAntiAlias = true mRectPaint.color = Color.RED mRectPaint.isDither = true mRectPaint.strokeWidth = 5f mRectPaint.style = Paint.Style.FILL // 关闭硬件加速 setLayerType(View.LAYER_TYPE_SOFTWARE, null) } /** * 加载背景图层 * @param canvas 画布 * @return 返回正方形图像的宽度 */ private fun loadBackgroundBitmap(canvas: Canvas){ val resBitmap = BitmapFactory.decodeResource(resources, R.drawable.bg) canvas.drawBitmap(resBitmap, 100f, 100f, mRectPaint) } /** * 创建一个渐变的半透明图层,用作蒙版 * @param width 正方形蒙版宽高 * @return 绘制了这个半透明图像的Bitmap对象 */ private fun createMaskBitmap(width: Int): Bitmap{ val tempBitmap = Bitmap.createBitmap(width, width, Bitmap.Config.ARGB_8888) val canvas = Canvas(tempBitmap) val paint = Paint(Paint.ANTI_ALIAS_FLAG) paint.color = Color.BLUE // 设置渐变,从底部到顶部 val x = width / 2 val linearGradient = LinearGradient( x.toFloat(), width.toFloat(), x.toFloat(), 0f, 0x44ffffff.toInt(), 0x00ffffff.toInt(), Shader.TileMode.CLAMP ) paint.setShader(linearGradient) canvas.drawRoundRect(0f, 0f, width.toFloat(), width.toFloat(), 8f, 8f, paint) return tempBitmap } override fun onDraw(canvas: Canvas?) { super.onDraw(canvas) canvas?.apply { // this == canvas // 因为这里两个地方需要,所以加载图片的放入了公共部分 // 绘制原图 canvas.drawBitmap(resBitmap, 100f, 100f, mRectPaint) // 新建图层 val saveLayerId = saveLayer(0f, 0f, width.toFloat(), height.toFloat(), mRectPaint) // 移动画布 translate(0f, resBitmap.width.toFloat()) // 镜像翻转画布 scale(1f, -1f, 100f + resBitmap.width / 2, 100f + resBitmap.width / 2) // 绘制目标 canvas.drawBitmap(resBitmap, 100f, 100f, mRectPaint) // 设置图像混合模式 mRectPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.MULTIPLY) // 创建图层蒙版 val bitmap = createMaskBitmap(resBitmap.width) // 绘制 drawBitmap(bitmap, 100f, 100f, mRectPaint) // 清空图像混合模式 mRectPaint.xfermode = null restoreToCount(saveLayerId) } } } ~~~