Android视图加载流程(6)之View的详细绘制流程Draw

流程图

Android视图加载流程(5)之View的详细绘制流程Layout

上一篇文章我们对View的测量(Measure)进行讲解了。接着我们开始聊布局(Layout),以下是我们熟悉的performTraversals方法。

private void performTraversals() {
    ......
    mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    ......
    mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
    ......
    //本文重点
    canvas = mSurface.lockCanvas(dirty);
    mView.draw(canvas);
    ......
} 

我们看出ViewRootImpl创建一个canvas,然后mView(DecorView)调用draw方法并传入canvas,此方法执行具体的绘制工作。与Measure和Layout类似的需要递归绘制。

理解图 本文重点Draw

源码解读

Step1 View

由于ViewGroup没有重写View的draw方法,我们看下View的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
    ......
        if (drawTop) {
            canvas.saveLayer(left, top, right, top + length, null, flags);
        }
    ......

    // 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) {
        matrix.setScale(1, fadeHeight * topFadeStrength);
        matrix.postTranslate(left, top);
        fade.setLocalMatrix(matrix);
        p.setShader(fade);
        canvas.drawRect(left, top, right, top + length, p);
    }
    ......

    // Step 6, draw decorations (scrollbars)
    onDrawScrollBars(canvas);
    ......
}

整个的绘制流程分成6步,通过注释可以知道第2步和第5步(skip step 2 & 5 if possible (common case))可以忽略跳过,我们对剩余4步就行分析。

Step2 View

第一步:对View的背景进行绘制

private void drawBackground(Canvas canvas) {
    //获取xml中通过android:background属性或者代码中setBackgroundColor()、setBackgroundResource()等方法进行赋值的背景Drawable
    final Drawable background = mBackground;
    ......
    //根据layout过程确定的View位置来设置背景的绘制区域
    if (mBackgroundSizeChanged) {
        background.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);
        mBackgroundSizeChanged = false;
        rebuildOutline();
    }
    ......
    //调用Drawable的draw()方法来完成背景的绘制工作
    background.draw(canvas);
    ......
}

draw方法通过调运drawBackground(canvas);方法实现了背景绘制。

Step3 View

第三步:对View的内容进行绘制

protected void onDraw(Canvas canvas) {
}

ViewGroup没有重写该方法,view的方法也紧紧是一个空方法而已。大家都知道不同的View是显示不同的内容的,所以这块必须是子类去实现具体逻辑。

Step4.1 View

第四步:对当前View的所有子View进行绘制

protected void dispatchDraw(Canvas canvas) {
}

View的dispatchDraw()方法是一个空方法。这个我们也比较好理解,本身View自身就没有所谓的子视图,而拥有子视图的就是ViewGroup!所以我们可以看下ViewGroup的dispatchDraw

Step4.2 ViewGroup

@Override
protected void dispatchDraw(Canvas canvas) {
    ......
    final int childrenCount = mChildrenCount;
    final View[] children = mChildren;
    ......
    for (int i = 0; i < childrenCount; i++) {
        ......
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
            more |= drawChild(canvas, child, drawingTime);
        }
    }
    ......
    // Draw any disappearing views that have animations
    if (mDisappearingChildren != null) {
        ......
        for (int i = disappearingCount; i >= 0; i--) {
            ......
            more |= drawChild(canvas, child, drawingTime);
        }
    }
    ......
}
    
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    return child.draw(canvas, this, drawingTime);
}

可见,Viewgroup重写了dispatchDraw()方法,该方法内部会遍历每个子View,然后调用drawChild()方法。而drawChild方法内部是直接由子视图调用draw()方法。

Step5 View

第六步:对View的滚动条进行绘制

protected final void onDrawScrollBars(Canvas canvas) {
    //绘制ScrollBars分析不是我们这篇的重点,所以暂时不做分析
    ......
}

可以看见其实任何一个View都是有(水平垂直)滚动条的,只是一般情况下没让它显示而已。

总结 Summary

通过以上几个步骤的分析,绘制(draw)的流程基本与measure和layout类似。通过循环调用draw来绘制各个子视图。

  1. 如果对象为view就不用遍历子视图,如果对象为viewGroup就要遍历子视图
  2. View默认不会绘制任何内容,子类必须重写onDraw方法
  3. View的绘制是借助onDraw方法传入的Canvas类来进行的
  4. 默认情况下子View的ViewGroup.drawChild绘制顺序和子View被添加的顺序一致,但是你也可以重载ViewGroup.getChildDrawingOrder()方法提供不同顺序。

额外 extra

我们经常在自定义View的时候会遇到两种方法invalidate和postinvalidate。我们来看看两个方法与视图的绘制有什么样的联系呢?

invalidate方法源码分析

由于ViewGroup并没有重写该方法,所以我们直接看View的invalidate

View
//public,只能在UI Thread中使用,别的Thread用postInvalidate方法,View是可见的才有效,回调onDraw方法,针对局部View
public void invalidate(Rect dirty) {
    final int scrollX = mScrollX;
    final int scrollY = mScrollY;
    //实质还是调运invalidateInternal方法
    invalidateInternal(dirty.left - scrollX, dirty.top - scrollY,
            dirty.right - scrollX, dirty.bottom - scrollY, true, false);
}

//public,只能在UI Thread中使用,别的Thread用postInvalidate方法,View是可见的才有效,回调onDraw方法,针对局部View
public void invalidate(int l, int t, int r, int b) {
    final int scrollX = mScrollX;
    final int scrollY = mScrollY;
    //实质还是调运invalidateInternal方法
    invalidateInternal(l - scrollX, t - scrollY, r - scrollX, b - scrollY, true, false);
}

//public,只能在UI Thread中使用,别的Thread用postInvalidate方法,View是可见的才有效,回调onDraw方法,针对整个View
public void invalidate() {
    //invalidate的实质还是调运invalidateInternal方法
    invalidate(true);
}


//default的权限,只能在UI Thread中使用,别的Thread用postInvalidate方法,View是可见的才有效,回调onDraw方法,针对整个View
void invalidate(boolean invalidateCache) {
    //实质还是调运invalidateInternal方法
    invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}

//这是所有invalidate的终极调运方法!!!!!!
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
        boolean fullInvalidate) {
        ......
        // Propagate the damage rectangle to the parent view.
        final AttachInfo ai = mAttachInfo;
        final ViewParent p = mParent;
        if (p != null && ai != null && l < r && t < b) {
            final Rect damage = ai.mTmpInvalRect;
            //设置刷新区域
            damage.set(l, t, r, b);
            //传递调运Parent ViewGroup的invalidateChild方法
            p.invalidateChild(this, damage);
        }
        ......
}

View的invalidate方法最终调动invalidateInternal方法。而invalidateInternal方法是将要刷新的区域传递给父视图,并调用父视图的invalidateChild。

ViewGroup
public final void invalidateChild(View child, final Rect dirty) {
    ViewParent parent = this;
    final AttachInfo attachInfo = mAttachInfo;
    ......
    do {
        ......
        //循环层层上级调运,直到ViewRootImpl会返回null
        parent = parent.invalidateChildInParent(location, dirty);
        ......
    } while (parent != null);
}

这个过程不断的向上寻找父亲视图,当父视图为空时才停止。所以我们可以联想到根视图的ViewRootImpl

ViewRootImpl
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
    ......
    //View调运invalidate最终层层上传到ViewRootImpl后最终触发了该方法
    scheduleTraversals();
    ......
    return null;
}

返回为空。刚好符合上面的循环!接着我们看scheduleTraversals()这个方法是不是感觉很熟悉呢?
这就是Android视图加载流程(3)之ViewRootImpl的UI刷新机制的Step4.2。ViewRootImpl正准备调用绘制View视图的代码。

ViewRootImpl
void scheduleTraversals() {
    mChoreographer.postCallback(
           Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
    
//实现了Runnable接口
final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}
    
void doTraversal() {
     performTraversals();
}
    
private void performTraversals() {
    //测量
    mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    //布局
    mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
    //绘制
    mView.draw(canvas);
} 

一组看下来是不是觉得很清晰呢。View调用invalidate方法,其实是层层往上递,直到传递到ViewRootImpl后出发sceheduleTraversals方法,然后整个View树开始进行重绘制任务。

理解图:


postInvalidate方法源码分析

上面也说道invalidate方法只能在UI线程中执行,其他需要postInvalidate方法

View
public void postInvalidate() {
    postInvalidateDelayed(0);
}
    
public void postInvalidateDelayed(long delayMilliseconds) {
    // We try only with the AttachInfo because there's no point in invalidating
    // if we are not attached to our window
    final AttachInfo attachInfo = mAttachInfo;
    //核心,实质就是调运了ViewRootImpl.dispatchInvalidateDelayed方法
    if (attachInfo != null) {
        attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
    }
}

此方法必须是在视图已经绑定到Window才能使用,即attachInfo是否为空。随后调用ViewRootImpl的dispatchinvalidateDelayed

ViewRootImple
public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
    Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
    mHandler.sendMessageDelayed(msg, delayMilliseconds);
}

Handler乱入!此时ViewrootImpl类的Handler发送了一条MSG_INVALIDATE消息。哪里接收这个消息呢?

ViewRootImple
final class ViewRootHandler extends Handler {
    
    public void handleMessage(Message msg) {
        ......
        switch (msg.what) {
        case MSG_INVALIDATE:
            ((View) msg.obj).invalidate();
            break;
        ......
        }
        ......
    }
}

实质上还是在UI线程中调用了View的invalidate()方法。

postInvalidate是在子线程中发消息,UI线程接收消息并刷新UI。

理解图:

常见的引起invalidate方法操作的原因主要有:

  • 直接调用invalidate方法.请求重新draw,但只会绘制调用者本身。
  • 触发setSelection方法。请求重新draw,但只会绘制调用者本身。
  • 触发setVisibility方法。 当View可视状态在INVISIBLE转换VISIBLE时会间接调用invalidate方法,继而绘制该View。当View的可视状态在INVISIBLE\VISIBLE 转换为GONE状态时会间接调用requestLayout和invalidate方法,同时由于View树大小发生了变化,所以会请求measure过程以及draw过程,同样只绘制需要“重新绘制”的视图。
  • 触发setEnabled方法。请求重新draw,但不会重新绘制任何View包括该调用者本身。
  • 触发requestFocus方法。请求View树的draw过程,只绘制“需要重绘”的View。

requestLayout方法源码分析

和invalidate类似,层层往上传递。

View
public void requestLayout() {
    ......
    if (mParent != null && !mParent.isLayoutRequested()) {
        //由此向ViewParent请求布局
        //从这个View开始向上一直requestLayout,最终到达ViewRootImpl的requestLayout
        mParent.requestLayout();
    }
    ......
}

获取父类对象,调用requestlayout(),最后到达ViewRootImpl

ViewRootImpl
@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        //View调运requestLayout最终层层上传到ViewRootImpl后最终触发了该方法
        scheduleTraversals();
    }
}

与invalidate是不是很像呢?requestLayout()会分别调用measure和layout过程,在layout过程的时候视图如果有位置变化,如长宽变化,此时会调用draw的过程重新绘制,如果视图没有位置变化,则不会调用draw的过程

至此一整块的视图加载流程结束了!


PS:本文整理自以下文章,若有发现问题请致邮 caoyanglee92@gmail.com

工匠若水 Android应用层View绘制流程与源码分析

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容