Android Canvas PorterDuff not masking correctly

Viewed 296

What I'm trying to do

I'm trying to implement a view that can be masked. Let's say the user has an image like this:

enter image description here

which gets masked by a custom shape like this:

mask image

The resulting image should only let the shoe shine through and make the background transparent.

Implementation

I have tried to implement it this way:

  1. Make all black pixels in the mask transparent
  2. Override the View's draw function to draw the mask over the original image using an xfermode (PorterDuffXfermode) of DST_IN. (See Android docs for PorterDuff.Mode)

This works perfectly fine and gives me this image (green pixels mean transparent):

enter image description here

While this works perfectly fine, I couldn't implement a custom "drawing" functionality to let the user draw or erase the mask. I only succeeded in doing one or the other, but not both at the same time. This is what I achieved right now:

enter image description here

note that erasing the mask works as expected, but trying to extend the mask doesn't work and paints white pixels (instead of letting the original image (shoe) shine through).

This is the code I'm using right now:

  override fun draw(baseCanvas: Canvas) {
    super.draw(baseCanvas)

    val image = imageBitmap
    val mask = maskBitmap
    val drawingBitmap = drawingBitmap

    if (image != null && mask != null && drawingBitmap != null) {
      run {
        val canvas = Canvas(drawingBitmap)

        // 1. Fill with white
        canvas.drawColor(Color.WHITE)

        // 2. Draw mask and only let non-transparent pixels through
        val paint = Paint(Paint.ANTI_ALIAS_FLAG)
        paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN)
        canvas.drawBitmap(mask, 0f, 0f, paint)

        // 3. Draw all lines point to point with white color and custom xfermode
        paint.xfermode = null
        paint.style = Paint.Style.STROKE
        paint.color = Color.WHITE
        paint.strokeWidth = penSize
        paint.isDither = true
        paint.strokeJoin = Paint.Join.ROUND
        paint.strokeCap = Paint.Cap.ROUND
        paint.pathEffect = CornerPathEffect(10f)
        paint.isAntiAlias = true

        lines.forEach { line ->
          paint.xfermode = when (line.drawMode) {
            DrawMode.ERASE -> PorterDuffXfermode(PorterDuff.Mode.DST_OUT)
            DrawMode.DRAW -> PorterDuffXfermode(PorterDuff.Mode.SRC)
          }

          val path = Path().also { path ->
            val points = line.points
            val range = points.size - 1
            for (i in 1..range) {
              path.moveTo(points[i - 1].x, points[i - 1].y)
              path.lineTo(points[i].x, points[i].y)
            }
          }
          canvas.drawPath(path, paint)
        }
      }

      run {
        val canvas = Canvas(image)
        val paint = Paint(Paint.ANTI_ALIAS_FLAG)

        paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_ATOP)
        canvas.drawBitmap(drawingBitmap, 0f, 0f, paint)
      }
    }
  }

Important variables in scope:

  • imageBitmap: The shoe bitmap
  • maskBitmap: The bitmap that contains white colors for the original shape to shine through, transparent for everything that should be transparent
  • drawingBitmap: An empty bitmap with the same size as imageBitmap, I use this for drawing only and then draw that result onto the imageBitmap (using a Canvas, see second run block)
  • lines: The lines I want to draw. A line consists of a drawMode (draw or erase) and a list of all points I have tracked.

Weird Observation

The thing that's confusing me right now is that when I fill the canvas black before adding the image:

      run {
        val canvas = Canvas(image)
        canvas.drawColor(Color.BLACK) // <-- ADD THIS HERE

        val paint = Paint(Paint.ANTI_ALIAS_FLAG)

        paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_ATOP)
        canvas.drawBitmap(drawingBitmap, 0f, 0f, paint)
      }

it looks like it's working fine:

enter image description here

Same result if I fill the canvas RED, it lets the Red color shine through instead of Black. Why does it work with the color, but not the original bitmap?

It even looks correct if inspected in the debugger:

enter image description here

Question

Does anyone here know why this does not work as I expect it to work? I tried to play around with all kinds of different PorterDuff modes, but couldn't manage to get it working smoothly.

Any help appreciated!

1 Answers

Okay I got it working. I'm not sure if there's a more efficient way to achieve this, but this is what I did:

  1. Create a fourth Bitmap called imageDrawingBitmap.
  2. Set imageDrawingBitmap to the actual ImageView
  3. As in the code snippet above, first draw the mask, then paths ontop of that
  4. Then create a Canvas for the imageDrawingBitmap
  5. Draw original image in that Canvas
  6. Draw mask (maskDrawingBitmap from step 3.) in that Canvas with PorterDuff DST_ATOP.

That seems to be working fine.

Related