View绘制流程及源码解析(三)——Layout与Draw流程分析

提纲(三).png

在上一篇View绘制流程及源码解析(二)——onMeasure()流程分析这篇文章中,我们详细的分析了很多测量过程的细节(好吧,真是有点过于细节了,感觉寒假的时候这时间花的,简直心疼),这篇我们来分析三大流程最后两个流程,鉴于上一篇文章中事无巨细的分析细节造成的惨痛教训,这篇文章会避免对于细节的死扣,而更加注重整体流程的把握。

一.Layout流程

1.Layout的基本过程

Layout的过程就是ViewGroup用来确定子元素的位置,当ViewGroup的位置被确定之后,它在onLayout中会遍历所有的子元素并调用其Layout方法,在Layout方法中onLayout方法又会被调用。这段话摘自《Android开发艺术探索》,既然Layout的过程是ViewGroup用来确定子元素的位置的,那么ViewGroup的位置又是怎么确定的呢?实际上,类似于Measure()过程,整个布局的过程也是一个递归调用的过程——首先从最顶层的DecorView开始,我们说过,DecorView是一个FrameLayout,也就是一个ViewGroup,调用他的Layout方法,那么他会遍历循环他的子元素,并调用子元素的onLayout方法,如果子元素依然是个ViewGroup,那么调用这个子ViewGroup(如LinearLayout)重写过的onLayout方法;然后在该方法中又会接着往下遍历,调用子View的layout方法,一直扒到最后一层View,假设这个子View是TextView,那么就会调用TextView的onLayout的方法,具体的控件,他们的onLayout的方法都是重写过的,每个有每个自己的布局规矩。

screen_simple.xml中的关系.png

这里我们再贴一下前两篇中都提到的图,大家可以对着这个图心理想一遍这个递归调用的过程。

2.DecorView的Layout过程

首先我们要回到三大流程开始的地方,也就是ViewRootImpl类中的performTraversals()方法中:
(framewoks/base/core/java/android/view/ViewRootImpl):

if (didLayout) {
    performLayout(lp, mWidth, mHeight);

View绘制流程及源码解析(一)——performTraversals()源码分析这篇文章的“第二段代码”前面的一段中,我们说了这个mWidth与mHeight,这两个值实际上就表示的是当前窗口(Window)的大小,或者说当前DecorView的宽高,lp就是DecorView的布局参数。
我们接着看这个方法(framewoks/base/core/java/android/view/ViewRootImpl):

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
        int desiredWindowHeight) {
    ......
    final View host = mView;

    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
    try {
        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

这个方法中,可以看到host调用了layout()方法,这个host是mView赋值过来的,这个mView就是DecorView,我们去到View类中看下这个layout()方法(framewoks/base/core/java/android/view/View):

/*
 * @param l Left position, relative to parent
 * @param t Top position, relative to parent
 * @param r Right position, relative to parent
 * @param b Bottom position, relative to parent
 */
public void layout(int l, int t, int r, int b) {
    ......
    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;

    boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        onLayout(changed, l, t, r, b);
        mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnLayoutChangeListeners != null) {
            ArrayList<OnLayoutChangeListener> listenersCopy =
                    (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
            int numListeners = listenersCopy.size();
            for (int i = 0; i < numListeners; ++i) {
                listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
            }
        }
    }
    ......
}

可以看到,调用了onLayout(changed, l, t, r, b);方法,这个方法的四个参数,看注释可以知道,依次是View/ViewGroup的左、上、右、下四个坐标,而他得调用方法是host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());,这里的host变量就是DecorView,那么传入的这四个参数是什么意思呢?首先我们知道,DecorView是Android屏幕的的顶级View,他上面已经没有父View了,而我们知道,Android当中的原点坐标系是从左上角开始的:

图解Android Window区域划分.png

我们贴一张之前贴过的图,注意左上角的那个红点,我们已经标出了他是Android坐标系的原点,以及DecorView的区域示意图(忽略过扫描区域,那玩意是在屏幕外面的)。因此,我们也不难理解,host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());中的参数了,前两个0,0表示的是Android的坐标系原点(DecorView的左上角),后两个表示是DecorView的测量宽高。

回到上面的源码中,我们可以看到在layout()方法中,调用了onLayout()方法,我们来看看这个方法:

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

如你所见,这个方法是空的——上面我们已经提到,onLayout的实现方法,是整个下发到子View/ViewGroup中的,具体的View具体实现,我们对着上面的screen_simple.xml的结构层次图,试着分析一下LinearLayout的onLayout()方法:

3.LinearLayout的Layout过程

(framewroks/base/core/java/android/widget/linearlayout):

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    if (mOrientation == VERTICAL) {
        layoutVertical(l, t, r, b);
    } else {
        layoutHorizontal(l, t, r, b);
    }
}
void layoutVertical(int left, int top, int right, int bottom) {
    final int paddingLeft = mPaddingLeft;

    int childTop;
    int childLeft;

    // Where right end of child should go
    final int width = right - left;
    int childRight = width - mPaddingRight; //确定子元素的右界限

    // Space available for child
    int childSpace = width - paddingLeft - mPaddingRight;   //子元素的生存空间为,LinearLayout的宽度减左右Padding

    final int count = getVirtualChildCount();   //获取子元素的个数

    final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
    final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;

    switch (majorGravity) { //根据BOTTOM,CENTER_VERTICAL,TOP三种Gravity来确定childTop,即子元素的顶部位置
       case Gravity.BOTTOM:
           childTop = mPaddingTop + bottom - top - mTotalLength;
           break;
           ......
       default:
           childTop = mPaddingTop;
           break;
    }

    for (int i = 0; i < count; i++) {
        final View child = getVirtualChildAt(i);
        if (child == null) {
            childTop += measureNullChild(i);
        } else if (child.getVisibility() != GONE) {     //可见性不是GONE的元素都参与布局
            final int childWidth = child.getMeasuredWidth();
            final int childHeight = child.getMeasuredHeight();

            final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
            ......
            switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {    //根据三种Gravity确定子元素左位置
                case Gravity.CENTER_HORIZONTAL:
                    childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                            + lp.leftMargin - lp.rightMargin;
                 ......
                default:
                    childLeft = paddingLeft + lp.leftMargin;
                    break;
            }

            ......
            childTop += lp.topMargin;
            setChildFrame(child, childLeft, childTop + getLocationOffset(child),childWidth, childHeight);
            childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

            i += getChildrenSkipCount(child, i);
        }
    }
}

上面的流程比较清楚,我们加了一些必要的注释,就不死扣实现的细节了,可以看到调用了setChildFrame(child, childLeft, childTop + getLocationOffset(child),childWidth, childHeight);这个方法:

private void setChildFrame(View child, int left, int top, int width, int height) {
    child.layout(left, top, left + width, top + height);
}

可以看到,这个方法中又调用了子View的layout()方法,如果此时子View也是个ViewGroup,那么回到上面的流程中接着调用;如果此时子View是一个View,那么调用子View(如TextView)的onLayout方法。

二.Draw流程

1.Canvas与Surface

上面performLayout()方法之之心完之后,就该执行performDraw()方法了,我们直接来看这个方法(framewoks/base/core/java/android/view/ViewRootImpl):

private void performDraw() {
    ......
    final boolean fullRedrawNeeded = mFullRedrawNeeded;
    mFullRedrawNeeded = false;

    mIsDrawing = true;
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");
    try {
        draw(fullRedrawNeeded);
    } finally {
        mIsDrawing = false;
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
    ......
}

调用了draw(fullRedrawNeeded);,这个fullRedrawNeeded意思是,需要完全重绘的意思,笔者看了下这个变量,整个ViewRootImpl类中,只有上面一段中的这句:

 final boolean fullRedrawNeeded = mFullRedrawNeeded;
 mFullRedrawNeeded = false;

mFullRedrawNeeded的值变为了false,其他地方均为true,也就是说,你可以先不管这个boolean值代表什么,只要知道他是一个true值就行了。
然后我们接着看draw(fullRedrawNeeded);方法:

private void draw(boolean fullRedrawNeeded) {
    Surface surface = mSurface;
    if (!surface.isValid()) {
        return;
    }
    ......

        if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
            return;
        }
    ......
1).什么是surface?

这个surface是什么呢?Surface是原始图像缓冲区(raw buffer)的一个句柄,而原始图像缓冲区是由屏幕图像合成器(screen compositor)管理的。SDK的中对该类的注释为:Handle onto a raw buffer that is being managed by the screen compositor——处理由屏幕合成器管理的原始缓冲区。

  • 句柄,英文:HANDLE,数据对象进入内存之后获取到内存地址,但是所在的内存地址并不是固定的,需要用句柄来存储内容所在的内存地址。从数据类型上来看它只是一个32位(或64位)的无符号整数。
  • Surface 充当句柄的角色,用来获取原始图像缓冲区以及其中的内容
  • 原始图像缓冲区(raw buffer)用来保存当前窗口的像素数据。

看了这段话,我们大概已经知道了这个Surface是用来干什么的了,他就是一个用来获取原始图像缓冲区中图像数据的句柄~~我们接着看if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty))这个巨长的方法:

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

    // Draw with software renderer.
    final Canvas canvas;
    try {
        ......
        canvas = mSurface.lockCanvas(dirty);
        ......
    } catch (IllegalArgumentException e) {
        ......
    }
    ......
    try {
        ......

        mView.draw(canvas);
        .......

2).什么是Canvas ?

上面这段代码中出现了一个Canvas对象,我们省略了除了关键代码之外的所有代码。首先说一下Canvas是什么?
  先看下这个类的注释:

/**
 * The Canvas class holds the "draw" calls. To draw something, you need
 * 4 basic components: A Bitmap to hold the pixels, a Canvas to host
 * the draw calls (writing into the bitmap), a drawing primitive (e.g. Rect,
 * Path, text, Bitmap), and a paint (to describe the colors and styles for the
 * drawing).
 * /

为了绘制一个东西,我们需要4个元素来协同完成:

  • 位图:Bitmap 来保持(hold)那些像素
  • 画布:Canvas 来响应画画(draw)的调用(并将其写入 bitmap)
  • 画笔:paint 描述画画的颜色和样式等
  • “颜料“:drawing primitive,比如矩形、路径、文字、位图等其他元素

关于这四个元素,我们如果了解自定义View的话应该很熟悉,这个Canvas实际上就是一个画布.

3).Surface与Canvas

可以看到,每一个Surface中保存了一个Canvas对象,因为Canvas是“画布”嘛,它可以暂时保存我们绘画的数据(当然最终是要存到BitMap中);而Surface是操纵图像数据的句柄,因此他保存一个Canvas,通过这个Canvas我们就可以进行各种绘制的工作。
  不论是普通的自定义View,还是SurfaceView自定义Veiw,我们都需要首先获取Surface中的Canvas,通过canvas = mSurface.lockCanvas(dirty);方法,然后才能进行一系列操作,并在操作完成后unlockCanvasAndPost(canvas)方法释放canvas——只不过在SurfaceView中需要手动调用这两个方法,但是在普通的View或者自定义View中,这两个方法是在ViewRootImpl类中由系统自行调用。

我们看看这句:canvas = mSurface.lockCanvas(dirty);,这句代码调用了Surface类中的lockCanvas()方法(framewoks/base/core/java/android/view/Surface):

public Canvas lockCanvas(Rect inOutDirty)
        throws Surface.OutOfResourcesException, IllegalArgumentException {
    synchronized (mLock) {
        checkNotReleasedLocked();
        if (mLockedObject != 0) {
            throw new IllegalArgumentException("Surface was already locked");
        }
        mLockedObject = nativeLockCanvas(mNativeObject, mCanvas, inOutDirty);
        return mCanvas;
    }
}

可以看到,这个方法中,我们返回了一个Canvas对象,并调用native层的方法nativeLockCanvas()锁定这个画布。

2.绘制流程

之后我们就可以看上面那段代码中真正的开始绘制的地方了:mView.draw(canvas);,我们之前说过,mView表示的就是DecorView:

@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;
        }
        ......
    }

可以看到,上面的注释中说的很清楚了,绘制的过程主要分为4个步骤(本来6个步骤,一般情况下跳过2和5):

①绘制背景 drawBackground(canvas);
②绘制自己onDraw(canvas)
/**
 * Implement this to do your drawing.
 *
 * @param canvas the canvas on which the background will be drawn
 */
protected void onDraw(Canvas canvas) {
}

这个方法是一个空方法,不同的子View会重写这个方法来实现自己的绘制;ViewGroup里边没有实现这个方法,笔者看了一下,貌似只有LinearLayout实现了这个方法,但也仅仅是为了在其中调用分割线(drawable)的draw()方法,

③绘制childrendispatchDraw(canvas)
/**
 * 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) {

}

View的绘制机制通过dispatchDraw()方法来传递的,该方法在View类中是一个空方法,但是在ViewGroup类中确实实现的(framewoks/base/core/java/android/view/ViewGroup):

@Override
protected void dispatchDraw(Canvas canvas) {
    boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
    final int childrenCount = mChildrenCount;
    final View[] children = mChildren;
    int flags = mGroupFlags;

    if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
        final boolean buildCache = !isHardwareAccelerated();
        for (int i = 0; i < childrenCount; i++) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
                final LayoutParams params = child.getLayoutParams();
                attachLayoutAnimationParameters(child, params, i, childrenCount);
                bindLayoutAnimation(child);
            }
        }
    ......
        // Draw any disappearing views that have animations
        if (mDisappearingChildren != null) {
            final ArrayList<View> disappearingChildren = mDisappearingChildren;
            final int disappearingCount = disappearingChildren.size() - 1;
            // Go backwards -- we may delete as animations finish
            for (int i = disappearingCount; i >= 0; i--) {
                final View child = disappearingChildren.get(i);
                more |= drawChild(canvas, child, drawingTime);
            }
        }
        ......

可以看到,在ViewGroup的这个方法中,我们循环遍历了子View,并调用他们的draw方法,如此draw事件就这样一层层传递下去了。

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

推荐阅读更多精彩内容