Simon Simon - 2 months ago 21
Android Question

Create bitmap mask programmatically

I have this code in onDraw().

radius = drawGmpImage(this.gmpImage, canvas);
canvas.drawCircle(kHorizontalOffset, kScreenVerticalOffset, radius , maskPaint);


drawGmpImage creates a complex graphic which is a circle with many lines drawn on it. It's a library function which I cannot change. The lines are polygons and can extend beyond the circumference of the circle.

The need is to "blank out" everything drawn outside the circle.

This is a port from iOS and the original developers solution is to use a simple bitmap mask, stored as a resource, with a transparent circle which matches the size of the circle drawn. Simply drawing the bitmap over the drawn circle has the desired effect but is not an option on Android as I need to support all possible resolutions and ratios.

Therefore, the canvas.drawCircle() call is the beginning of my attempt to mask out everything outside the circle. It works fine in that a filled circle is drawn over my drawn circle so that the only thing left are the polygon lines outside the drawn circles circumference. Radius is the radius of the drawn circle.

How can I invert this so that I am left with the contents of the circle?

Answer Source

Why is it that you can spend hours working on something, give up, ask the question, then stumble upon the answer 20 minutes later? The joys of life.

Path path = new Path();
path.addCircle(kHorizontalOffset, kScreenVerticalOffset, radius, Path.Direction.CW);
canvas.clipPath(path);

I'd missed the clipPath method which will take any path and use it as a clipping region. Adding my masking circle to the path does exactly what I need.

[EDIT]

This works well, but there is a problem. It doesn't work if hardware acceleration is turned on. I could turn acceleration off, but then I lose a lot of performance in the rest of the draw which is complex.

Here's how I finally solved it:

In onSizeChanged(), create a bitmap mask. I draw a transparent circle in the right place on the bitmap using this paint. The key to is to use a PorterDuffXfermode.

 maskPaint = new Paint();
 maskPaint.setColor(Color.TRANSPARENT);
 maskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
 maskPaint.setStyle(Paint.Style.FILL);

then create the bitmap

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {

    super.onSizeChanged(w, h, oldw, oldh);

    createMask(w,h,this.radius);

}

private void createMask(int w,int h, int radius){

    if (mask!=null){mask.recycle();}

    mask = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
    Canvas maskCanvas = new Canvas(mask);
    maskCanvas.drawCircle(w, h, radius, maskPaint);

}

Then in onDraw(), I simply draw the mask over the entire view:

@Override
protected void onDraw(Canvas canvas){

    // draw the image();
    setRadius(drawGmpImage(this.gmpImage, canvas));
    canvas.drawCircle(kHorizontalOffset, kScreenVerticalOffset, radius , maskPaint);

    // overlay the mask bitmap
    if (mask != null) {
        canvas.drawBitmap(mask, 0f, 0f, bitmapPaint);
    }

If the radius changes, the mask is recreated:

 private void setRadius(int radius){

     this.radius = radius;
     createMask(kHorizontalOffset, kScreenVerticalOffset, radius);

 }