来解释一下这个简单布局为什么是这个效果吧!

2,831 阅读8分钟

前言

友情提示:文章比较长,包成了大量代码和debug调用图。为了避免浪费大家的时间,开篇咱们看一段极为简单的代码(如果你能明确的解释这个想象,那么这篇文章没必要看下去):

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/black" />
</LinearLayout>

很简单的布局文件,大家在脑海里尝试预览一下这个布局的效果。如果你认为是满屏黑色,那么这篇文章对你来说是有意义的

因为这个布局预览出来是这样的:

黑色只有很短的一条,而这里本质的问题就在于TextView的onMeasure()方法。OK,今天的文章让咱们好好了解measure的过程...

正文

从现象上来看,很明显咱们这个TextView在measure()的过程之后,就被认定为只有这个高。因此咱们今天就借这个case,好好来研究一番View在测量的过程中到底会受哪些因素影响。

一、我们布局中众多的View是怎么串起来的

开始前,咱们先不着急进入测量这部分。先进行一波准备工作,这一部分,咱们先来回顾一下setContentView的一些知识:

  • 1、我们setContentView的layout文件被LayoutInflate解析完,会以DecorView为parent,和DecorView关联起来。
  • 2、在Activity可见的流程中,Window会调用addView()传入DecorView,这其中会new一个ViewRootImpl,将DecorView加到ViewRootImpl中,同样以parent的形式。
  • 3、这样整个View便串起来了:ViewRootImpl -> DecorView -> FrmeLayout -> ...
  • 4、而后在ViewRootImpl的addView(DecorView)时,会执行requestLayout()方法,开启measure、layout、draw的流程。

debug调用链,如下:

此时requestLayout(),由于parent是null,所以无从执行。而真正意义上的requestLayout()是下边的调用链。

二、如何理解View的测量

首先根据官方文档我们能够明确:measure()的过程是自上而下的。

必须吐槽一下!这是Google开发者文档(国内的官网)...这翻译也是无语了。

有些小伙伴,可能并不了解View的测量过程,但是onMeasure()方法总还是多少有些涉猎吧?(不了解也没关系,这篇文章就是从一个小的demo,来聊一聊measure过程中的关键点)measure的流程可以简单用一个串行流程图表示:


OK,有了上述知识储备,我们就可以开启开篇那个效果的分析了:

requestLayout()方法会调用到View的measure()中,而measure()又会调用到自身的onMeasure()中。而measure()并不是一个可重写的方法,所以既然测量是自上而下,那咱们就从外围LinearLayout中的omMeasure()开始。

三、LinearLayout的onMeasure()

onMeasure()中比较简单,但是这里我们需要明确一下,这个方法的参数是什么含义:

  • MeasureSpec就不用多说了,记录当前View的尺寸和测量模式
  • 另外明确一点,这里的MeasureSpec是父View的
/**
 * @param widthMeasureSpec horizontal space requirements as imposed by the parent.
 * @param heightMeasureSpec vertical space requirements as imposed by the parent.
 */
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    if (mOrientation == VERTICAL) {
        measureVertical(widthMeasureSpec, heightMeasureSpec);
    } else {
        measureHorizontal(widthMeasureSpec, heightMeasureSpec);
    }
}

这里咱们就选measureVertical()追进去,方法里的边界条件非常的多,但其中对于子View的测量过程比较的简单,遍历所有的子View,挨个调用measureChildBeforeLayout()方法,而这个方法最终会走到ViewGroup中的measureChildWithMargins():

protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
    // 这个方法主要就是做了一件事情:通过子View的LayoutParams和父View的MeasureSpec来决定子View的MeasureSpec
    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的LayoutParams是什么时候设置的?

四、View的LayoutParams,是什么时机被设置的?

这里咱们就插空解决一下标题的问题:View的LayoutParams,是什么时机被设置的?

总结起来就是一句话:在LayoutInflate中解析xml中设置的。具体什么样?直接上代码:

void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
    // 省略部分代码
    final View view = createViewFromTag(parent, name, context, attrs);
    final ViewGroup viewGroup = (ViewGroup) parent;
    final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
    rInflateChildren(parser, view, attrs, true);
    viewGroup.addView(view, params);
}

public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new LayoutParams(getContext(), attrs);
}

public LayoutParams(Context c, AttributeSet attrs) {
    TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
    setBaseAttributes(a,
            R.styleable.ViewGroup_Layout_layout_width,
            R.styleable.ViewGroup_Layout_layout_height);
    a.recycle();
}

上述代码描述比较清晰,说白了就是解析xml中,基于这个View在布局中的layout_width、layout_height属性来生成对应的LayoutParams。然后在通过addView()方法将View和LayoutParams绑定到一起。

五、子View的measure()

书归正传,measureChildWithMargins()方法中,同于父View的MeasureSpec子View的LayoutParams来共通决定子View的MeasureSpec,然后调用子View的measure()方法。

这里一共包含了俩个重点:

  • 生成子View的MeasureSpec
  • 执行子View的measure()方法

5.1、生成子View的MeasureSpec

这部分逻辑主要在getChildMeasureSpec()方法中,我们直接追进去就好了:

 public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    // 省略部分初始化代码
    switch (specMode) { 
        case MeasureSpec.EXACTLY: 
            if (childDimension >= 0) { 
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
        case MeasureSpec.AT_MOST: 
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
        case MeasureSpec.UNSPECIFIED: 
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

这部分代码,就是Google定的规则,也没什么好说的。总结起来就是《Android开发艺术探索》中的那张图:

看了这个,咱们就可以思考一下咱们开篇遇到的问题:父View(LinearLayout)是wrap_content,子View(TextView)是match_parent,那么子View的MeasureSpec是什么样子?

有了上边的分析,我们很容易得出答案:parentSize + AT_MOST。因此咱们就知道这种场景下,子View的match_parent意味自己的宽高就是父View的宽高。那么此时父View的宽高是多少呢?

由于这里的父View已经是根View了,那么它的外边便是DecorView,而DecorView的MeasureSpec相对简单些,直接基于Window的宽高和自身的LayoutParams进行计算。

private static int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {
        case ViewGroup.LayoutParams.MATCH_PARENT:
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
    }
    return measureSpec;
}

而DecorView的LayoutParams也很明确,看过setContentView代码的同学应该都比较清楚:

public void setContentView(View view) {
    setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}

因此这种场景下,DecorView的MeasureSpec是屏幕宽高 + EXACTLY,那么父View(LinearLayout)的宽高就很明确了:parentSize + AT_MOST。

是不是发现问题了?

  • 子View(TextView)的MeasureSpec是parentSize + AT_MOST
  • 父View(LinearLayout)的MeasureSpec是parentSize + AT_MOST
  • DecorView的MeasureSpec是屏幕的size + AT_MOST

有了上述的推导:子View的size就应该是屏幕的size!从debug出来的结果也是如此:

可是开篇的实际效果已经否定了这个答案,那么问题出在哪呢?

既然在获取子View的MeasureSpec流程中我们已经明确是:parentSize + AT_MOST。不过咱们别忘了,咱们现在仅仅是获取了子View的MeasureSpec,有了MeasureSpec还需要一个最关键的一步:执行子View的measure()方法

5.2、执行子View的measure()方法

接下来咱们去看一看子View的measure()方法,上述的部分我们已经知道measureChildWithMargins()方法中会基于父View的MeasureSpec和子View的LayoutParams计算子View的MeasureSpec然后调用子View的measure():

protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
    // 省略获取子View的MeasureSpec的过程
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

通过断点的调用链我们可以看到,子View的measure()会调用到子View的onMeasure()中,然后通过setMeasureDimension()最终定下View的测量宽高。

到此咱们可以大概有一个猜想:导致子View(TextView)最终高度不是parentSize的原因,极可能是因为自身的onMeasure()方法

走进onMeasure()方法中,我们会发现TextView的onMeasure()方法实现比较长,因此这里主要抽取关键逻辑:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 省略部分代码
    if (heightMode == MeasureSpec.EXACTLY) {
        height = heightSize;
        mDesiredHeightAtMeasure = -1;
    } else {
        int desired = getDesiredHeight();

        height = desired;
        mDesiredHeightAtMeasure = desired;

        if (heightMode == MeasureSpec.AT_MOST) {
            height = Math.min(desired, heightSize);
        }
    }
    // 省略部分代码
    setMeasuredDimension(width, height);
}

我们的子View(TextView)已经确定是AT_MOST,那么直接看计算结果:

因此我们接下来调查的重点便是getDesiredHeight()方法:

private int getDesiredHeight() {
    return Math.max(
        getDesiredHeight(mLayout, true),
        getDesiredHeight(mHintLayout, mEllipsize != null));
}
// 这其中又间接的调到了Layout中的这个方法
layout.getHeight()
// 而这个方法的实现,就是用一行的height * 所有文字的行数
public int getHeight() {
    return getLineTop(getLineCount());
}

可以看到,这种场景下,TextView的高度是期望mLayout或mHintLayout中max的那个,而这个也是TextView特有的逻辑

OK,看过上面代码注释的同学,到这里应该就恍然大悟了。开篇的那一条黑条就是一行文本的高度。而这个高度就是TextView默认Paint的高度。

这里就不在基于源码展开了,有兴趣的同学可以自己追进去看一下,下边贴几张图来佐证这个结论:

<TextView
    android:layout_width="match_parent"
    android:background="@color/black"
    android:text="111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111"
    android:textColor="@color/white"
    android:layout_height="match_parent"/>

<TextView
    android:layout_width="match_parent"
    android:background="@color/black"
    android:textSize="60sp"
    android:layout_height="match_parent"/>

六、延伸问题1

这里咱们思考一个小问题:我们能不能做到不输入text,就让TextView占满全屏呢?答案是肯定,因为这篇文章主要就是在聊对这个问题的理解。

咱们已经明确这种case下,子View的MeasureSpec是parentSize + AT_MOST,改变最终measure()结果的是onMeasure(),那么我们直接重写onMeasure()...

class TestTextView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null
) : TextView(context, attrs) {
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec))
    }
}

六、延伸问题2

由上述分析,其实咱们明白这种case下:子View的MeasureSpec = parentSize + AT_MOST。由于TextView本身复写了measure()才出现了开篇的效果。那么如果我们用View来替换TextView是不是就能够撑满全屏了?答案是肯定的:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <View
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#000000" />
</LinearLayout>

尾声

这篇文章涉及的知识面着实有些不少,最开始属实没有遇料到这篇文章会牵扯这么多精力。毕竟想要把众多知识点压缩到一篇文章中还是有些难度,何况自己还是一个彩笔。

希望这篇文章能给各位同学带来帮助吧,也欢迎大家留言一起讨论或者是分享给自己身边的好友~