自定义View②Xfermode

422 阅读2分钟

为什么要用 Xfermode?

为了把多次绘制进行「合成」,例如蒙版效果:用 A 的形状和 B 的图案

怎么做

  • Canvs.saveLayer() 把绘制区域拉到单独的离屏缓冲里
  • 绘制 A 图形
  • 用 Paint.setXfermode() 设置 Xfermode
  • 绘制 B 图形
  • 用 Paint.setXfermode(null) 恢复 Xfermode
  • 用 Canvas.restoreToCount() 把离屏缓冲中的合成后的图形放回绘制区域

为什么要用 saveLayer() 才能正确绘制 ?

为了把需要互相作用的图形放在单独的位置来绘制,不会受 View 本身的影响。 如果不使用 saveLayer(),绘制的目标区域将总是整个 View 的范围,两个图形 的交叉区域就错误了

画一个圆形头像

我们可以先画一个方形的头像,像这样
10

canvas.drawBitmap(getAvatar(IMAGE_WIDTH.toInt()),IMAGE_PADDING,IMAGE_PADDING,paint)

为了能够画出一个圆角头像,我们需要使用Xfermode

  • 首先我们要画一个圆形(源图形),直径跟方形头像边长一致,它是透明
  • 接下来使用SRC_IN设置paint的Xfermode
  • 然后画一个方形头像(目标图形)
  • 现在我们知道,头像的四个角并没有被实心原型盖住,对于SRC_IN来讲,圆形在方形头像中的展示,那么就会展示出一个圆形,这四个角是透明的,所以不会被绘制出来,那么最后的效果就是圆形头像了 11

完整代码如下

private val IMAGE_WIDTH = 200f.px
private val IMAGE_PADDING = 100f.px
private val XFERMODE = PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
class AvatarView(context: Context,attributeSet: AttributeSet): View(context,attributeSet) {

  private val paint = Paint(Paint.ANTI_ALIAS_FLAG);
  private val bounds = RectF(IMAGE_PADDING,IMAGE_PADDING,IMAGE_PADDING+ IMAGE_WIDTH,IMAGE_PADDING+IMAGE_WIDTH)

  @RequiresApi(VERSION_CODES.LOLLIPOP)
  override fun onDraw(canvas: Canvas) {
    val count = canvas.saveLayer(bounds,null);
    canvas.drawOval(IMAGE_PADDING,IMAGE_PADDING,IMAGE_PADDING+ IMAGE_WIDTH,IMAGE_PADDING+IMAGE_WIDTH,paint)
    paint.xfermode = XFERMODE
    canvas.drawBitmap(getAvatar(IMAGE_WIDTH.toInt()),IMAGE_PADDING,IMAGE_PADDING,paint)
    paint.xfermode = null
    canvas.restoreToCount(count)
  }

  fun getAvatar(width: Int): Bitmap {
    val options = BitmapFactory.Options()
    options.inJustDecodeBounds = true
    BitmapFactory.decodeResource(resources, R.mipmap.slmh, options)
    options.inJustDecodeBounds = false
    options.inDensity = options.outWidth
    options.inTargetDensity = width
    return BitmapFactory.decodeResource(resources, R.mipmap.slmh, options)
  }

}

XfermodeView的其他效果

  1. 先画一个目标图形红色圆形,使用canvas.drawOval的方式
  2. 再画一个源图形蓝色方形,使用canvas.drawRect的方式
  3. 绘制的时候变换Xfermode,看下效果

完整代码如下

private val XFERMODE = PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
class XfermodeView(context: Context,attributeSet: AttributeSet): View(context,attributeSet) {

  private val paint = Paint(Paint.ANTI_ALIAS_FLAG);
  private val bounds = RectF(50f.px,50f.px,300f.px,200f.px)
  private val circleBitmap = Bitmap.createBitmap(150f.px.toInt(),150f.px.toInt(),Bitmap.Config.ARGB_8888)
  private val squareBitmap = Bitmap.createBitmap(150f.px.toInt(),150f.px.toInt(),Bitmap.Config.ARGB_8888)


  init {
    val canvas = Canvas(circleBitmap)
    paint.color = Color.parseColor("#D81B60")
    //1 目标图形红色圆形
    canvas.drawOval(50f.px, 0f.px, 150f.px, 100f.px, paint)
    paint.color = Color.parseColor("#2196F3")
    //2 源图形蓝色方形
    canvas.setBitmap(squareBitmap)
    canvas.drawRect(0f.px, 50f.px, 100f.px, 150f.px, paint)
  }

  @RequiresApi(VERSION_CODES.LOLLIPOP)
  override fun onDraw(canvas: Canvas) {
    //3 绘制的时候变换xFermode
    val count = canvas.saveLayer(bounds,null);
    canvas.drawBitmap(circleBitmap,150f.px,50f.px,paint)
    paint.xfermode = XFERMODE
    canvas.drawBitmap(squareBitmap,150f.px,50f.px,paint)
    paint.xfermode = null
    canvas.restoreToCount(count)

  }

这里SRC_IN 也就是Source In 字面意思可以理解为源图片蓝色方形在目标图片红色原型中的展示,所以最后效果是一个蓝色90度扇形

更多效果可以参照下图理解
官方文档链接