Android——View的工作流程——draw过程

一、draw 作用

draw 的作用是将 View 绘制到屏幕上

二、draw 是什么

三、draw 过程

1. 单一 View 的 draw 过程

(1)draw()
draw 过程的入口是 View 类的draw()方法,该方法的作用是将视图渲染到给定的 Canvas 上。

  • 调用该方法前必须完成 layout 过程
  • 自定义 View 时,应复写onDraw(canvas)方法进行绘制,不应复写该方法
  • 自定义 View 时,若确实要复写该方法,则需先调用super.draw(canvas)完成系统的绘制,然后再进行自定义的绘制
/**
 * Manually render this view (and all of its children) to the given Canvas.// 手动将视图渲染到给定的Canvas上
 * The view must have already done a full layout before this function is called. // 调用该方法前必须要完成 layout 过程
 *  When implementing a view, implement  * {@link #onDraw(android.graphics.Canvas)} instead of overriding this method. // 自定义 View 时重写"onDraw()"方法,而不是本方法
 * If you do need to override this method, call the superclass version.// 若自定义的视图确实要复写该方法,那么需先调用 super.draw(canvas)完成系统的绘制,然后再进行自定义的绘制
 *
 * @param canvas The Canvas to which the View is rendered.
 */
@CallSuper
public void draw(Canvas canvas) {
    final int privateFlags = mPrivateFlags;
    final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
        (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
    mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

    /*
     * 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
    int saveCount;

    if (!dirtyOpaque) {
        drawBackground(canvas);
    }

    // skip step 2 & 5 if possible (common case)
    final int viewFlags = mViewFlags;
    boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
    boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
    if (!verticalEdges && !horizontalEdges) {
        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);

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

        // Overlay is part of the content and draws beneath Foreground
        if (mOverlay != null && !mOverlay.isEmpty()) {
            mOverlay.getOverlayView().dispatchDraw(canvas);
        }

        // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);

        // we're done...
        return;
    }
}

draw 过程必须以合适的顺序进行:
① 绘制 View 的背景
② 绘制 View 的内容
③ 绘制子 View
④ 绘制装饰(渐变框、滑动条、前景等)

(2)drawBackground()

/**
 * Draws the background onto the specified canvas.
 *
 * @param canvas Canvas on which to draw the background
 */
private void drawBackground(Canvas canvas) {
    // 获取背景 drawable
    final Drawable background = mBackground;
    if (background == null) {
        return;
    }

    // 根据 layout 过程中获取的 View 的位置参数,来设置背景的边界
    setBackgroundBounds();

    // Attempt to use a display list if requested.
    if (canvas.isHardwareAccelerated() && mAttachInfo != null &&
        mAttachInfo.mHardwareRenderer != null) {
        mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode);

        final RenderNode renderNode = mBackgroundRenderNode;
        if (renderNode != null && renderNode.isValid()) {
            setBackgroundRenderNodeProperties(renderNode);
            ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
            return;
        }
    }

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

(3)onDraw()
作用是绘制 View 本身的内容,由于每个 View 的内容各不相同,所以该方法的实现是一个空实现。在自定义 View 中,需子类去复写该方法,从而绘制自身的内容。自定义 View 中必须重写onDraw()方法

/**
 * 作用:绘制View本身的内容
 *
 * @param canvas the canvas on which the background will be drawn
 */
protected void onDraw(Canvas canvas) {}

(4)dispatchDraw()
作用是绘制子 View,由于单一 View中无子 View,故 View 类中该方法为空实现

/**
 * Called by draw to draw the child views. This may be overridden
 * by derived classes to gain control just before its children are drawn
 * (but after its own view has been drawn).
 * @param canvas the canvas on which to draw the view
 */
protected void dispatchDraw(Canvas canvas) {

}

(5)onDrawForeground()
作用是绘制装饰。

/**
 * 作用:绘制装饰
 *
 * <p>Foreground content may consist of scroll bars, a {@link #setForeground foreground}
 * drawable or other view-specific decorations. The foreground is drawn on top of the
 * primary view content.</p>
 *
 * @param canvas canvas to draw into
 */
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);
    }
}

2. ViewGroup 的 draw 过程

(1)draw()
ViewGroup 绘制的入口是从 View 类中继承的方法draw(),该方法的具体介绍参见上文单一 View 的绘制。

(2)drawBackground()
继承 View 类中该方法,参见上文单一 View 的绘制。

(3)onDraw()
继承 View 类中该方法,参见上文单一 View 的绘制。

(4)dispatchDraw()
ViewGroup 类中重写了该方法。

/**
 * 作用:遍历子View & 绘制子View
 * 注:
 *   a. ViewGroup中:由于系统为我们实现了该方法,故不需重写该方法
 *   b. View中默认为空实现(因为没有子View可以去绘制)
 */
protected void dispatchDraw(Canvas canvas) {
    ......

    // 1. 遍历子View
    final int childrenCount = mChildrenCount;
    ......

    for (int i = 0; i < childrenCount; i++) {
        ......
        if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
            transientChild.getAnimation() != null) {
            // 2. 绘制子View视图
            more |= drawChild(canvas, transientChild, drawingTime);
        }
        ....
    }
}

/**
 * 作用:绘制子View
 */
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    // 最终还是调用了子 View 的 draw ()进行子View的绘制
    return child.draw(canvas, this, drawingTime);
}

四、重点说明

1. View 类的setWillNotDraw()方法

该方法用于设置标志位WILL_NOT_DRAW的值。当一个 View 不需要绘制内容时,并且该标志位为 true,系统进行相应的优化。

/**

     * If this view doesn't do any drawing on its own, set this flag to // 如果这个View不需要绘制任何内容,那么设置这个标记位为true后,系统会进行相应的优化。
     * allow further optimizations. By default, this flag is not set on // 默认情况下,View没有启用这个优化标志位,但是ViewGroup默认启用这个标志位。
     * View, but could be set on some View subclasses such as ViewGroup. // 当我们的自定义控件继承于ViewGroup并且本身不具备绘制功能时,可开启开标志位便于系统进行后续的优化
     *
     * Typically, if you override {@link #onDraw(android.graphics.Canvas)}
     * you should clear this flag. // 如果一个自定义View重写了onDraw()来绘制内容时,应显示关闭WILL_NOT_DRAW这个标记位。
     *
     * @param willNotDraw whether or not this View draw on its own
     */
public void setWillNotDraw(boolean willNotDraw) {
    setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}

对实际开发的意义:

  • 自定义控件继承自 ViewGroup、且本身不进行任何绘制
    标志位设为 true,系统会进行相应的优化。

  • 自定义控件继承自 ViewGroup、且明确知道一个 ViewGroup 需要通过onDraw()来绘制内容时
    标志位设为 false。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342