Draw过程

一、调用流程

1. ViewRootImpl # performTraversals()

performTraversals()调用了performDraw(),三大流程的起点都是performTraversals()。

// 没有取消draw也没有创建新的平面 第一次traversals时newSurface为true
if (!cancelDraw && !newSurface) {
    // ...
    // 开始draw流程
    performDraw();
} else {
    if (isViewVisible) {
        // Try again
        // 如果是可见的, 就再调用一次traversals
        scheduleTraversals();
        // ...
    }
    // ...
}

2. ViewRootImpl # performDraw()

调用到ViewRootImpl的draw()。

private void performDraw() {
    // ...
    // mFullRedrawNeeded表示是否需要完全重绘
    final boolean fullRedrawNeeded = mFullRedrawNeeded;
    mFullRedrawNeeded = false;
    mIsDrawing = true;
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");
    try {
        // 调用draw()
        draw(fullRedrawNeeded);
    } finally {
        mIsDrawing = false;
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
    // ...
}

3. ViewRootImpl # draw()

这个方法非常长,主要是去处理绘制区域、坐标等准备工作,之后调用drawSoftware()。

private void draw(boolean fullRedrawNeeded) {
    // ...
    // 获取需要绘制的区域
    final Rect dirty = mDirty;
    // ...
    // 判断是否需要完全绘制
    if (fullRedrawNeeded) {
        mAttachInfo.mIgnoreDirtyState = true;
        // 如果需要就将区域设置为屏幕的所有区域
        dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
    }
    // ...
    if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
        if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) {
            // ...
        } else {
            // ...
            // 调用drawSoftware()绘制
            if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
                return;
            }
        }
    }
    // ...
}

4. ViewRootImpl # drawSoftware()

  • 初始canvas对象
  • 调用DecorView的draw()
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
        boolean scalingRequired, Rect dirty) {
    final Canvas canvas;
    try {
        final int left = dirty.left;
        final int top = dirty.top;
        final int right = dirty.right;
        final int bottom = dirty.bottom;
        
        // 创建一个绘制区域的canvas对象
        canvas = mSurface.lockCanvas(dirty);
        
        // 判断lockCanvas有没有改变dirty的顶点值
        if (left != dirty.left || top != dirty.top || right != dirty.right
                || bottom != dirty.bottom) {
            attachInfo.mIgnoreDirtyState = true;
        }
        // 设置画布密度
        canvas.setDensity(mDensity);
    } catch (Surface.OutOfResourcesException e) {
       // ...
    }
    try {
        // ...
        // 先清空画布
        if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {
            canvas.drawColor(0, PorterDuff.Mode.CLEAR);
        }
        dirty.setEmpty();
        mIsAnimating = false;
        mView.mPrivateFlags |= View.PFLAG_DRAWN;
        
        try {
            // 设置画布偏移值
            canvas.translate(-xoff, -yoff);
            if (mTranslator != null) {
                mTranslator.translateCanvas(canvas);
            }
            canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
            attachInfo.mSetIgnoreDirtyState = false;
            // 调用draw() 从DecorView开始绘制流程
            mView.draw(canvas);
            // ...
        }
        // ...
    } 
    // ...
    return true;
}

5. DecorView # draw()

  • 先调用父类的draw()去绘制。
  • 如果有菜单背景的drawable的画就画上。
@Override
public void draw(Canvas canvas) {
    super.draw(canvas);
    if (mMenuBackground != null) {
        mMenuBackground.draw(canvas);
    }
}

6. View # draw()

FrameLayout和ViewGroup都没有重写draw(),所以就调用到了View的draw()。View的draw()也很长,但是注释写的分步很清楚。

/*
 * 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)
 */

绘制背景、绘制自身、绘制子View、绘制阴影、绘制其它的装饰。到这里就调用到Draw向子View分发的过程了。下面就仔细的看看这个方法。

二、View的draw()的实现

根据注释可以将draw()分成六个阶段。

  • 绘制背景
  • 存储图层
  • 绘制自身
  • 绘制子View
  • 绘制阴影并恢复图层
  • 绘制滚动条等效果

1. 绘制背景

View # draw()

如果不为透明,就调用drawBackground()去绘制背景。

final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
        (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);

// 判断是否为透明
if (!dirtyOpaque) {
    // 调用drawBackgroud()去绘制背景
    drawBackground(canvas);
}
View # drawBackground()
  • 设置背景图片边界
  • 判断View是否有移动
    • N:直接绘制背景图片
    • Y:移动到View的偏移位置,绘制背景,再移动回来。
private void drawBackground(Canvas canvas) {
    final Drawable background = mBackground;
    if (background == null) {
        return;
    }
    // 设置背景图片的边界位置
    setBackgroundBounds();
    
    // 硬件加速相关......
    
    final int scrollX = mScrollX;
    final int scrollY = mScrollY;
    // 判断View是否滑动
    if ((scrollX | scrollY) == 0) {
        // 没有滑动就绘制背景图片
        background.draw(canvas);
    } else {
        // 如果移动了,就先移动canvas,绘制背景,canvas再移回
        canvas.translate(scrollX, scrollY);
        background.draw(canvas);
        canvas.translate(-scrollX, -scrollY);
    }
}
View # setBackgroundBounds()

调用drawable的setBounds()设置边界,传参是由layout过程中生成的顶点值组合的,就相当于是View的顶点。

void setBackgroundBounds() {
    if (mBackgroundSizeChanged && mBackground != null) {
        // 设置背景图的四个坐标,这四个值就是View的四个顶点值
        mBackground.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
        mBackgroundSizeChanged = false;
        rebuildOutline();
    }
}

2. 绘制自身

View # draw()
  • 如果不需要绘制阴影,就直接进入绘制自身的步骤。
  • 如果View不是透明的,就调用onDraw()去绘制自身。
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);
    // ...
}
View # onDraw()

空实现,没有做统一实现,自定义View时需要自己去实现。

protected void onDraw(Canvas canvas) {
}

3. 绘制子View分发过程

View # draw()

在绘制自己完成后就去分发调用子View的绘制过程。

if (!verticalEdges && !horizontalEdges) {
    // ...
    dispatchDraw(canvas);
    // ...
}
ViewGroup # dispatchDraw()

在View中是空实现,所以来看ViewGroup中的实现。会遍历所有的子View,如果子View可见或是存在动画,就调用drawChld()。

@Override
protected void dispatchDraw(Canvas canvas) {
    boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
    final int childrenCount = mChildrenCount;
    final View[] children = mChildren;
    int flags = mGroupFlags;
    // ...
    // 遍历子View
    for (int i = 0; i < childrenCount; i++) {
        // ...
        final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
        final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
            // 调用drawChild去传递
            more |= drawChild(canvas, child, drawingTime);
        }
    }
    // ...
}
ViewGroup # drawChild()

直接调用了子View的draw(),实现了传递。

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

这个draw()有三个参数,和之前分析的一个参数的draw()不同,注释上说得很清楚,这个draw()是ViewGroup.drawChild()调用去绘制子View的。和一个参数最大不同是,这个draw()判断了是绘制缓存内容还是去调用一个参数的draw()绘制。

boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
    // ...
    if (!drawingWithDrawingCache) {
        // 如果不使用缓存
        if (drawingWithRenderNode) {
            // 硬件缓存
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
            ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
        } else {
            // Fast path for layouts with no backgrounds
            if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                // 如果需要跳过,就直接去调用分发给这个View的子View的方法
                dispatchDraw(canvas);
            } else {
                // 调用普通的draw()
                draw(canvas);
            }
        }
    } else if (cache != null) {
        // 如果使用缓存并且缓存不为空
        mPrivateFlags &= ~PFLAG_DIRTY_MASK;
        if (layerType == LAYER_TYPE_NONE || mLayerPaint == null) {
            // no layer paint, use temporary paint to draw bitmap
            Paint cachePaint = parent.mCachePaint;
            if (cachePaint == null) {
                cachePaint = new Paint();
                cachePaint.setDither(false);
                parent.mCachePaint = cachePaint;
            }
            cachePaint.setAlpha((int) (alpha * 255));
            // 绘制缓存的内容
            canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint);
        } else {
            // use layer paint to draw the bitmap, merging the two alphas, but also restore
            int layerPaintAlpha = mLayerPaint.getAlpha();
            if (alpha < 1) {
                mLayerPaint.setAlpha((int) (alpha * layerPaintAlpha));
            }
            // 使用图层的Paint去绘制缓存内容
            canvas.drawBitmap(cache, 0.0f, 0.0f, mLayerPaint);
            if (alpha < 1) {
                mLayerPaint.setAlpha(layerPaintAlpha);
            }
        }
    }
    // ...
}

4. 绘制装饰物

View # draw()
if (!verticalEdges && !horizontalEdges) {
    // ...
    onDrawForeground(canvas);
    // ...
}
View # onDrawForeground()
public void onDrawForeground(Canvas canvas) {
    // 绘制滚动指示器
    onDrawScrollIndicators(canvas);
    // 绘制滚动条
    onDrawScrollBars(canvas);
    final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
    if (foreground != null) {
        // ...
        // 如果有前景图片,就绘制它
        foreground.draw(canvas);
    }
}

5. 如果需要绘制边框阴影

前面那些步骤走完后就能直接return了。如果需要绘制阴影的话,则不会进入前面分析的方法块。而是执行下面的步骤。注释上说这种情况不常见。

View # draw()
  • 同样地绘制背景
  • 去计算阴影带来的变量影响,对阴影分类去保存图层
  • 同样地绘制自身
  • 同样地分发绘制流程
  • 分类去绘制阴影
  • 加载保存的图层
  • 同样地绘制装饰
// 共同的步骤,绘制背景图片
if (!dirtyOpaque) {
    drawBackground(canvas);
}
// 不需要绘制阴影的情况
if (!verticalEdges && !horizontalEdges) {
    // ...
    return;
}

// 如果需要绘制阴影

// 根据阴影情况去改变参数......

if (solidColor == 0) {
    final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;
    // 保存图层
    if (drawTop) {
        canvas.saveLayer(left, top, right, top + length, null, flags);
    }
    // ...
} else {
    scrollabilityCache.setFadeColor(solidColor);
}
// 绘制自身
if (!dirtyOpaque) onDraw(canvas);
// 绘制子View
dispatchDraw(canvas);
// ...
// 绘制阴影
if (drawTop) {
    matrix.setScale(1, fadeHeight * topFadeStrength);
    matrix.postTranslate(left, top);
    fade.setLocalMatrix(matrix);
    p.setShader(fade);
    canvas.drawRect(left, top, right, top + length, p);
}
// ...
// 取出保存的图层
canvas.restoreToCount(saveCount);
// ...
// 绘制装饰
onDrawForeground(canvas);
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容