View工作原理之draw

根据前面分析的view的测量和布局流程都是从ViewRootImpl的PerforTraversals方法开始分析的,view的draw流程我们依旧从此看起

View的draw流程

ViewRootImpl#performDraw()

//...
final boolean fullRedrawNeeded = mFullRedrawNeeded || mReportNextDraw; 
mFullRedrawNeeded = false;
//...
try {
    boolean canUseAsync = draw(fullRedrawNeeded);
    if (usingAsyncReport && !canUseAsync) {
        mAttachInfo.mThreadedRenderer.setFrameCompleteCallback(null);
        usingAsyncReport = false;
    }
} finally {
    mIsDrawing = false;
    Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
//省略...

performDraw方法调用了draw方法去绘制视图,其中传入了参数fullRedrawNeeded,该参数是由mFullRedrawNeeded和mReportNextDraw参数获取到的,它的作用是判断是否需要重新绘制全部视图。如果是第一次绘制视图,当然需要全部绘制;如果不是第一次,就没有必要全部都再绘制一遍。

ViewRootImpl#draw()

draw方法比较长,我们只看关键的那一部分:

//...
//获取mDirty的值,该值表示需要重绘的区域
final Rect dirty = mDirty;
if (mSurfaceHolder != null) {
    // The app owns the surface, we won't draw.
    dirty.setEmpty();
    if (animating && mScroller != null) {
        mScroller.abortAnimation();
    }
    return false;
}
//如果fullRedrawNeeded为真,则把mDirty区域置为整个屏幕,表示整个视图都需要绘制
//第一次绘制流程,需要绘制所有的视图
if (fullRedrawNeeded) {
    mAttachInfo.mIgnoreDirtyState = true;
    dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
}

//...

if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
        scalingRequired, dirty, surfaceInsets)) {
    return false;
}

首先获取到mDirty值,该值保存了需要重新绘制的区域信息。接着根据fullRedrawNeeded来判断是否需要重置dirty区域为整个屏幕。最后调用ViewRootImpl#drawsoft方法,把相关参数传进去。

ViewRootImpl#drawSoftware

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty) {

    // Draw with software renderer.
    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区域,由dirty区域决定
        canvas = mSurface.lockCanvas(dirty);

        // The dirty rectangle can be modified by Surface.lockCanvas()
        //noinspection ConstantConditions
        if (left != dirty.left || top != dirty.top || right != dirty.right
                || bottom != dirty.bottom) {
            attachInfo.mIgnoreDirtyState = true;
        }

        canvas.setDensity(mDensity);
    } 

    try {
        
        if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {
            canvas.drawColor(0, PorterDuff.Mode.CLEAR);
        }

        dirty.setEmpty();
        mIsAnimating = false;
        attachInfo.mDrawingTime = SystemClock.uptimeMillis();
        mView.mPrivateFlags |= View.PFLAG_DRAWN;

        try {
            canvas.translate(-xoff, -yoff);
            if (mTranslator != null) {
                mTranslator.translateCanvas(canvas);
            }
            canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
            attachInfo.mSetIgnoreDirtyState = false;

            //正式开始绘制
            mView.draw(canvas);

        }
    } 
    return true;
}

这里首先实例化了canvas对象,然后锁定该canvas的区域,由dirty区域决定,接着对canvas进行了一系列的属性赋值,最后调用了mView.draw(canvas)方法。从performTraversals方法的源码中可以知道mView其实就是DecorView,也就是说从DecorView开始绘制,前面都是准备工作,现在才开始真正的绘制流程。

View#draw()

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;
    
    //dirtyOpaque判断当前veiw是否是透明的,如果view是透明的。那么view的背景、自己本身就没有必要绘制了
    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);

        drawAutofilledHighlight(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);

        // Step 7, draw the default focus highlight
        drawDefaultFocusHighlight(canvas);

        if (debugDraw()) {
            debugDrawFocus(canvas);
        }

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

draw方法比较长,但是源码中为我们写的很清楚,draw方法主要有六大步骤:

  • 绘制背景
  • 保存画布图层以准备淡化
  • 绘制视图内容
  • 绘制当前view的子view
  • 绘制淡化边缘并回复图层
  • 绘制装饰(例如滚动条)

这里省略第二步和第五步不看

下面看一下这四大步骤:

View#drawBackground()

private void drawBackground(Canvas canvas) {
    //背景其实是一个drawable,就像我们平常自己写的.xml背景图一样也是一个drawable
    final Drawable background = mBackground;
    if (background == null) {
        return;
    }
    
    //设置背景边界
    setBackgroundBounds();

    // ...
    
    //获取当前view的mScrollX和mScrollY值
    final int scrollX = mScrollX;
    final int scrollY = mScrollY;
    if ((scrollX | scrollY) == 0) {
        background.draw(canvas);
    } else {
        //如果scrollX和scrollY有值,则对canvas的坐标进行偏移,再绘制背景
        canvas.translate(scrollX, scrollY);
        background.draw(canvas);
        //绘制完后,将canvas移回原位置
        canvas.translate(-scrollX, -scrollY);
    }
}

如果有背景需要绘制,则在画布上先绘制背景。

View#draw()

protected void onDraw(Canvas canvas) {
}

draw方法是空方法,因为不同的view有不同的样子,当我们自定义view的时候需要自己去绘制。

ViewGroup#dispatchDraw

view中该方法是空方法,而ViewGroup继承了View,所以这里去看看ViewGroup的dispatchDraw:

 protected void dispatchDraw(Canvas canvas) {
    final int childrenCount = mChildrenCount;
    final View[] children = mChildren;
    
    //...
    
    //循环遍历子view,调用drawChild方法绘制子view
    for (int i = 0; i < childrenCount; i++) {
        while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
            final View transientChild = mTransientViews.get(transientIndex);
            if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                    transientChild.getAnimation() != null) {
                more |= drawChild(canvas, transientChild, drawingTime);
            }
            transientIndex++;
            if (transientIndex >= transientCount) {
                transientIndex = -1;
            }
        }

        final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
        final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
            more |= drawChild(canvas, child, drawingTime);
        }
    }
 }

方法很长,但可以看到这里有和layout相似的想法,调用drawChild方法去绘制view的子view。

View#drawChild()

drawChild()调用draw方法。这个方法主要是画ViewGroup的孩子view,他负责获取画布当前的状态,当画布被被裁剪,平移等操作时以便子view的起始坐标为(0,0)和有动画时

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

View#draw(canvas,this,drawing)

这个方法使每个子视图自己绘制,是view专门根据图层类型(cpu 还是 gpu)进行渲染和硬件加速。

boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
    //判断此view是否开启硬件加速
    final boolean hardwareAcceleratedCanvas = canvas.isHardwareAccelerated();
    
     /* If an attached view draws to a HW canvas, it may use its RenderNode + DisplayList.
         *
     * If a view is dettached, its DisplayList shouldn't exist. If the canvas isn't
     * HW accelerated, it can't handle drawing RenderNodes.
     */
     //源码中有这样一段话,意思大概是如果把view画到画布上,必须要有RenderNode + DisplayList
    
    //判断当前view是否支持硬件加速绘制
    boolean drawingWithRenderNode = mAttachInfo != null
                && mAttachInfo.mHardwareAccelerated
                && hardwareAcceleratedCanvas;
    //...
    
    //下面这一部分主要就是判断view是cpu绘制还是gpu绘制
    
    //硬件加速绘制用到的绘制节点
    RenderNode renderNode = null;
    //cpu绘制用到的绘制缓存
    Bitmap cache = null;
    //获取当前View的绘制类型 LAYER_TYPE_NONE LAYER_TYPE_SOFTWARE LAYER_TYPE_HARDWARE
    int layerType = getLayerType();
    //如果是cpu绘制类型 也就是带software的标志
    if (layerType == LAYER_TYPE_SOFTWARE || !drawingWithRenderNode) {
        if (layerType != LAYER_TYPE_NONE) {
            layerType = LAYER_TYPE_SOFTWARE;
            //开始cpu绘制缓存构建
            buildDrawingCache(true);
        }
        //获得cpu绘制缓存结果 存储在Bitmap中
        cache = getDrawingCache(true);
    }
    
     //支持硬件加速
    if (drawingWithRenderNode) {
         //更新gpu绘制列表 保存在RenderNode中
        renderNode = updateDisplayListIfDirty();
        if (!renderNode.isValid()) {
            renderNode = null;
            //gpu绘制失败标识
            drawingWithRenderNode = false;
        }
    }
    
    //...
    
    //cpu绘制成功并且gpu绘制失败了
    final boolean drawingWithDrawingCache = cache != null && !drawingWithRenderNode;
    
    //...
    
    //走gpu绘制
    if (!drawingWithDrawingCache) {
        if (drawingWithRenderNode) {//有硬件加速的支持
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
            //gpu绘制收集到的DisplayList
            ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
        }else{
            if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                    //没有内容不需要绘制自己,就直接向下分发绘制子view
                    mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                    dispatchDraw(canvas);
                } else {
                    //继续六大流程,绘制自己后再分绘制子view
                    draw(canvas);
                }
        }
    }else if((cache != null){//走cpu绘制且cpu绘制缓存不为null
    
        //...
        
        //把存储cpu绘制缓存的Bitmap用canvas走cpu绘制
        canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint);
    }
    return more;
}

当ViewGroup向下分发绘制子view的时候,会根据是否开启硬件加速和view的绘制类型来判断view是用cpu绘制还是gpu绘制,gpu的主要作用就是渲染图形,绘制图形的速度会高于cpu,但是gpu会比cpu更加耗电。之前在简单的绘制一个气泡图形的时候,运行后图形展示不出来,网上给出的解决方法就是开启硬件加速,看了这里的源码知道了开启硬件加速绘制的大概流程。

开启硬件加速的源码不止这里还有,以后会慢慢学习。一篇很好的理解硬件加速的博客

View#onDrawForeground

public void onDrawForeground(Canvas canvas) {
    onDrawScrollIndicators(canvas);
    onDrawScrollBars(canvas);
    
    final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
    if (foreground != null) {
        
        //先设定绘制区域
        
        //...
        
        //利用canvas进行绘制
        foreground.draw(canvas);
    }
}

总结

  • 准备工作:ViewRootImp中
    • performTravsals() ---> performDraw()
    • performDraw() ---> draw() 确定绘制的区域mDirty
    • drawSoftware() 创建Canvas对象,进行真正的绘制
  • 开始绘制的重要四大流程
    • drawBackground(canvas)
    • onDraw(canvas) 空方法 需要重写
    • dispatchDraw(canvas) 绘制child
    • onDrawForeground(canvas) 滚动条绘制

参考文章

Android应用层View绘制流程与源码分析

Android View 绘制流程(Draw) 完全解析

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

推荐阅读更多精彩内容