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。

图1

图1

  在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那些部分是不需要绘制的。

  下面使用CanvasclipRect(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所示,可见重叠部分不会被绘制了。

图2

图2

  注意上面的例子在canvas.clipRect()前通过canvas.save()保存当前状态,并在canvas.clipRect()后通过canvas.restore()恢复到之前的状态。如果不使用canvas.restore()恢复canvas的状态,canvas.clipRect()的修改会一直生效,影响之后的绘制。