Punch a hole in a rectangle overlay with HW acceleration enabled on View

Joseph Earl picture Joseph Earl · Nov 23, 2012 · Viewed 10.4k times · Source

I have a view which does some basic drawing. After this I want to draw a rectangle with a hole punched in so that only a region of the previous drawing is visible. And I'd like to do this with hardware acceleration enabled for my view for best performance.

Currently I have two methods that work, but only works when disabled hardware acceleration and the other is too slow.

Method 1: SW Acceleration (Slow)

final int saveCount = canvas.save();

// Clip out a circle.
circle.reset();
circle.addCircle(cx, cy, radius, Path.Direction.CW);
circle.close();
canvas.clipPath(circle, Region.Op.DIFFERENCE);

// Draw the rectangle color.
canvas.drawColor(backColor);

canvas.restoreToCount(saveCount);

This does not work with hardware acceleration enabled for the view because 'canvas.clipPath' is not supported in this mode (I know i can force SW rendering, but I'd like to avoid that).

Method 2: HW Acceleration (V. Slow)

// Create a new canvas.
final Bitmap b = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
final Canvas c = new Canvas(b);

// Draw the rectangle colour.
c.drawColor(backColor);

// Erase a circle.
c.drawCircle(cx, cy, radius, eraser);

// Draw the bitmap on our views  canvas.
canvas.drawBitmap(b, 0, 0, null);

Where eraser is created as

eraser = new Paint()
eraser.setColor(0xFFFFFFFF);
eraser.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));

This is obviously slow -- a new Bitmap the size of the view is created every drawing call.

Method 3: HW Acceleration (Fast, does not work on some devices)

canvas.drawColor(backColor);
canvas.drawCircle(cx, cy, radius, eraser);

Same as the HW acceleration compatible method, but no extra canvas required. There is a major problem with this though -- it works with SW rendering forced, but on the HTC One X (Android 4.0.4 -- and probably some other devices) at least with HW rendering enabled it leaves the circle completely black. This is probably related to 22361.

Method 4: HW Acceleration (Acceptable, works on all devices)

As per Jan's suggestion for improving method 2, I avoided creating the bitmap in each call to onDraw, instead doing so in onSizeChanged:

if (w != oldw || h != oldh) {
    b = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
    c = new Canvas(b);
}

And then just used these in onDraw:

if (overlayBitmap == null) {
   b = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
   c = new Canvas(b);
}
b.eraseColor(Color.TRANSPARENT);
c.drawColor(backColor);
c.drawCircle(cx, cy, radius, eraser);
canvas.drawBitmap(b, 0, 0, null);

The performance is not as good as method 3, but much better than 2 and slightly better than 1.

The question

How can I achieve the same effect but do so in a manner compatible with HW acceleration (AND that works consistently on devices)? A method which increases the SW rendering performance would also be acceptable.

NB: When moving the circle around I am just invalidating a region -- not the entire canvas -- so there's no room for a performance improvement there.

Answer

John Dvorak picture John Dvorak · Nov 23, 2012

Instead of allocating a new canvas on each repaint, you should be able to allocate it once and then reuse the canvas on each repaint.

on init and on resize:

Bitmap b = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(b);

on repaint:

b.eraseColor(Color.TRANSPARENT);
    // needed if backColor is not opaque; thanks @JosephEarl
c.drawColor(backColor);
c.drawCircle(cx, cy, radius, eraser);

canvas.drawBitmap(b, 0, 0, null);