Android--View的工作流程

来源:互联网 发布:雪利酒 知乎 编辑:程序博客网 时间:2024/05/17 07:09

关于 View 的重要性在上一篇文章中已经提到过,就不在赘述了,这两天刚把 View 的工作流程看了一遍,昨天美团笔试正好出了相关的题目,也是幸运。不过最后编程题考的动态规划…直接GG,数据结构还是得好好看啊。不多说,接下来进入正题。

View 的作用就是方便 APP 与用户进行交互,而为了适应不同的业务场景,我们可能需要不同的 View 。尽管现有的 View 库已经十分完善,但在业务中总是会有可能遇到一些比较奇葩的要求,因而我们就不得不去自定义 View 了。而要学会自定义开发一个 View ,就必须要求对 View 的工作流程有很深的理解。

Part.1 初识ViewRoot与DecorVeiw

一个View的初始化包括三个阶段,Measure 、 Layout 、 Draw ,只有经过这三个阶段的 View 才能完整的展现出来并提供给用户使用。但由于一个 APP 中总是含有许许多多不同的 View 组合在一起的,因而这个过程就变得稍微复杂了起来,其过程有点像上一篇文章说到的事件分发。实际上也是对 View 树的一次遍历。

1.1.ViewRoot

ViewRoot 对应 ViewRootImpl 类,它是 WindowManager 和 DecorView 的枢纽, View 的三大流程都是通过 ViewRoot 来完成的。在 ActivityThread 中,Activity 创建完成后,会将 DecorView 添加到 Window 中,同时创建 ViewRootImpl 对象,并建立两者的关联。

在 ViewRoot 中有一个 performTraversals 的方法,其代码十分长,但主要是在这个方法中,performMeasure()、performLayout()和 performDraw() 三个方法,这三个方法就分别对应上文的 Measure -> Layout -> Draw 过程。在 performMeasure 中顶级 View 的 meausre 方法会被调用,而 measure 方法中又会调用 View 的 onMeasure 方法,而在 onMeasure 方法中它又会对所有的子元素进行 measure 过程,进而整个 View 树都被完成了 measure。同理,Layout 和 Draw 的过程都是类似的。

在 measure 过程中,系统完成了对所有 View 的宽高的测量,Layout 过程中,系统将所有 View 以 measure 的结果进行定位,而最后的 Draw 则负责将所有 View 绘制出来的。

1.2.DecorView

尽管每次 Android Studio 给我们生成 MainActivity 的时候总会附带着一个 setContentView() 方法。但大家有没有考虑过为什么这个方法设置的是 contentVeiw 而不是 View 呢?

实际上,我们平时写的 LinerLayout 等 ViewGroup 并不是一个 App 最底层的 View,我们之前说过 View 的分布就像一棵树,这棵树的叶子节点就是各种各样的 View,而连接着这些叶子结点和根节点的就是 ViewGroup。而这棵树的根节点,就是我们所说的 DecorVeiw

public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks{}

可以看到 DecorView 继承自 FrameLayout,而 FrameLayout 本身是一个 ViewGroup,这说明 DecorView 也是一个 ViewGroup。一般来说,DecorView 内部包含着一个竖直方向的LinerLayout,在这个 LinerLayout 中包含着上下两部分,上面是标题栏,下面是内容栏。而我们通过 setContentView 实际上就是在设置内容栏里面的 View ,我们可以通过以下代码获得我们设置的 View:

ViewGroup content = (ViewGroup)findViewById(android.R.id.content);ViewGroup mySetView = content.getChildAt(0);

这也就衔接了上一篇文章,说明了事件是怎么从系统的 View 传递到我们设置的 View 上的了。

Part.2 MeasureSpec

2.1.MeasureSpec的定义

MeasureSpec是一个辅助系统完成 View 的测量的一个类。作为 View 的一个内部工具类,它提供了一种存储格式以存储某个东西的规格以及测量的方法,该值由两部分组成,他们分别是 Size 和 Mode。

每个MeasureSpec都代表着一个32位的int值,高2位代表着 SpecMode(测量方式),低30位代表着 SpecSize(测量大小),该类的最大好处就是将 Mode 和 Size 整合到了一个int值以避免过多的内存分配。

    private static final int MODE_SHIFT = 30;    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;  //将 Mode 位设为11,该值不代表任何测量规格    public static final int UNSPECIFIED = 0 << MODE_SHIFT;    public static final int EXACTLY     = 1 << MODE_SHIFT;    public static final int AT_MOST     = 2 << MODE_SHIFT;    //0 1 2 分别代表UNSPECIFIED、EXACTLY、AT_MOST三种测量模式    /*        将size和mode打包到一个int值里     */    public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,                                          @MeasureSpecMode int mode) {        if (sUseBrokenMakeMeasureSpec) {            return size + mode;        } else {            return (size & ~MODE_MASK) | (mode & MODE_MASK);        }    }    /*        取出int值中代表的测量规格     */    @MeasureSpecMode    public static int getMode(int measureSpec) {        //noinspection ResourceType        return (measureSpec & MODE_MASK);    }    /*        取出int值中的测量大小     */    public static int getSize(int measureSpec) {        return (measureSpec & ~MODE_MASK);    }

SpecMode有三种,对应如下:

UNSPECIFIED

父容器不对 View 有任何限制,要多大给多大,多用于系统内部,表示一种仍未测量的状态。

EXACTLY

父容器已经检测出 View 所需要的精确大小,这个时候 View 的最终大小就是 SpecSize 指定的值。它对应于 LayoutParams 中的 match_parent 和 具体数值的两种模式。

AT_MOST

父容器制定了一个可用的 SpecSize ,View的大小不能比这个值大。它对应着 LayoutParams 中的 wrap_content。

2.2.MeasureSpec 和 LayoutParams 对应关系

系统内部是通过 MeasureSpec 来对 View 进行测量的,但正常状况下我们是通过给 View 设置 LayoutParams 来对 View 加以变化。在测量 View 的时候,系统会将我们给定的 LayoutParams 在父容器的约束下转换成对应的 MeasureSpec ,然后通过这个 MeasureSpec 来确定 View 的测量宽高。

由于顶级 View 没有父容器,故它的测量只需要赋予手机的屏幕大小的长宽和 LayoutParams 即可;但对于普通的 View ,其 MeasureSpec 由父容器啊的 MeasureSpec 和自身的 LayoutParams 来确定。

2.2.1 DecorView 测量

/*    顶级View的测量过程 */    childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);    childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);    private static int getRootMeasureSpec(int windowSize, int rootDimension) {        int measureSpec;        switch (rootDimension) {        case ViewGroup.LayoutParams.MATCH_PARENT:            // Window can't resize. Force root view to be windowSize.            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);            break;        case ViewGroup.LayoutParams.WRAP_CONTENT:            // Window can resize. Set max size for root view.            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);            break;        default:            // Window wants to be an exact size. Force root view to be that size.            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);            break;        }        return measureSpec;    }

可以看到 DecorView 的 MeasureSpec 取决于Windows层的设定,当 DecorView 设置成全屏或者某个自定义大小(无法调整)的时候,此时采取的是 LayoutParams.MATCH_PARENT (对应 MeasureSpec.EXACTLY );当 DecorView 可以调整大小但不能超过 Window 的大小,此时采取 LayoutParams.WRAP_CONTENT (对应 MeasureSpec.AT_MOST);当 DecorView 是一个确切的值的时候(假设长宽均为100dp),此时采取 MeasureSpec.EXACTLY。

2.2.2 普通 View 的测量

    protected void measureChildWithMargins(View child,            int parentWidthMeasureSpec, int widthUsed,            int parentHeightMeasureSpec, int heightUsed) {        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin                        + widthUsed, lp.width);        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin                        + heightUsed, lp.height);        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);    }

上面是普通的 View 获取自身 MeasureSpec 的过程,可以注意到,ViewGroup 本身是会处理父窗体的 padding 和 margin属性的。下面看看不同的父 MeasureSpec 与 View 自身的 LayoutParams 组合如何决定 MeasureSpec的。

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {        int specMode = MeasureSpec.getMode(spec);        int specSize = MeasureSpec.getSize(spec);        int size = Math.max(0, specSize - padding);        int resultSize = 0;        int resultMode = 0;        switch (specMode) {        // Parent has imposed an exact size on us        case MeasureSpec.EXACTLY:      //父View为EXACTLY(定值/LayoutParams.MATCH_PARENT)            if (childDimension >= 0) {                   resultSize = childDimension;                resultMode = MeasureSpec.EXACTLY;            } else if (childDimension == LayoutParams.MATCH_PARENT) {                   // Child wants to be our size. So be it.                resultSize = size;                resultMode = MeasureSpec.EXACTLY;            } else if (childDimension == LayoutParams.WRAP_CONTENT) {                // Child wants to determine its own size. It can't be                // bigger than us.                resultSize = size;                resultMode = MeasureSpec.AT_MOST;            }            break;        // Parent has imposed a maximum size on us        case MeasureSpec.AT_MOST:    //父View为AT_MOST(WRAP_CONTENT)            if (childDimension >= 0) {                // Child wants a specific size... so be it                resultSize = childDimension;                resultMode = MeasureSpec.EXACTLY;            } else if (childDimension == LayoutParams.MATCH_PARENT) {                // Child wants to be our size, but our size is not fixed.                // Constrain child to not be bigger than us.                resultSize = size;                resultMode = MeasureSpec.AT_MOST;            } else if (childDimension == LayoutParams.WRAP_CONTENT) {                // Child wants to determine its own size. It can't be                // bigger than us.                resultSize = size;                resultMode = MeasureSpec.AT_MOST;            }            break;        // Parent asked to see how big we want to be        case MeasureSpec.UNSPECIFIED: //父View为被测量            if (childDimension >= 0) {                // Child wants a specific size... let him have it                resultSize = childDimension;                resultMode = MeasureSpec.EXACTLY;            } else if (childDimension == LayoutParams.MATCH_PARENT) {                // Child wants to be our size... find out how big it should                // be                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;                resultMode = MeasureSpec.UNSPECIFIED;            } else if (childDimension == LayoutParams.WRAP_CONTENT) {                // Child wants to determine its own size.... find out how                // big it should be                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;                resultMode = MeasureSpec.UNSPECIFIED;            }            break;        }        //noinspection ResourceType        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);    }

上面的代码虽比较长,但是并不复杂,具体就是父 SpecMode 与子 LayoutParams 的值互相组合得到不同的结果,对应的值为如下:

|childLayoutParams\parentSpecMode    |EXACTLY | AT_MOST | UNSPECIFIED||:----------------------------------:|:------:|:-------:|:----------:|| dp/px                              | EXACTLY(childSize)  | EXACTLY(childSize)|EXACTLY(childSize) || match_parent | EXACTLY(parentSize) | AT_MOST(parentSize) | UNPSPECIFIED(0) || wrap_content | AT_MOST(parentSize) | AT_MOST(parentSize) | UNPSPECIFIED(0) |

当 View 采取固定宽高的时候,无论父容器的 MeasureSpec 是什么, View 的 MeasureSpec 都是精确模式,且其大小就是 LayoutParams 设置的大小;

当 View 采取 MATCH_PARENT 时:

  1. 如果父容器采取精确模式,那么子 View 也采取精确模式但大小是父容器的大小。
  2. 如果父容器采取至多模式,那么子 View 也才去至多模式但大小事父容器的大小。

当 View 采取 WRAP_CONTENT 时:
1. 如果父容器采取精确模式,那么子 View 将采取至多模式但大小是父容器的大小。
2. 如果父容器采取至多模式,那么子 View 将采取至多模式但大小是父容器的大小。

从理解上来讲,当子 View 已经确定好自己的宽高的时候,父容器只需要考虑不要让子 View 比自己大就行了,所以子 View 即是精确模式。当子 View 使用 match_content 的时候,由于要让自己填充父容器,所以父容器是什么模式,那自己就应该是什么模式。当子 View 采取 wrap_content 的时候,直观的看来 View 的大小应该随自身的内容的多少变化而变化,因而不太受到父容器的测量限制(不能超过最大尺寸),因而都应采用至多模式。

至于 UNPSPECIFIED 这个模式并没有讲是因为我们一般用不到,这个状态一般是系统内部多次测量的情形。

Part.3 Measure

经过前面两 Part 的铺垫,我们已经得知了一个 View 工作过程中所需要的信息,接下来我们来看看一个 View 是如何 measure 的。

如果是一个普通的 View ,那么只需要直接调用 measure 方法就可以完成对自己的测量,但如果是一个 ViewGroup,它会先调用自己的 measure 方法,再遍历它的子 View 调用他们的 measure 方法。

3.1.View 的测量过程

View 的 measure 过程通过调用 View.measure() 方法完成

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {        ...        if (forceLayout || needsLayout) {            // first clears the measured dimension flag            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;            resolveRtlPropertiesIfNeeded();            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);            if (cacheIndex < 0 || sIgnoreMeasureCache) {                // measure ourselves, this should set the measured dimension flag back                onMeasure(widthMeasureSpec, heightMeasureSpec);                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;            } else {                long value = mMeasureCache.valueAt(cacheIndex);                // Casting a long to int drops the high 32 bits, no mask needed                setMeasuredDimensionRaw((int) (value >> 32), (int) value);                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;            }            ...        }        ...    }

从上面的代码可以看出, measure 方法被 final 关键字修饰,因而无法被子类修改。但我们可以发现,当一个 View 需要被 Layout 的时候,它将会调用自己的 onMeasure 方法,该方法是允许子类更改的。在该方法中,View 的宽高就被设定好了。

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));    }    public static int getDefaultSize(int size, int measureSpec) {        int result = size;        int specMode = MeasureSpec.getMode(measureSpec);        int specSize = MeasureSpec.getSize(measureSpec);        switch (specMode) {        case MeasureSpec.UNSPECIFIED:            result = size;            break;        case MeasureSpec.AT_MOST:        case MeasureSpec.EXACTLY:            result = specSize;            break;        }        return result;    }    protected int getSuggestedMinimumWidth() {        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());    }

在 getDefault 方法中,会根据 specMode 来返回最终的结果,由于子 View 的 MeasureSpec 在其父容器内部就已经被决定好了,所以这里的 size 就是 View 测量后的大小。

注意 getDeafultSize 中的 size,该属性来自于 getSuggestedMinimumWidth/Height 方法。当 View 没有背景的时候,此方法返回 mMinWidth,该值对应着 xml 文件中的 android:MinWidth 属性,默认为0。否则返回背景 Drawable 的原始宽度,若无原始宽度则返回0。

从上面的代码中我们可以发现一个问题,当子 View 采取 WRAP_CONTENT 的时候,此时测量模式必定为 AT_MOST,其 specSize 为父容器中可用的大小,这与我们使用 MATCH_PARENT 是一样的,也就是 WRAP_CONTENT 并没有达到我们的预期。

事实上,系统给的一些 View 中,如 ImageView、TextView 等,都针对 WRAP_CONTENT 进行了优化,方才达到了 WRAP_CONTENT 的字面含义。故我们自定义 View 的时候需要根据业务的不同而给定不同的解决方案,最简单的莫过于直接在 onMeasure 中针对 WRAP_CONTENT 的情况直接设定值。

3.2 ViewGroup 的测量

ViewGroup 除了要测量自己的宽高以外,还要依次遍历每个子 View 并调用他们的 measure 方法。和 View 不同的是,ViewGroup 中并没有重写 onMeasure 方法,但它提供了 measureChildren 的方法。

    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {        final int size = mChildrenCount;        final View[] children = mChildren;        for (int i = 0; i < size; ++i) {            final View child = children[i];            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {                measureChild(child, widthMeasureSpec, heightMeasureSpec);            }        }    }    protected void measureChild(View child, int parentWidthMeasureSpec,            int parentHeightMeasureSpec) {        final LayoutParams lp = child.getLayoutParams();        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,                mPaddingLeft + mPaddingRight, lp.width);        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,                mPaddingTop + mPaddingBottom, lp.height);        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);    }

该方法十分简单,遍历并找出所有不处于 GONE 状态的所有子 View ,然后调用 measureChild 方法,然后处理好与 padding 和 margin 相关的属性之后,将它交给子 View 自己测量。

由于不同的 ViewGroup 有不同的特性,故它没有重写 onMeasure 方法,而是交给自己的子类根据自己的需求去覆写。

下面举一个 LinearLayout 的例子:

    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        if (mOrientation == VERTICAL) {            measureVertical(widthMeasureSpec, heightMeasureSpec);        } else {            measureHorizontal(widthMeasureSpec, heightMeasureSpec);        }    }

我们都知道 LinearLayout 需要设置排列方向,其意义就在于辅助系统去绘制出不同的结果,我们以垂直布局来描述 ViewGroup 的具体 measure 过程。

在 measureVertical 方法中,系统首先获取所有子元素的数量,然后逐个遍历并执行 measureChildBeforeLayout 方法,该方法内部调用了子元素的 measure 方法。在该方法中有一个名为 mTotalLength 的变量,当子元素 measure 执行完毕后便立刻获取子元素的高度并加到 mTotalLength 上。在这之后,mTotalLength 会加上自己的 padding。

紧接着就要考虑各种各样的状态了,竖直方向上的 MATCH_PARENT 与确切的值只要考虑子元素的总高度不能高于父容器的剩余空间,主要关注 WRAP_CONTENT。对于使用竖直布局 LinearLayout 而言,处理该问题直接使得子元素的长度为最终的高度即可。

3.? 获取 View 的宽与高

如何获取一个 View 组件的宽高?直觉上来说我们只要直接在 onCreate 方法内部调用 View.getHeight/Width()即可。但这样是不行的,因为 View 的工作的过程与 Activity 的生命周期是不同步的,故在 Activity 的生命周期的回调方法中试图获取宽高很有可能什么都获取不到。为了解决这个问题,这里提出四个解决方案。

  1. Activity/View#onWindowFocusChanged
    @Override    public void onWindowFocusChanged(boolean hasFocus) {        super.onWindowFocusChanged(hasFocus);    }

该方法会在 Activity 的窗口获得和失去焦点的时候各调用一次,此时 View 已经被初始化完毕了,因而在此处可以顺利获取宽高。

  1. view.post(runnable)
    floatingView.post(new Runnable() {        @Override        public void run() {            int width = floatingView.getWidth();            int height = floatingView.getHeight();        }    });

该方法通过 post 方法向 Looper 投递一个任务,当 View 开始处理该任务的时候证明它自身已经被初始化完毕,所以可以顺利获取到宽高。

  1. ViewTreeObserver
    ViewTreeObserver observer = floatingView.getViewTreeObserver();        observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {            @Override            public void onGlobalLayout() {                int width = floatingView.getWidth();                int height = floatingView.getHeight();            }        });

每个 View 中都会有一个 ViewTreeObserver 的对象,该对象提供了众多接口以监控 ViewTree 的状态,此处调用 OnGlobalLayoutListener 的时候,全体 View 已经完成了 layout 的过程,因此可以获取到宽高。

  1. view.measure()
    该方法较为复杂,需要根据不同的情况进行变化。

当 LayoutParams 为 MATCH_PARENT 的时候,对照上面说到的表格,此处会根据父容器的状态生成一致的 MesureMode,但此时需要父容器的大小才能进一步测量子容器,所以无法测量。

当 View 的 LayoutParams 设置为具体的数值的时候,根据表我们知道 MeasureMode 为 EXACTLY,所以可以调用以下方法。

        int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(mySetWidth, View.MeasureSpec.EXACTLY);        int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(mySetHeight, View.MeasureSpec.EXACTLY);        floatingView.measure(widthMeasureSpec,heightMeasureSpec);

当 View 的 LayoutParams 为 WRAP_CONTENT 的时候,对应的 MesureMode 为 AT_MOST,尽管我们不知道父容器的大小的多少,但我们可以用尺寸的最大值进行构造

        int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30) - 1, View.MeasureSpec.AT_MOST);        int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30) - 1, View.MeasureSpec.AT_MOST);        floatingView.measure(widthMeasureSpec,heightMeasureSpec);

这样显式的调用 measure 方法之后再去获取宽高就能成功了。

Part.4 Layout

在经历过 measure 之后,View 就可以进入 Layout 阶段了。该行为首先发生于 ViewGroup 中,紧接着就遍历所有子 View 依次对他们进行 Layout。由于 Layout 不再涉及到测量的部分,因而比起 measure 显得比较简单。

4.1.View 的 Layout

    public void layout(int l, int t, int r, int b) {        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;        }        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);            if (shouldDrawRoundScrollbar()) {                if(mRoundScrollbarRenderer == null) {                    mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);                }            } else {                mRoundScrollbarRenderer = null;            }            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);                }            }        }        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;    }

相比于 measure 来说已经短了很多了,其实上面的代码思路也很清晰,首先是通过setFrame方法设置自己的 left、right、top、bottom 四个值,然后进一步调用 onLayout 方法。注意 View 的 onLayout 方法内部实现是空的,需要开发者自己处理。

4.1.ViewGroup 的 Layout

由于不同的 ViewGroup 对 Layout 的处理不一样,这里我们仍用 VertialLinearLayout 来讲述

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

由于 ViewGroup layout 的逻辑跟 View 的逻辑类似,这里不再复述。此处可以发现 onLaytout 被覆写。

void layoutVertical(int left, int top, int right, int bottom) {        ...        for (int i = 0; i < count; i++) {            final View child = getVirtualChildAt(i);            if (child == null) {                childTop += measureNullChild(i);            } else if (child.getVisibility() != GONE) {                final int childWidth = child.getMeasuredWidth();                final int childHeight = child.getMeasuredHeight();                final LinearLayout.LayoutParams lp =                        (LinearLayout.LayoutParams) child.getLayoutParams();                ...                childTop += lp.topMargin;                setChildFrame(child, childLeft, childTop + getLocationOffset(child),                        childWidth, childHeight);                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);                i += getChildrenSkipCount(child, i);            }        }    }

实际上过程与 measure 的过程类似,它会遍历所有子元素节点并调用子元素的 setChildFrame 方法,而该方法又会去调用子元素的 layout 方法。其中 childtop 会不断的增加,因为子元素不断的堆叠在一起,这正好符合了竖直方向的 linearlayout 的特性。如此反复循环, layout 的过程也完成了。

Part.4 Draw

Draw 过程就是将 View 绘制出来,由于在之前的两步已经完成了,所以这个过程就变得十分简单了,它遵循以下一个过程。

  1. 绘制背景( background.draw( canvas ))

  2. 绘制自己( onDraw )

  3. 绘制子 View ( dispatchDraw )

  4. 绘制装饰 ( onDrawScrollBars )

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

上面的逻辑也十分明了,ViewGroup 会直接调用 View 的 draw 方法,dispatchDraw 方法在 View 内部是空实现,但由于 ViewGroup 覆写了该方法,因而就传递到了子 View 的 draw 上去了。

需要注意的是,View 中有一个特殊的方法 setWillNotDraw,该方法设置为 true 的时候,系统会本 View 进行优化,当我们 View 本身并不需要绘制功能的时候,就可以将其关闭(默认关闭),但这仅仅是对于 View 来说的。

通常的 ViewGroup 都是默开启这个标记位的(通常都不需要绘制 ViewGroup ),但当我们明确的知道某个 ViewGroup 需要被绘制的时候,就要手动打开该标记位。

Part.5 总结

这篇文章写了快有一个星期,主要是最近状态不太好,同时本章涉及到的东西又比较繁杂,所以写的比较慢。反正也是个没有人看的博客嘛哈哈哈~更多的笔试面试将要到来了,可不能懈怠呀!

1 0