Android--View的工作流程

关于 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
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值