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:
which gets masked by a custom shape like this:
The resulting image should only let the shoe shine through and make the background transparent.
Implementation
I have tried to implement it this way:
- Make all black pixels in the mask transparent
- Override the View's
drawfunction to draw the mask over the original image using anxfermode(PorterDuffXfermode) ofDST_IN. (See Android docs forPorterDuff.Mode)
This works perfectly fine and gives me this image (green pixels mean transparent):
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:
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 bitmapmaskBitmap: The bitmap that contains white colors for the original shape to shine through, transparent for everything that should be transparentdrawingBitmap: An empty bitmap with the same size asimageBitmap, I use this for drawing only and then draw that result onto theimageBitmap(using a Canvas, see secondrunblock)lines: The lines I want to draw. A line consists of adrawMode(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:
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:
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!





