Android View 的绘制流程 04 - performDraw

Android View 的绘制流程 - 开篇 MeasureSpec
Android View 的绘制流程 01 - 前置流程
Android View 的绘制流程 02 - performMeasure
Android View 的绘制流程 03 - performLayout
Android View 的绘制流程 04 - performDraw
Android View 的绘制流程总结

在前两章分别学习了 measure 和 layout 的流程, 今天学习绘制流程的最后一步 draw.

4. ViewRootImpl.performTraversals()

ViewRootImpl.java 1576 行

private void performTraversals() {
    ...
    if (...) {
        ...
        if (...) {  
            if (...) {
                int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
                int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);        
                 // Ask host how big it wants to be
                //测量
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                             ...
                layoutRequested = true;
            }
        }
    } else {
       ...
    }
    final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
    if (didLayout) {
        //摆放
        performLayout(lp, mWidth, mHeight);
                ...
    }
        ...
    boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;

    if (!cancelDraw && !newSurface) {
        ...
        //绘制-------------本章学习内容
        performDraw();
    } else {
        ...
    }
        ...
}

从 performDraw 开始跟进.
进入到 performDraw()


 
 

4.1 ViewRootImpl.performDraw()

ViewRootImpl.java 2781行

private void performDraw() {
  ...
  draw(fullRedrawNeeded);
  ...
}

又调用了 ViewRootImpl 的 draw 方法, 继续跟进


 
 

4.2 ViewRootImpl.draw()

ViewRootImpl.java 2843行

private void draw(boolean fullRedrawNeeded) {
    ...
    if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
        return;
    }
    ...
}

分析代码得知, 这里又调用了 ViewRootImpl 的 drawSofware 方法. 跟进.


 
 

4.3 ViewRootImpl.drawSofware ()

ViewRootImpl.java 3022行

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, boolean scalingRequired, Rect dirty) {
  // Draw with software renderer.
  final Canvas canvas;
  ...
  mView.draw(canvas);
  ...
}

官方对这个方法的说明是

如果绘图成功,则为 true, 如果出错,则为 false.

这也就对应了步骤4.2 中的那个 if 判断了. 应该就是判断是否绘制成功的.
这里又看到了 mView 了. 还记得它是什么吗? 不记得的朋友可能要去前几章回忆一下了. 现在就要开始 DecorView 的绘制了.
那么我们进入 DecorView 的 draw 方法中, 看一下是怎么绘制的.


 
 

4.4 DecorView.draw()

DecorView.java 784行

@Override
public void draw(Canvas canvas) {
    super.draw(canvas);
    if (mMenuBackground != null) {
        mMenuBackground.draw(canvas);
    }
}

看到代码非常简单,
先调用了父类( FrameLayout )的 draw 方法.
然后又绘制自己的菜单背景. (这个先不关注, 有需要的朋友自行进去看看)
但是进入 FrameLayout 中后发现并没有重写 draw 方法.
那就再去 FrameLayout 的父类 ViewGroup 看一下重写没有, 进去后, 发现还是没有. 那估计就只有在顶级父类 View 中了.
进入 View 中, 发现有 draw 方法. 我们就跳转到 View 中的 draw 方法
(View 的 draw 方法一般不去重写,官网文档也建议不要去重写 draw 方法)


 
 

4.5 View.draw() 返回值为 void

View.java 19088行

/**
 * Manually render this view (and all of its children) to the given Canvas.
 * The view must have already done a full layout before this function is
 * called.  When implementing a view, implement
 * {@link #onDraw(android.graphics.Canvas)} instead of overriding this method.
 * If you do need to override this method, call the superclass version.
 *
 * @param canvas The Canvas to which the View is rendered.
*/
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
    ...
   if (!dirtyOpaque) {
       drawBackground(canvas);
   }
    ...
    // skip step 2 & 5 if possible (common case)
    ...
    // Step 3, draw the content
    ...
    if (!dirtyOpaque) onDraw(canvas);
    // Step 4, draw the children
    dispatchDraw(canvas);
        ...
    // Step 6, draw decorations (foreground, scrollbars)
    onDrawForeground(canvas);
        ...
}

为什么会说明返回值为 void 呢, 因为后面还有有一个返回值为 booleaen 的 draw() 方法, 后面会有说明
官方对该方法的说明是
手动将该 View ( 及其所有的子 View ) 渲染到给定的画布上. 在调用该方法之前, 该 View 必须已经完成了完整的布局.
当实现一个 View 时, 应该实现 onDraw(android.graphics.Cavas) 而不是本方法. 如果确实需要重写该方法,请调用超类版本。
参数canvas:将view渲染到的画布。

解读: 其实无论当前 View 是 ViewGroup 还是单一 View, 都是需要实现这套流程的, 不同的是 ViewGroup 中, 实现了 dispatchDraw 方法, 而单一 View 中不需要实现这个方法. 自定义 View 一般都需要实现 onDraw 方法, 在其中绘制不同的样式.

从代码中看出做了不少事情, 还都设置好了步骤. 第2步和第5步在有需要的时候才会用到 (skip step 2 & 5 if possible (common case)

那么目前需要关心的就是下面3个步骤 (Step6 绘制前景和滚动条可以自行了解)

Step1: drawBackground(canvas) 如果有需要的话,画背景. 其实就是我们在布局文件中设置的 android:background 属性. 这是开始画的第一步.
Step3: onDraw(canvas) 画 View 的内容 (重点).
Step4: dispatchDraw(canvas) 画子 View. 这个方法用于帮助 ViewGroup 来递归画它的子 View

注: Step1 和 Step3 在调用之前, 都进行了一个判断, 也就是说 drawBackground 和 onDraw 方法不一定会被调用. 这个判断是什么意思呢?
dirtyOpaque 标志位的作用是判断当前 View 是否是透明的, 如果是透明的, 将不会执行 drawBackground 和 onDraw. 而是直接执行 dispatchDraw 去绘制内部的子 View. 也就是说 dirtyOpaque 标志位要为 false (透明, 非实心)才会调用这两个方法.

那么先跟进到 View 的 drawBackground 方法中.


 
 

4.6 View.drawBackground ()

View.java 19302行

private void drawBackground(Canvas canvas) {
    final Drawable background = mBackground;
    //如果没有设置背景,就不进行绘制
    if (background == null) {
      return;
    }
    ...
    background.draw(canvas);
    ...
    final int scrollX = mScrollX;
    final int scrollY = mScrollY;
    if ((scrollX | scrollY) == 0) {
        //调用 Drawable 的 draw 方法来进行背景的绘制
        background.draw(canvas);
    } else {
        //平移画布
        canvas.translate(scrollX, scrollY);
        //调用 Drawable 的 draw 方法来进行背景的绘制
        background.draw(canvas);
        canvas.translate(-scrollX, -scrollY);
    }

}

如果没有设置背景, 就直接 return 了. 设置了背景就会调用 Drawable.draw 方法来画. 这里就不再继续跟进去看, 我们需要把重点放到 onDraw, 和 dispatchDraw 这两个方法上面.
既然 onDraw 是画内容, 那么肯定是需要由具体的子类去实现的. 那么直接进入到 DecorView.onDraw


 
 

4.7 DecorView.onDraw()

DecorView.java 315行

@Override
public void onDraw(Canvas c) {
  super.onDraw(c);
  mBackgroundFallback.draw(isResizing() ? this : mContentRoot, mContentRoot, c,mWindow.mContentParent);
}

先调用了 super.onDraw方法, 然后后面又画了自己的东西.
跟踪发现 FrameLayout 也没有实现 onDraw 方法, ViewGroup 也没有. 等于说这里的 super.onDraw 调用的是 View.onDraw(),
现在进入到 View.onDraw()


 
 

4.8 View.onDraw()

View.java 16827行

protected void onDraw(Canvas canvas) {
}

View 的 onDraw(canvas) 是空方法. 因为每个 View 的内容是各不相同的,所以需要由子类去实现具体逻辑。
就像上面的 DecorView.onDraw() 方法一样, 实现了 onDraw() 方法, 然后画了菜单背景. DecorView 到这里就画完自己了. 接着就开始调用 dispatchDraw() 方法画子 View 了.

那么先看下 View.dispatchDraw() 中做了什么, 进入后发现是一个空方法. 什么事情都没做. 就像 View.onDraw 一样. 为什么呢?
因为对于叶子 View 来说, 该方法没有什么意义, 它自己就是叶子 View 了, 没有子 View 需要画了. 但是对于 ViewGroup 来说, 就需要重写该方法来画它的子 View.

那么进入 DecorView 中查看 dispatchDraw() 方法. 发现没有重写这个方法, FrameLayout 也没有重写, 一直跟到 ViewGroup 中才找到了这个方法. 所以当 DecorView 执行完 onDraw() 方法后, 流程就会切到 ViewGroup 中的 dispatchDraw() 方法中.
( 在源码中发现, 像平时常用的 LinearLayout, FrameLayout, RelativeLayout 等常用的布局控件, 都没有再重写该方法. )


 
 

4.9 ViewGroup.dispatchDraw()

ViewGroup.java 3927行

@Override
protected void dispatchDraw(Canvas canvas) {
    ...
    final int childrenCount = mChildrenCount;
    final View[] children = mChildren;
    ...
    for (int i = 0; i < childrenCount; i++) {
        ...
        if (...) {
            more |= drawChild(canvas, child, drawingTime);
        }
        ...
    }
    ...
}

又看到了 递归子 View 的循环. 在循环中调用了 ViewGroup 中的 drawChild 方法, 并传入了子 View 对象.
( DecorView 的第一个子 VIew 是 LinearLayout. ) 现在进入 ViewGroup.drawChild() 方法


 
 

4.10 ViewGroup.drawChild()

ViewGroup.java 4213行

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

先看一下官方对该方法的说明

绘制当前 ViewGroup 的一个子 View. 该方法负责在正确的状态下获取画布. 这包括了裁剪, 移动, 以便子 View 的滚动原点在0.0处, 以及提供任何动画转换。
参数1: 画布
参数2: 需要绘制的子 View
参数3: 绘制发生的时间点.

注意: 这里调用的 draw() 方法, 是带有返回值的方法, 上面我们看到调用的draw() 方法都是没有返回值的.
DecorView 的子 View 是 LinearLayout , 进入 LinearLayout 后发现没有重写这个方法, ViewGroup 中也没有重写, 那么肯定还是在View 中了. 进入到 View.Draw() 返回值为 boolean 的方法中.


 
 

4.11 View.draw() 返回值为 boolean.

View.java 18760行

boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
    ...
            // Fast path for layouts with no backgrounds
            if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                //跳过当前 View 的绘制, 直接绘制子 View.
                mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                dispatchDraw(canvas);
            } else {
                draw(canvas);
            }
    ...
    return more;
}

官方对该方法的注释说明为:

该方法被 ViewGroup.drawChild() 方法调用. 为每个子 View 绘制自己.
是 View 专门根据图层类型(cpu 还是 gpu)进行渲染和硬件加速.

代码中省略的部分意思大概为:

当 ViewGroup 向下分发绘制 子View 的时候, 会根据是否开启硬件加速和 View 的绘制类型来判断当前 View 是用 CPU 绘制还是 GPU 绘制, GPU 的主要作用就是渲染图形, 绘制图形的速度会高于 CPU, 但是 GPU 会比 CPU 更加耗电.

注: 从代码中看到, 会先有一个判断, 这个判断和上面 void draw() 方法中的类似, 都和 mPrivateFlags 有关.
      如果 mPrivateFlags 包含 PFLAG_SKIP_DRAW 那么会直接调用 dispatchDraw 绘制当前 View 的子 View.
      如果不包含, 则会调用当前 View 中返回值值为 void 的 draw() 方法. 继续执行 void draw() 方法中的几个步骤

至于是否包含 PFLAG_SKIP_DRAW, 这里不再过多描述, 请参考 你真的了解Android ViewGroup的draw和onDraw的调用时机吗 这篇文章, 介绍的非常清晰.

到此差不多结束了View 的绘制流程. 下面用一张图来描述下 draw 的过程.


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

推荐阅读更多精彩内容