Android Performance: clipRect和quickReject
Android Framework会通过裁剪(Clipping)的方式避免重绘不可见的元素,以此来优化性能。但这一优化对于一些复杂的自定义View无效,如果自定义View重写了onDraw()
,系统无法知道View中各个元素的位置和层级关系,也就无法自动省略绘制不可见的元素。Canvas
提供了一些特殊的方法,可以用来向Android Framework告知Canvas
的哪些部分不可见、不需要绘制。其中最常用的方法是Canvas.clipRect()
,可以定义绘制的边界,边界以外的部分不会进行绘制。Canvas.quickReject()
可以用来测试指定区域是否在裁剪范围之外,如果要绘制的元素位于裁剪范围之外,就可以直接跳过绘制步骤。
下面的示例代码可以在这里找到。在开发者模式中打开Debug GPU overdraw,运行结果如图1所示,三个自定义View叠放在一起,重叠部分出现Overdraw。
在DroidCardsView.java中可以看到自定义View的绘制方法:
protected void onDraw(Canvas canvas) { super.onDraw(canvas); // Don't draw anything until all the Asynctasks are done and all the DroidCards are ready. if (mDroids.length > 0 && mDroidCards.size() == mDroids.length) { // Loop over all the droids, except the last one. for (int i = 0; i < mDroidCards.size(); i++) { // Each card is laid out a little to the right of the previous one. mCardLeft = i * mCardSpacing; drawDroidCard(canvas, mDroidCards.get(i), mCardLeft, 0); } } // Invalidate the whole view. Doing this calls onDraw() if the view is visible. invalidate(); }
这里只是按顺序依次绘制各个卡片,上面的卡片会盖住下面的卡片,卡片间重叠的部分也会进行绘制,发生Overdraw。由于重写了onDraw()
,Android Framework无法自动对绘制进行优化,需要手动告诉Android Framework那些部分是不需要绘制的。
下面使用Canvas的clipRect(float left, float top, float right, float bottom)
手动指定被覆盖的卡片区域:
protected void onDraw(Canvas canvas) { super.onDraw(canvas); // Don't draw anything until all the Asynctasks are done and all the DroidCards are ready. if (mDroids.length > 0 && mDroidCards.size() == mDroids.length) { // Loop over all the droids, except the last one. int i; for (i = 0; i < mDroidCards.size() - 1; i++) { // Each card is laid out a little to the right of the previous one. mCardLeft = i * mCardSpacing; // Save the canvas state. canvas.save(); // Restrict the drawing area to only what will be visible. canvas.clipRect( mCardLeft, 0, mCardLeft + mCardSpacing, mDroidCards.get(i).getHeight() ); // Draw the card. Only the parts of the card that lie within the bounds defined by // the clipRect() get drawn. drawDroidCard(canvas, mDroidCards.get(i), mCardLeft, 0); // Revert canvas to non-clipping state. canvas.restore(); } // Draw the final card. This one doesn't get clipped. drawDroidCard(canvas, mDroidCards.get(mDroidCards.size() - 1), mCardLeft + mCardSpacing, 0); } // Invalidate the whole view. Doing this calls onDraw() if the view is visible. invalidate(); }
首先计算卡片左边的位置mCardLeft
;然后调用canvas.save()
保存canvas
的当前状态;接着通过canvas.clipRect()
指定要绘制的区域,mCardLeft + mCardSpacing
就是每张卡片露出来的宽度;使用drawDroidCard()
进行绘制后,调用canvas.restore()
撤销之前canvas.clipRect()
的配置。注意顶部的卡片是完全可见的,需要完整地绘制出来。
修改后的效果如图2所示,可见重叠部分不会被绘制了。
注意上面的例子在canvas.clipRect()
前通过canvas.save()
保存当前状态,并在canvas.clipRect()
后通过canvas.restore()
恢复到之前的状态。如果不使用canvas.restore()
恢复canvas
的状态,canvas.clipRect()
的修改会一直生效,影响之后的绘制。