使用视图中启用硬件加速的矩形叠加层打一个洞

35
我有一个视图,它进行一些基本的绘制。之后,我想要画一个带有孔洞的矩形,以便只有先前绘制区域可见。我希望在启用硬件加速的情况下为我的视图提供最佳性能。
目前我有两种方法可以实现,但只有在禁用硬件加速时才有效,另一种则太慢。
方法1:软件加速(慢)
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);

这不支持视图开启硬件加速,因为在此模式下不支持“canvas.clipPath”(我知道可以强制使用SW渲染,但我想避免这种情况)。
方法2:硬件加速(非常缓慢)
// 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);

橡皮擦被创建为

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

这显然很慢——每次绘制调用都会创建一个新的与视图大小相同的Bitmap
方法三:硬件加速(快速,但在某些设备上不起作用)。
canvas.drawColor(backColor);
canvas.drawCircle(cx, cy, radius, eraser);

与硬件加速兼容的方法相同,但不需要额外的画布。然而,这种方法存在一个主要问题——它在强制使用软件渲染时可以运行,但在至少HTC One X(Android 4.0.4——可能还有其他设备)启用硬件渲染后,它会使圆形完全变黑。这可能与22361相关。
方法4:硬件加速(可接受,在所有设备上都有效)。根据Jan的建议改进方法2,我避免了在每次调用onDraw时创建位图,而是在onSizeChanged中创建。
if (w != oldw || h != oldh) {
    b = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
    c = new Canvas(b);
}

然后只需在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);

这种方法的性能不如第三种,但比第二种好得多,略优于第一种。

问题

我该如何以与硬件加速兼容的方式实现相同的效果(并且在设备上始终有效)?增加软件渲染性能的方法也是可以接受的。

NB: 当移动圆圈时,我只是使一个区域失效--而不是整个画布--因此在那里没有性能提升的空间。


仅仅缓存 Bitmap bCanvas c 是可能的吗?这样做会有帮助吗? - John Dvorak
我认为不行 - 如果你只是让onSizeChanged中的bc进行更新而不是在onDraw中,那么你最终会在同一画布上多次绘制而不清除(因此你会得到多个图层)。实际上,如果我可以在onDraw中清除b,那么它就可以工作,这很可能是可行的 - 我会尝试一下,谢谢! - Joseph Earl
@Jan:谢谢,这个方法确实有效——我在onSizeChanged中创建了位图b和画布c,然后在进行任何绘制之前只需要调用b.eraseColor(Color.Transparent)来擦除c上的任何先前绘制。如果我在接下来的一天左右没有得到更有用的答案,我会将您的评论标记为正确答案。虽然它仍不如第三种方法平滑,但比第二种好多了。 - Joseph Earl
c.drawColor 不是已经重置了画布吗?这样 b.eraseColor 就变得多余了吧? - John Dvorak
@Jan 如果使用c.drawColor绘制的颜色不是完全不透明的话,那么这种情况更有可能发生;) 方法4是基于您的答案提出的问题。 - Joseph Earl
有一个很好的解决方案: https://dev59.com/NmYq5IYBdhLWcg3wxjg7 - Fedir Tsapana
1个回答

19

不要在每次重绘时分配新画布,而是应该在一开始分配它,然后在每次重绘时重复使用画布。

在初始化和调整大小时:

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

在重绘时:

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);

有点晚了,但我有一个关于那个的问题。我想做同样的事情(矩形中的洞),但我的问题是我的视图需要占据整个屏幕,因此创建的位图将具有屏幕大小,这将约为 4 MB - 10 MB,具体取决于屏幕的大小。创建如此大的位图可能会导致 OutOfMemoryError。有什么解决方法吗? - steliosf
@MScott 我无法想象10 MB对于今天的任何智能手机来说都是一个挑战。别忘了GPU必须持有这么多的内存,以便它可以将其提供给液晶显示器。很可能是两倍 - 一份副本显示给用户,另一份由软件在此期间更新。你可以尝试绘制四个矩形,每个边一个,但如果它们是半透明的,并且只能产生矩形孔,则效果不会那么好。 - John Dvorak

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接