35

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.

Joseph Earl
  • 23,351
  • 11
  • 76
  • 89
  • Is simply caching the `Bitmap b` and `Canvas c` possible? Would it help? – John Dvorak Nov 23 '12 at 12:00
  • I don't think so -- if you just make the `b` and `c` say in `onSizeChanged` and not in `onDraw` then you end up drawing multiple times on the same canvas without clearing (so you'd end up with multiple layers). Actually it would work if I could clear `b` in `onDraw` which may well be possible -- I'll try it, thanks! – Joseph Earl Nov 23 '12 at 12:36
  • @Jan: Thanks this did work -- I created the Bitmap `b` and Canvas `c` in `onSizeChanged`, then I just needed to call `b.eraseColor(Color.Transparent)` before doing any drawing to erase any previous drawing on `c`. Post your comment as an answer and I'll mark it correct if I don't get any more useful answers in the next day or so. It still not as smooth as method 3, but much better than 2. – Joseph Earl Nov 23 '12 at 13:15
  • Doesn't `c.drawColor` have the effect of reseting the canvas, making `b.eraseColor` redundant? – John Dvorak Nov 23 '12 at 13:19
  • @Jan Not if the color drawn with `c.drawColor` is not completely opaque, which is more probable than not given the use case of this view ;) Method 4 my question is based on your answer. – Joseph Earl Nov 23 '12 at 13:23
  • There is good solution: http://stackoverflow.com/questions/14197823/draw-path-with-hole-android – Fedir Tsapana Jan 28 '16 at 21:28

1 Answers1

19

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);
John Dvorak
  • 26,799
  • 13
  • 69
  • 83
  • It's a bit late, however I do have one question about that. I want to do that exact same thing (hole in rectangle), however my problem is that my view needs to take up the whole screen, therefore the bitmap created will have the size of the screen which will be about 4 MBs - 10 MBs depending on the size of the screen. Creating such large bitmap might cause an OutOfMemoryError. Is there any workaround about that? – steliosf May 13 '17 at 17:13
  • @MScott I can't imagine that 10 MB would be any challenge for any today's smartphone to allocate. Don't forget that the GPU has to hold this amount of memory anyways just so that it can feed it to the LCD. Twice, most likely - one copy to display to the user, the other to be updated by software in the meantime. You could fiddle with painting four rectangles, one for each side, but it won't work that well with antialiasing if they are semi-transparent and it can only produce rectangular holes. – John Dvorak May 13 '17 at 17:21