Android 开发艺术探索读书笔记 4 -- View 的工作原理(上)

本篇文章主要介绍以下几个知识点:

  • 初识 ViewRoot 和 DecorView;
  • 理解 MeasureSpec;
  • View 的工作流程:measure、layout、draw。
hello,夏天 (图片来源于网络)

4.1 初识 ViewRoot 和 DecorView

为更好的理解 View 的三大流程(measurelayoutdraw),先了解一些基本的概念。

ViewRoot 对应于 ViewRootImpl 类,是连接 WindowManagerDecorView 的纽带,View 的三大流程都是通过 ViewRoot 来完成的。

View 的绘制流程从 ViewRootperformTraversals 方法开始,它经过 measure(测量 View 的宽高),layout(确定 View 在父容器的位置) 和 draw(负责将 View 绘制在屏幕上) 三个过程才能将一个 View 绘制出来,如下:

performTraversals 的工作流程

DecorView 是一个 FrameLayout,View 层的事件都先经过 DecorView,再传递给 View。

DecorView 作为顶级 View,一般它内部会包含一个竖直方向的 LinearLayout,上面是标题栏,下面是内容栏。在 Activity 中通过 setContentView 设置的布局文件就是被加到内容栏中,而内容栏的 id 为 content,可通过 ViewGroup content = findviewbyid(android.R.id.content) 得到 content,通过 content.getChildAt(0) 得到设置的 View。其结构如下:

顶级 View:DecorView 的结构

4.2 理解 MeasureSpec

MeasureSpec 很大程度上决定了一个 View 的尺寸规格。在 View 的测量过程中,系统会将 View 的 LayoutParams 根据父容器所施加的规则转换成对应的 MeasureSpec,再根据这个 measureSpec 来测量出 View 的宽高(测量宽高不一定等于 View 的最终宽高)。

4.2.1 MeasureSpec

MeasureSpec 代表一个32位 int 值,高两位代表 SpecMode(测量模式),低30位代表 SpecSize(某个测量模式下的规格大小),MeasureSpec 内部的一些常量定义如下:

private static final int MODE_SHIFT = 30;
private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
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;

// MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配
public static int makeMeasureSpec(int size, int mode) {
    if (sUseBrokenMakeMeasureSpec) {
        return size + mode;
    } else {
        return (size & ~MODE_MASK) | (mode & MODE_MASK);
    }
}

// 解包:获取其原始的 SpecMode
@MeasureSpecMode
public static int getMode(int measureSpec) {
    return (measureSpec & MODE_MASK);
}

// 解包:获取其原始的 SpecSize
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

4.2.2 MeasureSpec 和 LayoutParams 的对应关系

Layoutparams 需要和父容器一起才能决定 View 的 MeasureSpec,一旦确定 MeasureSpec 后,onMeasure 中就可以确定 View 的测量宽高。

顶级 View(DecorView),其 MeasureSpec 由窗口的尺寸和自身的 Layoutparams 来共同决定;普通 View,其 MeasureSpec 由父容器的 MeasureSpec 和自身的 Layoutparams 来决定。

对于 DecorView,在 ViewRootImpl 中的 measureHierarchy 方法中的一段代码展示了其 MeasureSpec 的创建过程:

// 其中 desiredWindowWidth 和 desiredWindowHeight 是屏幕的尺寸
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth , lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

接下来看下 getRootMeasureSpec 方法的实现:

    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 的 MesourSpec 的产生过程,根据其 Layoutparams 的宽高的参数来划分,遵守如下规则:

  • LayoutParams.MATCH_PARENT
    精确模式,大小就是窗口的大小

  • LayoutParams.WRAP_CONTENT
    最大模式,大小不定,但是不能超出屏幕的大小

  • 固定大小(比如100dp)
    精确模式,大小为 LayoutParams 中指定的大小

对于 普通的 View,指布局中的 View,其 measure 过程由 ViewGroup 传递而来,先看下 ViewGroup 的 measureChildWithMargins 方法:

    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);
        // 调用子元素的 measure 方法前会通过上面的 getChildMeasureSpec 方法得到子元素的 MesureSpec
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

上述对子元素进行 measure,显然,子元素的 MesureSpec 的创建和父容器的 MesureSpec 、子元素的 LayoutParams 有关和 View 的 margin 有关,其中 getChildMeasureSpec 方法如下:

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
        // 参数中的 pading 是指父容器中已占有的控件大小
        // 因此子元素可以用的大小为父容器的尺寸减去 pading
        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:
            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:
            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:
            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);
    }

上述方法主要作用是根据父容器的 MeasureSpec 同时结合 View 本身的 Layoutparams 来确定子元素的 MesureSpec。

上面getChildMeasureSpec 展示了普通 View 的 MeasureSpec 创建规则,也可参考下表(表中的 parentSize 指父容器中目前可使用的大小):

普通 View 的 MeasureSpec 的创建规则

当 View 采用固定宽/高时,不管父容器的 MeasureSpec 是什么,View 的 MeasureSpec 都是精确模式并且其大小遵循 LayoutParams 中的大小。

当 View 的宽/高是 match_parent 时,若父容器是精准模式,那么 View 也是精准模式并且其大小是父容器的剩余空间;若父容器是最大模式,那么 View 也是最大模式并且其大小不会超过父容器的剩余空间。

当 View 的宽/高是 wrap_content 时,不管父容器的模式是精准还是最大化,View 的模式总是最大化,并且大小不能超过父容器的剩余空间。

注:UNSPECIFIED 模式主要用于系统内部多次 Measure 的情形,一般不需关注此模式。

综上,只要提供父容器的 MeasureSpec 和子元素的 LayoutParams,就可以快速地确定出子元素的 MeasureSpec 了,有了 MeasureSpec 就可以进一步确定出子元素测量后的大小了。

4.3 View 的工作流程

View 的工作流程主要是指 measure(测量,确定 View 的测量宽/高)、layout(布局,确定 View 的最终宽/高和四个顶点的位置)、draw(绘制,将 View 绘制到屏幕上)这三大流程。

4.3.1 measure 过程

若只是一个原始的 View,那么通过 measure 方法就完成了其测量过程,若是一个 ViewGroup,除了完成自己的测量过程外,还会遍历去调用所有子元素的 measure 方法,各个子元素再递归去执行这个流程。

4.3.1.1 View 的 measure 过程

View 的 measure 过程由其 measure 方法来完成,measure 方法中会去调用 View 的 onMesure 方法如下:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 设置 View 宽/高的测量值
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

其中 getDefaultSize 方法如下:

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

上面的 AT_MOSTEXACTLY 这两种情况,可理解为 getDefaultSize 返回的大小就是 mesourSpec 中的 specSize,而这个 specSize 就是 View 测量后的大小(测量大小不一定等于 View 的最终大小)。

至于 UNSPECIFIED 这种情况,一般用于系统内部的测量过程,View 的大小为 getDefaultSize的第一个参数是 size,其宽/高获取方法如下:

protected int getSuggestedMinimumWidth() {
    // 1. 若 View 没有设置背景,View 的宽度为 mMinwidth,
    // 而 mMinwidth 对应于 android:minwidth 这个属性所指定的值,
    // 因此 View 的宽度即为 android:minwidth 属性所指定的值,
    // 若这个属性不指定,那么 mMinWidth 则默认为0;
    // 2. 若 View 指定了背景,则View的宽度为max(mMinwidth,mbackground().getMininumwidth)
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

protected int getSuggestedMinimumHeight() {
    return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}

上面注释分析了 getSuggestedMinimumWidth 方法的实现,getSuggestedMinimumHeight和它的原理一样。注释中未说明的 mBackground.getMinimumWidth() 方法(即 Drawable 的 getMinimumWidth方法)如下:

public int getMinimumWidth() {
    final int intrinsicWidth = getIntrinsicWidth();
    // 返回 Drawable的原始宽度(有原始宽度的话),否则就返回0
    return intrinsicWidth > 0 ? intrinsicWidth : 0;
}

总结 getSuggestedMinimumWidth 的逻辑:
若 View 没设背景,那么返回 android:minwidth所指定的值(可为0);
若 View 设了背景,则返回 android:minwidth和背景的最小宽度这两者中的最大值。
View 在 UNSPECIFIED 情况下的测量宽/高即为 getSuggestedMinimumWidthgetSuggestedMinimumHeight的返回值 。

结论:直接继承 View 的自定义控件需要重写 onMeasure 方法并设置 wrap_content 时的自身大小,否则在布局中使用 wrap_content 就相当于使用 match_parent

从上述代码中知道,若 View 在布局中使用 wrap_content,那么它的 specMode 是 AT_MOST 模式,它的宽/高等于 specSize;此情况下 View 的 specSize 是 parentSize,而 parentSize 是父容器中目前可以使用的大小,即父容器当前剩余的空间大小。显然,View 的宽/高就等于父容器当前剩余的空间大小,这种效果和在布局中使用 match_parent 完全一致。

解决上述问题代码如下:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getMode(heightMeasureSpec);
        // 给 View 指定一个默认的内部宽/高(mWidth, mHeight),并在 wrap_content 时设置此宽/高即可
        // 对于非 wrap_content 情形,沿用系统的测量值即可
        //(注:TextView、ImageView 等针对 wrap_content 情形,它们的 onMeasure 方法做了特殊处理)
        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(mWidth, mHeight);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(mWidth, heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpecSize, mHeight);
        }
    }

4.3.1.2 ViewGroup 的 measure 过程

和 View 不同的是,ViewGroup 是一个抽象类,它没有重写 View 的 onMeasure 方法,但它提供了一个 measureChildren 方法:

    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        // ViewGroup 在 measure 时,会对每一个子元素进行 measure 
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

上述代码中的 measureChild 方法如下:

    protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        // 1. 取出子元素的 LayoutParams
        final LayoutParams lp = child.getLayoutParams();
        // 2. 通过 getChidMeasureSpec 来创建子元素的 MeasureSpec
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);
        // 3. 将 MeasureSpec 直接传递给 View 的 measure 方法来进行测量
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

上面代码注释说明了 measurechild 的思想。

由于 ViewGroup 是一个抽象类,其测量过程的 onMeasure 方法需要各个子类去具体实现;不同的 ViewGroup 子类有不同的布局特性,它们的测量细节各不相同,如 LinearLayout 和 RelativeLayout 这两者的布局特性不同,因此 ViewGroup 无法对其 onMeasure 方法做统一实现。


下面通过 LinearLayout 的 onMeasure 方法来分析 ViewGroup 的 measure 过程,先来看一下 LinearLayout 的 onMeasure 方法:

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

这里选择查看竖直方向的 LinearLayout 测量过程,即 measureVertical 方法(其源码比较长就不贴了),这里只描述其大概逻辑:系统会遍历子元素并对每个子元素执行 measureChildBeforeLayout 方法,此方法内部会调用子元素的 measure 方法,当子元素测量完毕之后,LinearLayout 会根据子元素的情况来测量自己的大小。


View 的 measure 过程完成后,通过 getMeasureWidth/Height 可以正确地获取到 View 的测量宽/高。但在系统要多次 measure 才能确定最终的测量宽/高的情况下,在 onMeasure 方法中拿到的测量宽/高可能是不准确的。因此建议在 onLayout 方法中去获取 View 的测量宽/高或者最终宽/高。

问题:如何在 Activity 已启动的时候获取某个 View 的宽/高?

注:由于 View 的 measure 过程和 Activity 的生命周期方法不是同步执行的,无法保证 Activiy 执行了 onCreate、onStart、onResume 时某个 View 已经测量完毕了,从而在 onCreate、onStart、onResume 中均无法正确得View的宽/高信息(若 View 还没测量完毕,那么获得的宽/高就是0)。

这里给出四种方法:

(1)Activity/View#onWindowFocusChanged

onWindowFocusChanged方法是指:View 已初始化完毕,宽/高已准备好,此时去获取宽/高是没问题的(注:当 Activity 继续执行和暂停执行时,onWindowFocusChanged 均会被调用,若频繁地进行 onResumeonPause,那么 onWindowFocusChanged 也会被频繁地调用)。典型代码如下:

    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if (!hasFocus) {
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
        }
    }

(2)view.post(runnable)

通过 post 可将一个 runnable 投递到消息队列的尾部,然后等待 Lopper 调用此 runnable 时,View 就初始化好了。典型代码如下:

    protected void onStart() {
        super.onStart();
        view.post(new Runnable() {
            @Override
            public void run() {
                int width = mTextView.getMeasuredWidth();
                int height = mTextView.getMeasuredHeight();
            }
        });
    }

(3)ViewTreeObserver

使用 ViewTreeObserver 的众多回调可完成这个功能,典型代码如下:

    protected void onStart() {
        super.onStart();

        ViewTreeObserver observer = view.getViewTreeObserver();
        observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                int width = mTextView.getMeasuredWidth();
                int height = mTextView.getMeasuredHeight();
            }
        });
    }

(4)view.measure(int widthMeasureSpec , int heightMeasureSpec)

通过手动测量 View 的宽高,此方法较复杂,根据 View 的LayoutParams 来分情况来处理:

  • match_parent:无法测量出具体的宽高

  • 具体的数值(dp/px):如宽高都是100dp,如下 measure:

 int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
 int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
 view.measure(widthMeasureSpec, heightMeasureSpec);
  • wrap_content:如下measure:
 // View 的尺寸使用30位的二进制表示,即最大是30个1(即 2^30-1),也就是 (1<<30)-1
 int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30)-1, View.MeasureSpec.AT_MOST);
 int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30)-1, View.MeasureSpec.AT_MOST);
 view.measure(widthMeasureSpec, heightMeasureSpec);

关于 View 的 measure,网络上有两个错误的用法。为什么说是错误的,首先其违背了系统的内部实现规范(因为无法通过错误的 MeasureSpec 去得出合理的 SpecMode,从而导致 measure 过程出错),其次不能保证 measure 出正确的结果。

  • 第一种错误的方法:
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(-1, View.MeasureSpec.UNSPECIFIED);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(-1, View.MeasureSpec.UNSPECIFIED);
view.measure(widthMeasureSpec, heightMeasureSpec);
  • 第二种错误的方法:
view.measure(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);

4.3.2 layout 过程

Layout 是 ViewGroup 用来确定子元素的位置的,当 ViewGroup 的位置被确定后,它在 onLayout 中会遍历所有的子元素并调其 layout 方法,在 layout 方法中 onLayout 又被调用。layout 方法确定 View 本身的位置,而 onLayout 方法则会确定所有子元素的位置,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;

        // 1. 通过 setFrame 方法来设定 View 的四个顶点的位置,
        // 即初始化 mLeft,mTop,mRight,mBottom 这四个值
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            // 2. View 的四个顶点一旦确定,那么 View 在父容器的位置也就确定了,
            // 接下来会调用onLayout方法(用途:父容器确定子元素的位置)
            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);
                }
            }
        }

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    }

和 onMeasure 类似,onLayout 的具体位置实现同样和具体布局有关,所有 View 和 ViewGroup 均没有真正的实现 onLayout 方法。 LinearLayout 的 onLayout 如下:

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

LinearLayout 的 onLayout 和 onMeasure 的实现逻辑类似,就 layoutVertical 来说,其主要代码如下:

     void layoutVertical(int left, int top, int right, int bottom) {
        . . .

        final int count = getVirtualChildCount();
        // 遍历所有子元素
        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();

                . . .

                if (hasDividerBeforeChildAt(i)) {
                    childTop += mDividerHeight;
                }

                childTop += lp.topMargin;
                // 调用 setChildFrame 为子元素指定对应的位置
                setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight);
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

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

上述方法中的 setChildFrame 方法,仅仅是调用子元素的 layout 方法而已,如下:

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

这样父元素在 layout 方法中完成自己的定位后,就通过 onLayout 方法去调用子元素的 layout 方法,子元素又会通过自己的 layout 方法来确定自己的位置,这样一层一层传递下去完成整个 View 树的 layout 过程。

问题:View 的测量宽/高和最终宽/高有什么区别?(即:View 的 getMeasureWidthgetWidth 这两个方法有什么区别?)

为了回答这个问题,先看下 getWidthgetHeight 方法的实现:

    public final int getWidth() {
        return mRight - mLeft;
    }

    public final int getHeight() {
        return mBottom - mTop;
    }

可以看出,getWidthgetHeight 返回的刚好是 View 的测量宽度、高度。

对于上面的问题:在 View 的默认实现中,View 的测量宽/高和最终宽/高是相等的,只不过测量宽/高形成于 View 的 measure 过程,一个是 layout 过程,而最终宽/高形成于 View 的 layout 过程,即两者的赋值时机不同,测量宽/高的赋值时机稍微早一些。

日常开发中可用认为 View 的测量宽/高 = 最终宽/高,但某些特殊情况下,如重写 View 的 layout 方法如下:

 public void layout(int l,int t,int r, int b){
     super.layout(l, t, r + 100, b + 100);
 }

上述代码会导致在任何情况下 View 的最终宽/高总是比测量宽/高大 100px。

4.3.3 draw 过程

Draw 过程其作用是将 View 绘制到屏幕上面。View 的绘制过程遵循如下几步:

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

(2)绘制自己 (onDraw)

(3)绘制 children (dispatchDraw)

(4)绘制装饰 (onDrawSrcollBars)

这一点通过 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;

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

        . . .
    }

View 绘制过程的传递是通过 dispatchDraw 来实现的,dispatchDraw 会遍历所有子元素的 draw 方法,如此 draw 事件就一层层地传递下去。View 有一个特殊的方法 setwilINotDraw

public void setwilINotDraw(boolean willNotDraw){
    // 若一个 View 不需要绘制任何内容,那么设置这个标记位为 true 以后,系统会进行相应的优化。
    // 默认情况下,View 没有启用这个校化标记位,但 ViewGroup 会默认启用这个优化标记位。
    setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}

实际开发中,自定义控件继承于 ViewGroup 并且本身不具备绘制功能时,就可以开启这个标记位从而便于系统进行后续的优化。若明确知道一个 ViewGroup 需要通过 onDraw 来绘制内容时,需要显式地关闭 WILL_NOT_DRAW 这个标记位。

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

推荐阅读更多精彩内容