相关文章推荐
帅气的菠菜  ·  窗体 - Windows apps | ...·  2 周前    · 
怕老婆的柠檬  ·  ListBox ItemTemplate ...·  5 月前    · 
飘逸的荔枝  ·  了解gridstack.js-CSDN博客·  6 月前    · 
喝醉的拖把  ·  Python - ...·  1 月前    · 
爱喝酒的刺猬  ·  pagehelper ...·  7 月前    · 
阳光的墨镜  ·  C# GDAL ...·  1 年前    · 

透过另一个视角来观察,所有的 Widget,我们使用的小控件都是Widget。如果TextView和Buttton等

因此,自定义 View 的第一步,我们要在心里默念 – 我们现在要确定一个矩形了!

既然是矩形,那么它肯定有明确的宽高和位置坐标,宽高是在测量阶段得出。然后在布局阶段,确定好位置信息对矩形进行布局,之后的视觉效果就交给绘制流程了,我们是最好的画家。

布局绘画涉及两个过程:测量过程和布局过程。测量过程通过measure方法实现,是View树自顶向下的遍历,每个View在循环过程中将尺寸细节往下传递,当测量过程完成之后,所有的View都存储了自己的尺寸。第二个过程则是通过方法layout来实现的,也是自顶向下的。在这个过程中,每个父View负责通过计算好的尺寸放置它的子View。

好了,我们知道了测量的就是长和宽,我们的目的也就是长和宽。

View 设置尺寸的基本方法

接下来的过程,我将会用一系列比较细致的实验来说明问题,我们先看看在 Android 中使用 Widget 的时候,怎么定义大小。比如我们要在屏幕上使用一个 Button。

<Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="test"/>

这样屏幕上就出现了一个按钮。

上面就是我们日常开发中使用的步骤,通过 layout_width 和 layout_height 属性来设置一个 View 的大小。而在 xml 中,这两个属性有 3 种取值可能。

android:layout_height="wrap_content"   //View 本身的内容决定高度
android:layout_height="match_parent"   //与父视图等高  
android:layout_height="fill_parent"    //与父视图等高  
android:layout_height="100dip"         //精确设置高度值为 100dip  

我们再进一步,现在给 Button 找一个父容器进行观察。父容器背景由特定颜色标识。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="#ff0000">
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="test" />
</RelativeLayout>

可以看到 RelativeLayout 包裹着 Button。我们再换一种情况。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="150dp"
    android:layout_height="wrap_content"
    android:background="#ff0000">
    <Button
        android:layout_width="120dp"
        android:layout_height="wrap_content"
        android:text="test" />
</RelativeLayout>
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="150dp"
    android:layout_height="wrap_content"
    android:background="#ff0000">
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="test" />
</RelativeLayout>
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="150dp"
    android:layout_height="wrap_content"
    android:background="#ff0000">
    <Button
        android:layout_width="1000dp"
        android:layout_height="wrap_content"
        android:text="test" />
</RelativeLayout>

似乎发生了不怎么愉快的事情,Button 想要的长度是 1000 dp,而 RelativeLayout 最终给予的却仍旧是在自己的有限范围参数内。就好比山水庄园向光明开发区政府要地 1 万亩,政府说没有这么多,最多 2000 亩。

Button 是一个 View,RelativeLayout 是一个 ViewGroup。那么对于一个 View 而言,它相当于山水庄园,而 ViewGroup 类似于政府的角色。View 芸芸众生,它们的多姿多彩构成了美丽的 Android 世界,ViewGroup 却有自己的规划,所谓规划也就是以大局为重嘛,尽可能协调管辖区域内各个成员的位置关系。

山水庄园拿地盖楼需要同政府协商沟通,自定义一个 View 也需要同它所处的 ViewGroup 进行协商。

那么,它们的协议是什么?

View 和 ViewGroup 之间的测量协议 MeasureSpec

我们自定义一个 View,onMeasure()是一个关键方法。也是本文重点研究内容。测量自己的大小,为正式布局提供建议。(注意,只是建议,至于用不用,要看onLayout);

public class TestView extends View {
    public TestView(Context context) {
        super(context);
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

onMeasure() 中有两个参数 widthMeasureSpec、heightMeasureSpec。它们是什么?看起来和宽高有关。

它们确实和宽高有关,了解它们需要从一个类说起。MeasureSpec。

MeasureSpec

MeasureSpec 是 View.java 中一个静态类

* MeasureSpec类的源码分析 public class MeasureSpec { // 进位大小 = 2的30次方 // int的大小为32位,所以进位30位 = 使用int的32和31位做标志位 private static final int MODE_SHIFT = 30; // 运算遮罩:0x3为16进制,10进制为3,二进制为11 // 3向左进位30 = 11 00000000000(11后跟30个0) // 作用:用1标注需要的值,0标注不要的值。因1与任何数做与运算都得任何数、0与任何数做与运算都得0 private static final int MODE_MASK = 0x3 << MODE_SHIFT; // UNSPECIFIED的模式设置:0向左进位30 = 00后跟30个0,即00 00000000000 // 通过高2位 public static final int UNSPECIFIED = 0 << MODE_SHIFT; // EXACTLY的模式设置:1向左进位30 = 01后跟30个0 ,即01 00000000000 public static final int EXACTLY = 1 << MODE_SHIFT; // AT_MOST的模式设置:2向左进位30 = 10后跟30个0,即10 00000000000 public static final int AT_MOST = 2 << MODE_SHIFT; * makeMeasureSpec()方法 * 作用:根据提供的size和mode得到一个详细的测量结果吗,即measureSpec public static int makeMeasureSpec(int size, int mode) { return (size & ~MODE_MASK) | (mode & MODE_MASK); // 设计目的:使用一个32位的二进制数,其中:第32和第31位代表测量模式(mode)、后30位代表测量大小(size) * getMode()方法 * 作用:通过measureSpec获得测量模式(mode) public static int getMode(int measureSpec) { return (measureSpec & MODE_MASK); // 即:测量模式(mode) = measureSpec & MODE_MASK; // MODE_MASK = 运算遮罩 = 11 00000000000(11后跟30个0) //原理:保留measureSpec的高2位(即测量模式)、使用0替换后30位 // 例如10 00..00100 & 11 00..00(11后跟30个0) = 10 00..00(AT_MOST),这样就得到了mode的值 * getSize方法 * 作用:通过measureSpec获得测量大小size public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); // size = measureSpec & ~MODE_MASK; // 原理类似上面,即 将MODE_MASK取反,也就是变成了00 111111(00后跟30个1),将32,31替换成0也就是去掉mode,保留后30位的size

MeasureSpec 代表测量规则,而它的手段则是用一个 int 数值来实现。我们知道一个 int 数值有 32 bit。MeasureSpec 将它的高 2 位用来代表测量模式 Mode,低 30 位用来代表数值大小 Size。

  • wrap_content-> MeasureSpec.AT_MOST
  • match_parent -> MeasureSpec.EXACTLY
  • 具体值 -> MeasureSpec.EXACTLY
  • * MeasureSpec类的具体使用 // 1. 获取测量模式(Mode) int specMode = MeasureSpec.getMode(measureSpec) // 2. 获取测量大小(Size) int specSize = MeasureSpec.getSize(measureSpec) // 3. 通过Mode 和 Size 生成新的SpecMode int measureSpec=MeasureSpec.makeMeasureSpec(size, mode);

    上面讲了那么久MeasureSpec,那么MeasureSpec值到底是如何计算得来?
    结论:子View的MeasureSpec值根据子View的布局参数(LayoutParams)和父容器的MeasureSpec值计算得来的,具体计算逻辑封装在getChildMeasureSpec()里。如下图:

  • 子view的大小由父view的MeasureSpec值 和 子view的LayoutParams属性 共同决定
  • 下面,我们来看getChildMeasureSpec()的源码分析:

    * 源码分析:getChildMeasureSpec() * 作用:根据父视图的MeasureSpec & 布局参数LayoutParams,计算单个子View的MeasureSpec * 注:子view的大小由父view的MeasureSpec值 和 子view的LayoutParams属性 共同决定 public static int getChildMeasureSpec(int spec, int padding, int childDimension) { //参数说明 * @param spec 父view的详细测量值(MeasureSpec) * @param padding view当前尺寸的的内边距和外边距(padding,margin) * @param childDimension 子视图的布局参数(宽/高) //父view的测量模式 int specMode = MeasureSpec.getMode(spec); //父view的大小 int specSize = MeasureSpec.getSize(spec); //通过父view计算出的子view = 父大小-边距(父要求的大小,但子view不一定用这个值) int size = Math.max(0, specSize - padding); //子view想要的实际大小和模式(需要计算) int resultSize = 0; int resultMode = 0; //通过父view的MeasureSpec和子view的LayoutParams确定子view的大小 // 当父View的模式为EXACITY时,父view强加给子View确切的值 //一般是父View设置为match_parent或者固定值的ViewGroup switch (specMode) { case MeasureSpec.EXACTLY: // 当子View的LayoutParams>0,即有确切的值 if (childDimension >= 0) { //子View大小为子自身所赋的值,模式大小为EXACTLY resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; // 当子View的LayoutParams为MATCH_PARENT时(-1) } else if (childDimension == LayoutParams.MATCH_PARENT) { //子view大小为父view大小,模式为EXACTLY resultSize = size; resultMode = MeasureSpec.EXACTLY; // 当子view的LayoutParams为WRAP_CONTENT时(-2) } else if (childDimension == LayoutParams.WRAP_CONTENT) { //子view决定自己的大小,但最大不能超过父view,模式为AT_MOST resultSize = size; resultMode = MeasureSpec.AT_MOST; break; // 当父View的模式为AT_MOST时,父view强加给子View一个最大的值。(一般是父view设置为wrap_content) 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; // 当父View的模式为UNSPECIFIED时,父容器不对View有任何限制,要多大给多大 // 多见于ListView、GridView case MeasureSpec.UNSPECIFIED: if (childDimension >= 0) { // 子view大小为子自身所赋的值 resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // 因为父View为UNSPECIFIED,所以MATCH_PARENT的话子类大小为0 resultSize = 0; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // 因为父view为UNSPECIFIED,所以WRAP_CONTENT的话子类大小为0 resultSize = 0; resultMode = MeasureSpec.UNSPECIFIED; break; return MeasureSpec.makeMeasureSpec(resultSize, resultMode);

    MeasureSpec.UNSPECIFIED

    子元素告诉父容器它的宽高想要多大就要多大,你不要限制我(自己的事情自己做主,没有任何限制)。一般开发者几乎不需要处理这种情况,在 ScrollView 或者是 AdapterView 中都会处理这样的情况。所以我们可以忽视它。本文中的示例,基本上会跳过它。

    MeasureSpec.EXACTLY

    此模式说明可以给子元素一个精确的数值

    MeasureSpec.AT_MOST

    当一个 View 的 layout_width 或者 layout_height 的取值为 wrap_content 时,它的测量模式就是 MeasureSpec.AT_MOST。
    此模式下,子 View 希望它的宽或者高由自己决定。ViewGroup 当然要尊重它的要求,但是也有个前提,那就是子视图不能超过ViewGroup 提供的最大值,也就是它期望宽高不能超过父类提供的建议宽高。(自己的事情只能在一个范围内做主)

    <declare-styleable name="TestView"> <attr name="android:text" format="string" /> <attr name="android:textSize" format="dimension"/> </declare-styleable> </resources>

    TestView.java

    public class TestView extends View {
        private  int mTextSize;
        private TextPaint mPaint;
        private String mText;
        public TestView(Context context) {
            this(context,null);
        public TestView(Context context, AttributeSet attrs) {
            this(context, attrs,0);
        public TestView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            TypedArray ta = context.obtainStyledAttributes(attrs,R.styleable.TestView);
            mText = ta.getString(R.styleable.TestView_android_text);
            mTextSize = ta.getDimensionPixelSize(R.styleable.TestView_android_textSize,24);
            ta.recycle();
            mPaint = new TextPaint();
            mPaint.setAntiAlias(true);
            mPaint.setColor(Color.BLACK);
            mPaint.setTextSize(mTextSize);
            mPaint.setTextAlign(Paint.Align.CENTER);
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            int cx = (getWidth() - getPaddingLeft() - getPaddingRight()) / 2;
            int cy = (getHeight() - getPaddingTop() - getPaddingBottom()) / 2;
            canvas.drawColor(Color.RED);
            if (TextUtils.isEmpty(mText)) {
                return;
            canvas.drawText(mText,cx,cy,mPaint);
    
    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/activity_main"
        android:layout_width="match_parent"
        android:layout_margin="20dp"
        android:layout_height="match_parent">
        <com.example.improve.TestView
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:text="test" />
    </RelativeLayout>
    

    我们可以看到在自定义 View 的 TestView 代码中,我们并没有做测量有关的工作,因为我们根本就没有复写它的 onMeasure() 方法。但它却完成了任务,给定 layout_width 和 layout_height 两个属性明确的值之后,它就能够正常显示了。我们再改变一下数值。

    <com.example.improve.TestView
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:text="test" />
    

    将 layout_width 的值改为 match_parent,所以它的宽是由父类决定,但同样它也正常。

    我们已经知道,上面的两种情况其实就是对应 MeasureSpec.EXACTLY 这种测量模式,在这种模式下 TestView 本身不需要进行处理。

    那么有人会问,如果 layout_width 或者 layout_height 的值为 wrap_content 的话,那么会怎么样呢?
    我们继续测试观察。

    <com.example.improve.TestView
            android:layout_width="wrap_content"
            android:layout_height="100dp"
            android:text="test" />
    

    效果和前面的一样,宽度和它的 ViewGroup 同样了。我们再看。

    <com.example.improve.TestView
            android:layout_width="100dp"
            android:layout_height="wrap_content"
            android:text="test"/>
    

    再看一种情况

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/activity_main"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="20dp">
        <com.example.improve.TestView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="test" />
    </RelativeLayout>
    

    但是,这不是我想要的啊!

    wrap_content 对应的测量模式是 MeasureSpec.AT_MOST,所以它的第一要求就是 size 是由 View 本身决定,最大不超过 ViewGroup 能给予的建议数值。

    TestView 如果在宽高上设置 wrap_content 属性,也就代表着,它的大小由它的内容决定,在这里它的内容其实就是它中间位置的字符串。显然上面的不符合要求,那么就显然需要我们自己对测量进行处理。

    我们的思路可以如下:

  • 对于 MeasureSpec.EXACTLY 模式,我们不做处理,将 ViewGroup 的建议数值作为最终的宽高。
  • 对于 MeasureSpec.AT_MOST 模式,我们要根据自己的内容计算宽高,但是数值不得超过 ViewGroup 给出的建议值。
  • protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            int widthSize = MeasureSpec.getSize(widthMeasureSpec);
            int heightMode = MeasureSpec.getMode(heightMeasureSpec);
            int heightSize = MeasureSpec.getSize(heightMeasureSpec);
            /**resultW 代表最终设置的宽,resultH 代表最终设置的高*/
            int resultW = widthSize;
            int resultH = heightSize;
            int contentW = 0;
            int contentH = 0;
            /**重点处理 AT_MOST 模式,TestView 自主决定数值大小,但不能超过 ViewGroup 给出的
             * 建议数值
            if (widthMode == MeasureSpec.AT_MOST) {
                if (!TextUtils.isEmpty(mText)) {
                    contentW = (int) mPaint.measureText(mText);
                    contentW += getPaddingLeft() + getPaddingRight();
                    resultW = Math.min(contentW, widthSize);
            if (heightMode == MeasureSpec.AT_MOST) {
                if (!TextUtils.isEmpty(mText)) {
                    contentH = mTextSize;
                    contentH += getPaddingTop() + getPaddingBottom();
                    resultH = Math.min(contentH, heightSize);
            //一定要设置这个函数,不然会报错
            setMeasuredDimension(resultW, resultH);
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            int cx = getPaddingLeft() + (getWidth() - getPaddingLeft() - getPaddingRight()) / 2;
            int cy = getPaddingTop() + (getHeight() - getPaddingTop() - getPaddingBottom()) / 2;
            Paint.FontMetrics metrics = mPaint.getFontMetrics();
            cy += metrics.descent;
            canvas.drawColor(Color.GREEN);
            if (TextUtils.isEmpty(mText)) {
                return;
            canvas.drawText(mText, cx, cy, mPaint);
    

    代码并不难,我们可以做验证。

    <com.example.improve.TestView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:paddingLeft="10dp"
            android:paddingTop="10dp"
            android:paddingRight="10dp"
            android:text="test"
            android:textSize="24sp" />
    

    现在,我们已经掌握了自定义 View 的测量方法,其实也很简单的嘛。

    但是,还没有完。我们验证的刚刚是自定义 View,对于 ViewGroup 的情况是有些许不同的。

    View 和 ViewGroup,鸡生蛋,蛋生鸡的关系

    ViewGroup 是 View 的子类,但是 ViewGroup 的使命却是装载和组织 View。这好比是母鸡是鸡,母鸡下蛋是为了孵化小鸡,小鸡长大后如果是母鸡又下蛋,那么到底是蛋生鸡还是鸡生蛋?
    自定义 View 的测量,我们已经掌握了,那现在我们编码来测试自定义 ViewGroup 时的测量变现。
    假设我们要制定一个 ViewGroup,我们就给它起一个名字叫 TestViewGroup 好了,它里面的子元素按照对角线铺设,前面说过 ViewGroup 本质上也是一个 View,只不过它多了布局子元素的义务。既然是 View 的话,那么自定义一个 ViewGroup 也需要从测量开始,问题的关键是如何准确地得到这个 ViewGroup 尺寸信息?

    我们还是需要仔细讨论。

  • 当 TestViewGroup 测量模式为 MeasureSpec.EXACTLY 时,这时候的尺寸就可以按照父容器传递过来的建议尺寸。要知道 ViewGroup 也有自己的 parent,在它的父容器中,它也只是一个 View。
  • 当 TestViewGroup 测量模式为 MeasureSpec.AT_MOST 时,这就需要 TestViewGroup 自己计算尺寸数值。就上面给出的信息而言,TestViewGroup 的尺寸非常简单,那就是用自身 padding + 各个子元素的尺寸(包含子元素的宽高+子元素设置的 marging )得到一个可能的尺寸数值。然后用这个尺寸数值与 TestViewGroup 的父容器给出的建议 Size 进行比较,最终结果取最较小值。
  • 当 TestViewGroup 测量成功后,就需要布局了。自定义 View 基本上不要处理这一块,但是自定义 ViewGroup,这一部分却不可缺少。onLayout()是实现所有子控件布局的函数。注意,是所有子控件!那它自己的布局怎么办?后面我们再讲,先讲讲在onLayout()中我们应该做什么。
    我们先看看ViewGroup onLayout()函数的默认行为是什么
    在ViewGroup.java中
  • @Override
    protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
    

    是一个抽象方法,说明凡是派生自ViewGroup的类都必须自己去实现这个方法。像LinearLayout、RelativeLayout等布局,都是重写了这个方法,然后在内部按照各自的规则对子视图进行布局的。

    接下来,我们就可以具体编码了。

    public class TestViewGroup extends ViewGroup {
        public TestViewGroup(Context context) {
            this(context,null);
        public TestViewGroup(Context context, AttributeSet attrs) {
            this(context, attrs,0);
        public TestViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        @Override
        public LayoutParams generateLayoutParams(AttributeSet attrs) {
            //只关心子元素的 margin 信息,所以这里用 MarginLayoutParams
            return new MarginLayoutParams(getContext(),attrs);
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            int widthSize = MeasureSpec.getSize(widthMeasureSpec);
            int heightMode = MeasureSpec.getMode(heightMeasureSpec);
            int heightSize = MeasureSpec.getSize(heightMeasureSpec);
            /**resultW 代表最终设置的宽,resultH 代表最终设置的高*/
            int resultW = widthSize;
            int resultH = heightSize;
            /**计算尺寸的时候要将自身的 padding 考虑进去*/
            int contentW = getPaddingLeft() + getPaddingRight();
            int contentH = getPaddingTop() + getPaddingBottom();
            /**对子元素进行尺寸的测量,这一步必不可少*/
            measureChildren(widthMeasureSpec,heightMeasureSpec);
            MarginLayoutParams layoutParams = null;
            for ( int i = 0;i < getChildCount();i++ ) {
                View child = getChildAt(i);
                layoutParams = (MarginLayoutParams) child.getLayoutParams();
                //子元素不可见时,不参与布局,因此不需要将其尺寸计算在内
                if ( child.getVisibility() == View.GONE ) {
                    continue;
                contentW += child.getMeasuredWidth()
                        + layoutParams.leftMargin + layoutParams.rightMargin;
                contentH += child.getMeasuredHeight()
                        + layoutParams.topMargin + layoutParams.bottomMargin;
            /**重点处理 AT_MOST 模式,TestViewGroup 通过子元素的尺寸自主决定数值大小,但不能超过
             *  ViewGroup 给出的建议数值
            if ( widthMode == MeasureSpec.AT_MOST ) {
                resultW = contentW < widthSize ? contentW : widthSize;
            if ( heightMode == MeasureSpec.AT_MOST ) {
                resultH = contentH < heightSize ? contentH : heightSize;
            //一定要设置这个函数,不然会报错
            setMeasuredDimension(resultW,resultH);
        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            int topStart = getPaddingTop();
            int leftStart = getPaddingLeft();
            int childW = 0;
            int childH = 0;
            MarginLayoutParams layoutParams = null;
            for ( int i = 0;i < getChildCount();i++ ) {
                View child = getChildAt(i);
                layoutParams = (MarginLayoutParams) child.getLayoutParams();
                //子元素不可见时,不参与布局,因此不需要将其尺寸计算在内
                if ( child.getVisibility() == View.GONE ) {
                    continue;
                childW = child.getMeasuredWidth();
                childH = child.getMeasuredHeight();
                leftStart += layoutParams.leftMargin;
                topStart += layoutParams.topMargin;
                child.layout(leftStart,topStart, leftStart + childW, topStart + childH);
                leftStart += childW + layoutParams.rightMargin;
                topStart += childH + layoutParams.bottomMargin;
    

    然后我们将之添加进 xml 布局文件中进行测试。

    <?xml version="1.0" encoding="utf-8"?>
    <com.example.improve.TestViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">
        <com.example.improve.TestView
            android:layout_width="120dp"
            android:layout_height="wrap_content"
            android:paddingLeft="2dp"
            android:paddingTop="2dp"
            android:paddingRight="2dp"
            android:text="test"
            android:textSize="24sp" />
        <TextView
            android:layout_width="120dp"
            android:layout_height="50dp"
            android:background="#00ff40"
            android:paddingLeft="2dp"
            android:paddingTop="2dp"
            android:paddingRight="2dp"
            android:text="test"
            android:textSize="24sp" />
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@mipmap/ic_launcher"
            android:text="test" />
    </com.example.improve.TestViewGroup>
    

    再试验一下给 TestViewGroup 加上固定宽高。

    <?xml version="1.0" encoding="utf-8"?>
    <com.example.improve.TestViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="350dp"
        android:layout_height="400dp"
        android:background="#c3c3c3">
        <com.example.improve.TestView
            android:layout_width="120dp"
            android:layout_height="wrap_content"
            android:paddingLeft="2dp"
            android:paddingTop="2dp"
            android:paddingRight="2dp"
            android:text="test"
            android:textSize="24sp" />
        <TextView
            android:layout_width="120dp"
            android:layout_height="50dp"
            android:background="#00ff40"
            android:paddingLeft="2dp"
            android:paddingTop="2dp"
            android:paddingRight="2dp"
            android:text="test"
            android:textSize="24sp" />
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@mipmap/ic_launcher"
            android:text="test" />
    </com.example.improve.TestViewGroup>
    

    结果如下:

    自此,我们也知道了自定义 ViewGroup 的基本步骤,并且能够处理 ViewGroup 的各种测量模式。

    但是,在现实工作开发过程中,需求是不定的,我上面讲的内容只是基本的规则,大家熟练于心的时候才能从容应对各种状况。

    getMeasuredWidth()与getWidth()

    趁热打铁,就这个例子,我们讲一个很容易出错的问题:getMeasuredWidth()与getWidth()的区别。他们的值大部分时间都是相同的,但意义确是根本不一样的,我们就来简单分析一下。

  • 首先getMeasureWidth()方法在measure()过程结束后就可以获取到了,而getWidth()方法要在layout()过程结束后才能获取到。
  • getMeasureWidth()方法中的值是通过setMeasuredDimension()方法来进行设置的,而getWidth()方法中的值则是通过layout(left,top,right,bottom)方法设置的。
  • setMeasuredDimension()提供的测量结果只是为布局提供建议,最终的取用与否要看layout()函数。大家再看看我们上面写的TestViewGroup,是不是我们自己使用child.layout(leftStart,topStart, leftStart + childW, topStart + childH)来定义了各个子控件所应在的位置:

                childW = child.getMeasuredWidth();
                childH = child.getMeasuredHeight();
                leftStart += layoutParams.leftMargin;
                topStart += layoutParams.topMargin;
                child.layout(leftStart, topStart, leftStart + childW, topStart + childH);
    

    从代码中可以看到,我们使用child.layout(leftStart, topStart, leftStart + childW, topStart + childH);来布局控件的位置,其中getWidth()的取值就是这里的右坐标减去左坐标的宽度;因为我们这里的宽度是,leftStart + childW,而getMeasuredWidth()与getWidth()的值是一样的。如果我们在调用layout()的时候传进去的宽度值不与getMeasuredWidth()相同,那必然getMeasuredWidth()与getWidth()的值就不再一样了。

    一定要注意的一点是:getMeasureWidth()方法在measure()过程结束后就可以获取到了,而getWidth()方法要在layout()过程结束后才能获取到。再重申一遍!!!

    TestViewGroup自己什么时候被布局

    在onLayout()中布局它所有的子控件。那它自己什么时候被布局呢?它当然也有父控件,它的布局也是在父控件中由它的父控件完成的,就这样一层一层地向上由各自的父控件完成对自己的布局。真到所有控件的最顶层结点,在所有的控件的最顶部有一个ViewRoot,它才是所有控件的最终祖先结点。那让我们来看看它是怎么来做的吧。

    /* final 标识符 , 不能被重载 , 参数为每个视图位于父视图的坐标轴 
     * @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 final void layout(int l, int t, int r, int b) {  
        boolean changed = setFrame(l, t, r, b); //设置每个视图位于父视图的坐标轴  
        if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {  
            if (ViewDebug.TRACE_HIERARCHY) {  
                ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);  
            onLayout(changed, l, t, r, b);//回调onLayout函数 ,设置每个子视图的布局  
            mPrivateFlags &= ~LAYOUT_REQUIRED;  
        mPrivateFlags &= ~FORCE_LAYOUT;  
    

    在setFrame(l,t,r,b)就是设置自己的位置,设置结束以后才会调用onLayout(changed, l, t, r, b)来设置内部所有子控件的位置。
    OK啦,到这里有关onMeasure()和onLayout()的内容就讲完啦,想必大家应该也对整个布局流程有了一个清楚的认识了,下面我们再看一个紧要的问题:如何得到自定义控件的左右间距margin值。

    获取子控件Margin的方法

    我会先简单粗暴的教大家怎么先获取到margin值,然后再细讲为什么这样写,他们的原理是怎样的。

    如果要自定义ViewGroup支持子控件的layout_margin参数,则自定义的ViewGroup类必须重载generateLayoutParams()函数,并且在该函数中返回一个ViewGroup.MarginLayoutParams派生类对象,这样才能使用margin参数。
    我们在上面TestViewGroup例子的基础上,添加上layout_margin参数;

    <?xml version="1.0" encoding="utf-8"?>
    <com.as.customview.TestViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:id="@+id/activity_main"
        android:layout_width="match_parent"
        android:background="#ff00ff"
        android:layout_height="match_parent">
        <com.as.customview.TestView
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:layout_margin="10dp"
            android:text="test"
            android:background="#FF5722"
            android:textSize="60sp" />
        <com.as.customview.TestView
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:layout_margin="10dp"
            android:text="test"
            android:background="#4CAF50"
            android:textSize="60sp" />
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="10dp"
            android:src="@mipmap/ic_launcher"
            android:text="test" />
    </com.as.customview.TestViewGroup>
    

    我们在每个TestView中都添加了一android:layout_margin参数,而且值是10dp;背景也都分别改为了橙色,绿色和一张图片;
    现在我们运行一上,看看效果:

    我们在onLayout()中没有根据Margin来布局,当然不会出现有关Margin的效果啦。需要特别注意的是,如果我们在onLayout()中根据margin来布局的话,那么我们在onMeasure()中计算TestViewGroup的大小时,也要加上margin,不然会导致TestViewGroup太小,而控件显示不全的问题。费话不多说,我们直接看代码实现。

    @Override
    protected LayoutParams generateLayoutParams(LayoutParams p) {
        return new MarginLayoutParams(p);
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new MarginLayoutParams(LayoutParams.MATCH_PARENT,
                LayoutParams.MATCH_PARENT);
    

    首先,在TestViewGroup在初始化子控件时,会调用LayoutParams generateLayoutParams(LayoutParams p)来为子控件生成对应的布局属性,但默认只是生成layout_width和layout_height所以对应的布局参数,即在正常情况下的generateLayoutParams()函数生成的LayoutParams实例是不能够取到margin值的。即:

    *从指定的XML中获取对应的layout_width和layout_height值 public LayoutParams generateLayoutParams(AttributeSet attrs) { return new LayoutParams(getContext(), attrs); *如果要使用默认的构造方法,就生成layout_width="wrap_content"、layout_height="wrap_content"对应的参数 protected LayoutParams generateDefaultLayoutParams() { return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);

    所以,如果我们还需要margin相关的参数就只能重写generateLayoutParams()函数了:

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

    由于generateLayoutParams()的返回值是LayoutParams实例,而MarginLayoutParams是派生自LayoutParam的;所以根据类的多态的特性,可以直接将此时的LayoutParams实例直接强转成MarginLayoutParams实例;
    所以下面这句在这里是不会报错的:

    MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    

    大家也可以为了安全起见利用instanceOf来做下判断,如下:

    MarginLayoutParams lp = null
    if (child.getLayoutParams() instanceof  MarginLayoutParams) {
        lp = (MarginLayoutParams) child.getLayoutParams();
    

    所以整体来讲,就是利用了类的多态特性!下面来看看MarginLayoutParams和generateLayoutParams()都做了什么。

    generateLayoutParams()实现

    //位于ViewGrop.java中
    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();
    protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
        width = a.getLayoutDimension(widthAttr, "layout_width");
        height = a.getLayoutDimension(heightAttr, "layout_height");
    

    从上面的代码中明显可以看出,generateLayoutParams()调用LayoutParams()产生布局信息,而LayoutParams()最终调用setBaseAttributes()来获得对应的宽,高属性。
    这里是通过TypedArray对自定义的XML进行值提取的过程,难度不大,不再细讲。从这里也可以看到,generateLayoutParams生成的LayoutParams属性只有layout_width和layout_height的属性值。

    下面再来看看MarginLayoutParams的具体实现,其实通过上面的过程,大家也应该想到,它也是通过TypeArray来解析自定义属性来获得用户的定义值的(大家看到长代码不要害怕,先列出完整代码,下面会分段讲):

    public MarginLayoutParams(Context c, AttributeSet attrs) {
        super();
        TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout);
        int margin = a.getDimensionPixelSize(
                com.android.internal.R.styleable.ViewGroup_MarginLayout_layout_margin, -1);
        if (margin >= 0) {
            leftMargin = margin;
            topMargin = margin;
            rightMargin= margin;
            bottomMargin = margin;
        } else {
           leftMargin = a.getDimensionPixelSize(
                   R.styleable.ViewGroup_MarginLayout_layout_marginLeft,
                   UNDEFINED_MARGIN);
           rightMargin = a.getDimensionPixelSize(
                   R.styleable.ViewGroup_MarginLayout_layout_marginRight,
                   UNDEFINED_MARGIN);
           topMargin = a.getDimensionPixelSize(
                   R.styleable.ViewGroup_MarginLayout_layout_marginTop,
                   DEFAULT_MARGIN_RESOLVED);
           startMargin = a.getDimensionPixelSize(
                   R.styleable.ViewGroup_MarginLayout_layout_marginStart,
                   DEFAULT_MARGIN_RELATIVE);
           endMargin = a.getDimensionPixelSize(
                   R.styleable.ViewGroup_MarginLayout_layout_marginEnd,
                   DEFAULT_MARGIN_RELATIVE);
        a.recycle();
    

    这段代码分为两部分:
    第一部分:提取layout_margin的值并设置

    TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout);
    int margin = a.getDimensionPixelSize(
            com.android.internal.R.styleable.ViewGroup_MarginLayout_layout_margin, -1);
    if (margin >= 0) {
        leftMargin = margin;
        topMargin = margin;
        rightMargin= margin;
        bottomMargin = margin;
    } else {
    

    在这段代码中就是通过提取layout_margin的值来设置上,下,左,右边距的。
    第二部分:如果用户没有设置layout_margin,而是单个设置的,那么就一个个提取,代码如下:

    leftMargin = a.getDimensionPixelSize(
            R.styleable.ViewGroup_MarginLayout_layout_marginLeft,
            UNDEFINED_MARGIN);
    rightMargin = a.getDimensionPixelSize(
            R.styleable.ViewGroup_MarginLayout_layout_marginRight,
            UNDEFINED_MARGIN);
    topMargin = a.getDimensionPixelSize(
            R.styleable.ViewGroup_MarginLayout_layout_marginTop,
            DEFAULT_MARGIN_RESOLVED);
    startMargin = a.getDimensionPixelSize(
            R.styleable.ViewGroup_MarginLayout_layout_marginStart,
            DEFAULT_MARGIN_RELATIVE);
    endMargin = a.getDimensionPixelSize(
            R.styleable.ViewGroup_MarginLayout_layout_marginEnd,
            DEFAULT_MARGIN_RELATIVE);
    

    这里就是对layout_marginLeft、layout_marginRight、layout_marginTop、layout_marginBottom的值一个个提取的过程。
    从这里大家也可以看到为什么非要重写generateLayoutParams()函数了,就是因为默认的generateLayoutParams()函数只会提取layout_width、layout_height的值,只有MarginLayoutParams()才具有提取margin间距的功能!!!!

    TestViewGroup 作为一个演示用的例子,只为了说明测量规则和基本的自定义方法。对于 Android 开发初学者而言,还是要多阅读代码,关键是要多临摹别人的优秀的自定义 View 或者 ViewGroup。

    我个人觉得,尝试自己动手去实现一个流式标签控件,对于提高自定义 ViewGroup 的能力是有很大的提高,因为只有在自己实践中思考,在思考和实验的过程你才会深刻的理解测量机制的用途。

    不过自定义一个流式标签控件是另外一个话题了,也许我会另外开一篇来讲解,不过我希望大家亲自动手去实现它。

    洋洋洒洒写了这么多的内容,其实基本上已经完结了,已经不耐烦的同学可以直接跳转到后面的总结。但是,对于有钻研精神的同学来讲,其实还不够。还没有完。

    问题1:到底是谁在测量 View ?

    问题2:到底是什么时候需要测量 View ?
    针对问题 1:
    我们在自定义 TestViewGroup 的时候,在 onMeasure() 方法中,通过了一个 API 对子元素进行了测量,这个 API 就是 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);
      * 分析2:measureChild()
      * 作用:a. 计算单个子View的MeasureSpec
      *      b. 测量每个子View最后的宽 / 高:调用子View的measure()
      protected void measureChild(View child, int parentWidthMeasureSpec,
                int parentHeightMeasureSpec) {
            // 1. 获取子视图的布局参数
            final LayoutParams lp = child.getLayoutParams();
            // 2. 根据父视图的MeasureSpec & 布局参数LayoutParams,计算单个子View的MeasureSpec
            final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,// 获取 ChildView 的 widthMeasureSpec
                    mPaddingLeft + mPaddingRight, lp.width);
            final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,// 获取 ChildView 的 heightMeasureSpec
                    mPaddingTop + mPaddingBottom, lp.height);
            // 3. 将计算好的子View的MeasureSpec值传入measure(),进行最后的测量
            // 下面的流程即类似单一View的过程,此处不作过多描述
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    

    代码简短易懂,分别调用 child 的 measure() 方法。值得注意的是,传递给 child 的测量规格已经发生了变化,比如 widthMeasureSpec 变成了 childWidthMeasureSpec。原因是这两行代码:
    一开始我们就了解子视图是如何测量的

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);
    

    我们继续向前,ViewGroup 的 measureChild() 方法最终会调用 View.measure() 方法。我们进一步跟踪。

    * 源码分析:measure() * 定义:Measure过程的入口;属于View.java类 & final类型,即子类不能重写此方法 * 作用:基本测量逻辑的判断 public final void measure(int widthMeasureSpec, int heightMeasureSpec) { // 参数说明:View的宽 / 高测量规格 int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 : mMeasureCache.indexOfKey(key); if (cacheIndex < 0 || sIgnoreMeasureCache) { onMeasure(widthMeasureSpec, heightMeasureSpec); // 计算视图大小 ->>分析1 } else { * 分析1:onMeasure() * 作用:a. 根据View宽/高的测量规格计算View的宽/高值:getDefaultSize() * b. 存储测量后的View宽 / 高:setMeasuredDimension() protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 参数说明:View的宽 / 高测量规格 setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); protected int getSuggestedMinimumWidth() { //mMinWidth = android:minWidth属性所指定的值; return (mBackground == null) ? mMinWidth : max(mMinWidth,mBackground.getMinimumWidth()); //getSuggestedMinimumHeight()同理 * 分析2:setMeasuredDimension() * 作用:存储测量后的View宽 / 高 * 注:该方法即为我们重写onMeasure()所要实现的最终目的 protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { // 将测量后子View的宽 / 高值进行传递 mMeasuredWidth = measuredWidth; mMeasuredHeight = measuredHeight; mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET; // 由于setMeasuredDimension()的参数是从getDefaultSize()获得的 // 下面我们继续看getDefaultSize()的介绍 * 分析3:getDefaultSize() * 作用:根据View宽/高的测量规格计算View的宽/高值 public static int getDefaultSize(int size, int measureSpec) { // 参数说明: // size:提供的默认大小 // measureSpec:宽/高的测量规格(含模式 & 测量大小) // 设置默认大小 int result = size; // 获取宽/高测量规格的模式 & 测量大小 int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { // 模式为UNSPECIFIED时,使用提供的默认大小 = 参数Size case MeasureSpec.UNSPECIFIED: result = size; break; // 模式为AT_MOST,EXACTLY时,使用View测量后的宽/高值 = measureSpec中的Size case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = specSize; break; // 返回View的宽/高值 return result; public int getMinimumWidth() { final int intrinsicWidth = getIntrinsicWidth(); //返回背景图Drawable的原始宽度 return intrinsicWidth > 0 ? intrinsicWidth :0 ; // 由源码可知:mBackground.getMinimumWidth()的大小 = 背景图Drawable的原始宽度 // 若无原始宽度,则为0; // 注:BitmapDrawable有原始宽度,而ShapeDrawable没有

    最后,我们在看看测量的流程图

    道生一,一生二,二生三,三生万物,万物负阴而抱阳,冲气以为和。– 《道德经》

    我们已经知道,不管是对于 View 还是 ViewGroup 而言,测量的起始是 measure() 方法,沿着控件树一路遍历下去。那么,对于 Android 一个 Activity 而言,它的顶级 View 或者顶级 ViewGroup 是哪一个呢?

    从 setContentView 说起

    我们知道给 Activity 布局的时候,在 onCreate() 中设置 setContentView() 的资源文件就是我们普通开发者所能想到的比较顶层的 View 了。比如在 activity_main.xml 中设置一个 RelativeLayout,那么这个 RelativeLayout 就是 Activity 最顶层的 View 吗?谁调用它的 measure() 方法触发整个控件树的测量?

    public void setContentView(int layoutResID) { getWindow().setContentView(layoutResID); initActionBar(); public Window getWindow() { return mWindow; * Abstract base class for a top-level window look and behavior policy. An * instance of this class should be used as the top-level view added to the * window manager. It provides standard UI policies such as a background, title * area, default key processing, etc. * <p>The only existing implementation of this abstract class is * android.policy.PhoneWindow, which you should instantiate when needing a * Window. Eventually that class will be refactored and a factory method * added for creating Window instances without knowing about a particular * implementation. public abstract class Window { public class PhoneWindow extends Window implements MenuBuilder.Callback {

    可以看到,调用 Activity.setContentView() 其实就是调用 PhoneWindow.setContentView()。

    PhoneWindow.java

    @Override
    public void setContentView(View view) {
        setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    @Override
    public void setContentView(View view, ViewGroup.LayoutParams params) {
        if (mContentParent == null) {
            installDecor();
        } else {
            mContentParent.removeAllViews();
        mContentParent.addView(view, params);
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
    

    注意,在上面代码中显示,通过 setContentView 传递进来的 view 被添加到了一个 mContentParent 变量上了,所以可以回答上面的问题,通过 setContentView() 中传递的 View 并不是 Activity 最顶层的 View。我们再来看看 mContentParent。

    它只是一个 ViewGroup。我们再把焦点聚集到 installDecor() 这个函数上面。

    private void installDecor() {
        if (mDecor == null) {
            mDecor = generateDecor();
        if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);
            // Set up decor part of UI to ignore fitsSystemWindows if appropriate.
            mDecor.makeOptionalFitsSystemWindows();
            mTitleView = (TextView)findViewById(com.android.internal.R.id.title);
            if (mTitleView != null) {
                mTitleView.setLayoutDirection(mDecor.getLayoutDirection());
                if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) {
                    View titleContainer = findViewById(com.android.internal.R.id.title_container);
                    if (titleContainer != null) {
                        titleContainer.setVisibility(View.GONE);
                    } else {
                        mTitleView.setVisibility(View.GONE);
                    if (mContentParent instanceof FrameLayout) {
                        ((FrameLayout)mContentParent).setForeground(null);
                } else {
                    mTitleView.setText(mTitle);
            } else {
                mActionBar = (ActionBarView) findViewById(com.android.internal.R.id.action_bar);
    

    代码很长,我删除了一些与主题无关的代码。这个方法体内引出了一个 mDecor 变量,它通过 generateDecor() 方法创建。DecorView 是 PhoneWindow 定义的一个内部类,实际上是一个 FrameLayout。

    private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
    

    我们回到 generate() 方法

    protected DecorView generateDecor() {
        return new DecorView(getContext(), -1);
    

    DecorView 怎么创建的我们已经知晓,现在看看 mContentParent 创建方法 generateLayout()。它传递进了一个 DecorView,所以它与 mDecorView 肯定有某种关系。

    protected ViewGroup generateLayout(DecorView decor) {
            // Apply data from current theme.
        WindowManager.LayoutParams params = getAttributes();
        // Inflate the window decor.
        // Embedded, so no decoration is needed.
        layoutResource = com.android.internal.R.layout.screen_simple;
        // System.out.println("Simple!");
        mDecor.startChanging();
        View in = mLayoutInflater.inflate(layoutResource, null);
        decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        if (contentParent == null) {
            throw new RuntimeException("Window couldn't find content container view");
        mDecor.finishChanging();
        return contentParent;
    

    原代码很长,我删除了一些繁琐的代码,整个流程变得很清晰,这个方法内 inflate 了一个 xml 文件,然后被添加到了 mDecorView。而 mContentParent 就是这个被添加进去的 view 中。
    这个 xml 文件是 com.android.internal.R.layout.screen_simple,我们可以从 SDK 包中找出它来。

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="true"
        android:orientation="vertical">
        <ViewStub android:id="@+id/action_mode_bar_stub"
                  android:inflatedId="@+id/action_mode_bar"
                  android:layout="@layout/action_mode_bar"
                  android:layout_width="match_parent"
                  android:layout_height="wrap_content" />
        <FrameLayout
             android:id="@android:id/content"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:foregroundInsidePadding="false"
             android:foregroundGravity="fill_horizontal|top"
             android:foreground="?android:attr/windowContentOverlay" />
    </LinearLayout>
    

    就是一个 LinearLayout ,方向垂直。2 个元素,一个是 actionbar,一个是 content。并且 ViewStub 导致 actionbar 需要的时候才会进行加载。

    总之由以上信息,我们可以得到 Activity 有一个 PhoneWindow 对象,PhoneWindow 中有一个 DecorView,DecorView 内部有一个 LinearLayout,LinearLayout 中存在 id 为 android:id/content 的布局 mContentParent。 mContentParent加载Activity 通过 setContentView 传递进来的 View,所以整个结构呼之欲出。

    注意:因为代码有删简,实际上 LinearLayout 由两部分组成,下面的是 Content 无疑,上面的部分不一定是 ActionBar,也可能是 title,不过这不影响我们,我们只需要记住 content 就好了。

    谁测绘了顶级 View ?

    既然 DecorView 是整个测绘的发起点,那么谁对它进行了测绘?谁调用了它的 measure() 方法,从而导致整个控件树自上至下的尺寸测量?

    我们平常开发知道调用一个 View.requestLayout() 方法,可以引起界面的重新布局,那么 requestLayout() 干了什么?

    我们再回到 PhoneWindow 的 setContentView() 中来。

    public void setContentView(View view, ViewGroup.LayoutParams params) {
        if (mContentParent == null) {
            installDecor();
        } else {
            mContentParent.removeAllViews();
        mContentParent.addView(view, params);
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
    

    我们看看 mContentParent.addView(view, params) 的时候发生了什么。

    public void addView(View child, int index, LayoutParams params) {
        if (DBG) {
            System.out.println(this + " addView");
        // addViewInner() will call child.requestLayout() when setting the new LayoutParams
        // therefore, we call requestLayout() on ourselves before, so that the child's request
        // will be blocked at our level
        requestLayout();