Android 从0开始自定义控件之 View 的 draw 过程 (九)

版权声明:欢迎转载,转载请注明出处。 如果本文帮助到你,本人不胜荣幸,如果浪费了你的时间,本人深感抱歉。如果有什么错误,请一定指出,以免误导大家、也误导我。感谢关注。 https://blog.csdn.net/Airsaid/article/details/53872349

转载请标明出处: http://blog.csdn.net/airsaid/article/details/53872349
本文出自:周游的博客

前言

前面已经了解了 View 三大流程的 measure 和 layout 过程,这一篇继续学习最后的 draw 过程。draw 的过程依旧是在 ViewRootImpl#performTraversals 方法中调用的,其调用顺序是在最后, 相较与 measure 和 layout 过程要简单的多,它的作用就是将 View 绘制到屏幕上面。

View 的绘制

下面直接通过查看 View#draw 源码,来分析下其 draw 过程:

public void draw(Canvas canvas) {
    ......

    /*
     * Draw traversal performs several drawing steps which must be executed
     * in the appropriate order:
     *
     *      1. Draw the background
     *      2. If necessary, save the canvas' layers to prepare for fading
     *      3. Draw view's content
     *      4. Draw children
     *      5. If necessary, draw the fading edges and restore layers
     *      6. Draw decorations (scrollbars for instance)
     */

    // Step 1, draw the background, if needed
    ......
    if (!dirtyOpaque) {
        drawBackground(canvas);
    }

    // skip step 2 & 5 if possible (common case)
    ......

    // Step 2, save the canvas' layers
    ......
    int solidColor = getSolidColor();
    if (solidColor == 0) {
        final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;

        if (drawTop) {
            canvas.saveLayer(left, top, right, top + length, null, flags);
        }

        if (drawBottom) {
            canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
        }

        if (drawLeft) {
            canvas.saveLayer(left, top, left + length, bottom, null, flags);
        }

        if (drawRight) {
            canvas.saveLayer(right - length, top, right, bottom, null, flags);
        }
    } else {
        scrollabilityCache.setFadeColor(solidColor);
    }

    // Step 3, draw the content
    if (!dirtyOpaque) onDraw(canvas);

    // Step 4, draw the children
    dispatchDraw(canvas);

    // Step 5, draw the fade effect and restore layers
    ......

    if (drawTop) {
        ......
        canvas.drawRect(left, top, right, top + length, p);
    }

    if (drawBottom) {
        ......
        canvas.drawRect(left, bottom - length, right, bottom, p);
    }

    if (drawLeft) {
        ......
        canvas.drawRect(left, top, left + length, bottom, p);
    }

    if (drawRight) {
        ......
        canvas.drawRect(right - length, top, right, bottom, p);
    }
    ......
    // Step 6, draw decorations (foreground, scrollbars)
    onDrawForeground(canvas);
}

上面的源码注释写的很清晰,通过查看后我们了解到 View 的绘制共分为如下六步:

  • 1:绘制背景。
  • 2:如果需要,保存图层信息。
  • 3:绘制 View 的内容。
  • 4:如果 View 有子 View,绘制 View 的子 View。
  • 5:如果需要,绘制 View 的边缘(如阴影等)。
  • 6:绘制 View 的装饰(如滚动条等)。

其中以上六步,第二步和第五步并不是必须的,所以我们只需重点分析其他四步即可。

绘制背景

绘制背景调用了 View#drawBackground 方法:

private void drawBackground(Canvas canvas) {
    // 获取背景 drawable
    final Drawable background = mBackground;
    if (background == null) {
        return;
    }
    // 根据在 layout 过程中获取的 View 的位置参数,来设置背景的边界
    setBackgroundBounds();

    .....

    // 获取 mScrollX 和 mScrollY值 
    final int scrollX = mScrollX;
    final int scrollY = mScrollY;
    if ((scrollX | scrollY) == 0) {
        background.draw(canvas);
    } else {
        // 如果 mScrollX 和 mScrollY 有值,则对 canvas 的坐标进行偏移
        canvas.translate(scrollX, scrollY);
        // 调用 Drawable 的 draw 方法绘制背景
        background.draw(canvas);
        canvas.translate(-scrollX, -scrollY);
    }
}

绘制内容

绘制内容调用了 View#onDraw 方法,由于 View 的内容各不相同,所以该方法是一个空实现,需要由子类去实现:

protected void onDraw(Canvas canvas) {
}

绘制子 View

绘制子 View 调用了 View#dispatchDraw 方法,该方法同样是一个空实现:

protected void dispatchDraw(Canvas canvas) {

}

当只有包含子类的时候,才会去重写它,ViewGroup 不正好是吗? 来看下 ViewGroup 对该方法的实现吧:

protected void dispatchDraw(Canvas canvas) {
    ......
    final int childrenCount = mChildrenCount;
    ......

    for (int i = 0; i < childrenCount; i++) {
            ......
            if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                    transientChild.getAnimation() != null) {
                more |= drawChild(canvas, transientChild, drawingTime);
            }
            ......
    }
}

ViewGroup#dispatchDraw 方法的代码比较多,只分析重点,遍历了所有的子 View 并调用了 ViewGroup#drawChild 方法:

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    return child.draw(canvas, this, drawingTime);
}

该方法最终还是调用了子 View 的 draw 方法,似曾相识啊,和上篇中的 layout 过程是一样呢。

由于 ViewGroup 已经为我们实现了该方法,所以我们一般都不需要重写该方法。

绘制装饰

绘制装饰调用了 View#onDrawForeground 方法,源码如下:

public void onDrawForeground(Canvas canvas) {
    onDrawScrollIndicators(canvas);
    onDrawScrollBars(canvas);

    final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
    if (foreground != null) {
        if (mForegroundInfo.mBoundsChanged) {
            mForegroundInfo.mBoundsChanged = false;
            final Rect selfBounds = mForegroundInfo.mSelfBounds;
            final Rect overlayBounds = mForegroundInfo.mOverlayBounds;

            if (mForegroundInfo.mInsidePadding) {
                selfBounds.set(0, 0, getWidth(), getHeight());
            } else {
                selfBounds.set(getPaddingLeft(), getPaddingTop(),
                        getWidth() - getPaddingRight(), getHeight() - getPaddingBottom());
            }

            final int ld = getLayoutDirection();
            Gravity.apply(mForegroundInfo.mGravity, foreground.getIntrinsicWidth(),
                    foreground.getIntrinsicHeight(), selfBounds, overlayBounds, ld);
            foreground.setBounds(overlayBounds);
        }

        foreground.draw(canvas);
    }
}

该方法默认实现是绘制了滚动指示器、滚动条、和前景。

setWillNotDraw

View 中有一个特殊的方法,setWillNotDraw(boolean willNotDraw):

/**
 * If this view doesn't do any drawing on its own, set this flag to
 * allow further optimizations. By default, this flag is not set on
 * View, but could be set on some View subclasses such as ViewGroup.
 *
 * Typically, if you override {@link #onDraw(android.graphics.Canvas)}
 * you should clear this flag.
 *
 * @param willNotDraw whether or not this View draw on its own
 */
public void setWillNotDraw(boolean willNotDraw) {
    setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}

该方法是用于设置 WILL_NOT_DRAW 标记位的。默认情况下, View 没有启用这个优化标记位的,但是 ViewGroup 会默认启用。

如果当我们自定义的控件继承自 ViewGroup 并且本身并不具备任何绘制时,那么可以设置 setWillNotDraw 方法为 true,设置为 true 后,系统会进行相应的优化。

如果当我们知道我们自定义继承自 ViewGroup 的控件需要绘制内容时,那么需要设置 setWillNotDraw 方法为 false,来关闭 WILL_NOT_DRAW 这个标记位。

参考

  • 《Android 开发艺术探索》

猜你喜欢

转载自blog.csdn.net/Airsaid/article/details/53872349