Android自定义view之View的测量过程全解析

Android 应用层开发中绕不开自定义 View 这个话题,虽然现在 Github 上有形形色色的开源库供大家使用,

但是作为一名开发者而言,虽然不提倡重复造轮子,但是轮子都是造出来的。碰到一些新鲜的 UI 效果时,

如果现有的控件无法完成任务,那么我们就应该想到要自定义一个 View 了。 

我们知道,在 Android 中 View 绘制流程有测量、布局、绘制三个步骤,它们分别对应 3 个 API :onMeasure()、onLayout()、onDraw()。 

- 测量 onMeasure() :测量View的尺寸,决定View的大小

- 布局 onLayout() :通过设置l,t,r,b确定view在父容器中的位置

- 绘制 onDraw():通过canvas绘制我们想要展示的内容

没有办法说这三个阶段,哪个阶段最重要,只是相对而言,测量阶段对于开发者而言难度相对其它两个要大,处理的细节也要多得多,

自定义一个 View,正确的测量是第一步,正因为如此今天我将从源码的角度来学习View的测量过程.

View在本质上是一个Rect矩形的区域.

在Android中View的测量是从View树的根节点开始的,一步一步的往下测量而成的.

那么,首先我们来了解下View树的结构:

我们在Activity中一般通过setContentView()方法来设置我们的View视图:

public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

在setContentView()方法中,我们调用了getWindow().setContentView()方法,而这里的

getWindow()就是PhoneWindow,因此我们转到PhoneWindow的setContentView()方法:

public void setContentView(int layoutResID) {
          //如果顶层容器FrameLayout为空的话,需要从xml加载顶层容器
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            //将我们通过setContentView设置的布局资源加载到顶层容器mContentParent中
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

在PhoneWindow的setContentView()中会调用installDecor()方法(部分代码省略):

 private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            //生成DecorView
            mDecor = generateDecor(-1);
        } else {
            //将DecorView关联到PhoneWindow上
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
            //同时生成Activity的setContentView方法需要加载的View的顶层容器,并添加到DecorView中去
            mContentParent = generateLayout(mDecor);
            }
        }

分别调用generateDecor()方法生成DecorView:

protected DecorView generateDecor(int featureId) {
        //生成顶层的DecorView
        return new DecorView(context, featureId, this, getAttributes());
    }

同时我们会看到在installDecor()方法中,也会通过generateLayout方法生成mContentParent对象:

 protected ViewGroup generateLayout(DecorView decor) {
        
        int layoutResource;
        
        // Embedded, so no decoration is needed.
        layoutResource = R.layout.screen_simple;
        //在该方法中最终将mContentParent顶层view添加到DecorView中
        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
        //mContentParent顶层容器View的布局id为com.android.internal.R.id.content
        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        if (contentParent == null) {
            throw new RuntimeException("Window couldn't find content container view");
        }

        return contentParent;
    }

以上的流程中最终会将资源id为:R.layout.screen_simple的布局加载DecorView中:


    
    

可以看出我们的顶层布局DecorView中包含的是一个线性布局,包含两部分

ViewStub用于控制是否显示StatusBar或者TitleBar相关的显示

而id为content的FrameLayout则最终会加载我们的Activity通过setContentView()设置的布局资源

由此我们可以大致的画出Activity中view树的结构了:

在ActivityTherad中创建Activity,在Activity中创建PhoneWindow而PhoneWindow中包含了DecorView

因此Activity PhoneWindow  DecorView这三者相互关联实现Android view树的整体流程:

Android自定义view之View的测量过程全解析_第1张图片

可以看到,每一个Activity都包含了PhoneWindow,PhoneWindow又包含了一个DecorView,

而DecorView中包含了TitleView和一个ContentView,TitleView就是我们开发过程中需要设置

的ActionBar和StatusBar相关,而界面的主体结构都在这个ContentView中设置了,所以我们

每次给Activity设置布局文件的时候必须调用setContentVIew(int layoutResID)方法来设置。
而ContentView中就包含了一个或多个ViewGroup和View,关于ViewGroup和View,

整个ContentView的视图结构:最顶层是ViewGroup,ViewGroup下可能有多个ViewGroup和View,

就好像布局文件来说,我们写布局文件的时候,最外层一般都是一个ViewGroup(LinearLayout或者是ConstraintLayout等),

之后这个ViewGroup内部可能还是一个ViewGroup(LinearLayout或者是RelativeLayout等)或者直接放置了一个或者

多个View(如TextView或者是Button等)。

了解了Activity的view树的组成结构,接下来我们就要了解View的measure测量流程了:

在View的测量流程中,主要对应的就是onMeasure()方法,在该方法中有两个参数,分别是

widthMeasureSpec heightMeasureSpec因此我们学习View的测量measure过程中首先就要学习,MeasureSpec这个类:

 public static class MeasureSpec {
        
        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;

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

        @MeasureSpecMode
        public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }
        
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }
    }

MeasureSpec是View的一个静态内部类代表着测量的规格(包含mode和size),该类主要用于封装父View对于子View的布局要求:

而它的手段主要是通过一个32位的int类型的数值来实现的,我们知道一个int类型的数值有32位组成,MeasureSpec将它的高2位,

用来代表测量模式mode,低30位用来代表在该测量模式下具体的数值大小size,如下图所示:

Android自定义view之View的测量过程全解析_第2张图片

在MeasureSpec中分别有三个重要的变量和方法:

MeasureSpec.EXACTLY:

该模式表示,父容器已经检测出子view所需要的精确大小,在该模式下,子View的测量大小即为SpecSize

MeasureSpec.AT_MOST:

该模式表示,父容器未能检测出子view所需要的精确大小,但是指定了一个可用的大小即SpecSize,

在该模式下View的测量大小不能超过SpecSize

MeasureSpec.UNSPECIFIED:

该模式下父容器不对子view的大小做任何的限制,子view想要多大就多大

MeasureSpec.UNSPECIFIED这种模式一般用于我们的系统的内部,像我们的AdapterView,

ScrollView,ListView等,因此在以下的讨论中该模式不在我们的讨论范围内

而MeasureSpec的三个方法,则是获取和生成模式mode和size大小相关的:

getMode(measureSpec):根据测量规格获取测量的模式

getSize(measureSpec):根据测量规格获取测量的具体大小值

makeMeasureSpce(int mode,int size):根据指定的模式和大小生成一个指定的测量规格

在Android中由于View的树形结构,在测量时会从view树的顶端从上向下的依次进行遍历,完成对子view的测量

因此子View的测量是由父View发起的,并且子View的父view必须是一个容器,因为只有容器才有能力装载子

View因此我们可以判定,父view肯定是一个ViewGroup,由此我们可以从ViewGroup的源码来入手对view的测量流程

来一探究竟,由于ViewGroup继承自View并且ViewGroup并没有重写measure()和onMeasure()这两个方法,那么到底是怎么测量的呢?

我们可以从ViewGroup的子类中寻找答案,我们以我们不经常使用的AbsoluteLayout为例来进行讲解:

首先,我们应该知道绝对布局的布局特点是有以下的两个属性来决定的:

AbsoluteLayout_Layout_layout_x:控件相对于父容器左上角的left坐标
AbsoluteLayout_Layout_layout_y:控件相对于父容器左上角的top坐标

AbsoluteLayout中的子View通过设置自身的left top相对于父容器左上角的坐标位置来最终确定自身在容器中的位置

我们来看AbsoluteLayout的onMeasure()方法:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //获取容器中的子view的数量
        int count = getChildCount();
        
        int maxHeight = 0;  //定义容器的高度
        int maxWidth = 0;  //定义容器的宽度

        //首先通过measureChildren()方法来测量容器中的所有子view的宽高
        measureChildren(widthMeasureSpec, heightMeasureSpec);

        //接着遍历所有的子view,由于子view已经通过measureChildren()方法测量过一遍了
        //即子view已经有确定的测量大小了
        for (int i = 0; i < count; i++) {
            //依次获取指定的子view
            View child = getChildAt(i);
            //如果子view为GONE的话就跳过
            if (child.getVisibility() != GONE) {
                int childRight;
                int childBottom;
                //获取每个子View的LayoutParams
                AbsoluteLayout.LayoutParams lp = (AbsoluteLayout.LayoutParams) child.getLayoutParams();
                //通过设置的绝对坐标x,y 分别获取view的右边 和底部的位置
                childRight = lp.x + child.getMeasuredWidth();
                childBottom = lp.y + child.getMeasuredHeight();
            
                //获取容器的宽和高,容器的宽和高就是所有子view中(子view的x坐标 +  子view的宽度,子view的y坐标  +  子view的高度)的最大值
                maxWidth = Math.max(maxWidth, childRight);
                maxHeight = Math.max(maxHeight, childBottom);
            }
        }

       //分别在水平和垂直方向上将padding考虑进去
        maxWidth += mPaddingLeft + mPaddingRight;
        maxHeight += mPaddingTop + mPaddingBottom;

        //考虑layout_minWidth 和 layout_minHeight的值和我们计算出容器的宽高值,取最大值
        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
        //通过setMeasuredDimension将我们的值设置给容器自身,才算完成了一次完整的测量
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, 0),
                resolveSizeAndState(maxHeight, heightMeasureSpec, 0));
    }

通过以上对AbsoluteLayout的测量流程分析我们可以得出:

ViewGroup即容器的测量流程是先测量容器中的所有的子View,然后通过

测量好的子View的大小来计算自身的大小,最后通过setMeasureDimension()

将计算好的容器的大小值设置给自身才算完成了一次完整的测量

由于ViewGroup是一个容器,因此其不仅要测量自己还要测量其包含的子view

因此在ViewGroup中提供了几个测量子view的方法,用于子view的测量:

Android自定义view之View的测量过程全解析_第3张图片

在以上的三个方法中,measureChildren()是一次测量完所有的子view而measureChild()和measureChildWithMargins()则是只单独对某一个

子View进行测量,我们就挑选一个最复杂的measureChildWithMargins()来进行分析:

//注意方法的参数:
//child:父容器要进行测量的子View
//parentWidthMeasureSpec:父容器在width宽度方向上的测量规格MeasureSpce
//widthUsed:父容器在width方向上已经使用的宽度值  比如 其它子view已经使用的宽度 + 其它子view的left_marging + 其它子view的right_margin + 父容器的padding_left + 父容器的padding_right
//parentHeightMeasureSpec:父容器在height高度方向上的测量规格
//heightUsed:父容器在height高度方向上已经使用的高度空间 比如 其它子view已经使用的高度 + 其它子view的top_marging + 其它子view的bottom_margin + 父容器的padding_top + 父容器的padding_bottom
protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
    //获取要测量子View的LayoutParams
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
        
    //通过getChildMeasureSpec()方法获取要测量子view在width水平方向的宽度的测量规格childWidthMeasureSpec
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
    //通过getChildMeasureSpec()方法获取要测量子view在height垂直方向上的高度的测量规格childHeightMeasureSpec
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);
        //通过以上计算出要测量的子View在水平和垂直方向上的测量规格并调用要测量子view的measure()方法来对子view 进行测量
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

在以上调用measureChildWithMargins()方法测量子view的过程中主要有以下步骤:

1.获取要测量子view的LayoutParams

2.通过getChildMeasureSpec()方法传入父容器的测量宽高尺寸规格widthMeasureSpec / heightMeasureSpec 和子view的LayoutParams的width / height

生成要测量子view在width 和 height方向上的测量规格MeasureSpec

3.调用要测量子view的measure()方法并将计算出的子view的宽高测量规格传递给要测量子view的measure()方法

那么,子view的宽高测量规格到底具体是怎么生成的呢,我们需要对getChildMeasureSpec()方法一探究竟:

//注意方法参数:
//spec:要测量的子view的父容器在width 或者 height方向上的测量规格 即父容器的MeasureSpec
//padding:父容器在水平或者垂直方向上的已经占用的空间,比如mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed
//表示父容器的左右或者上下padding值 以及该控件的左右或者上下margin值,以及其它已经测量过的子view的宽高值,这些已经使用的空间不能纳入到要测量子view的MeasureSpec的计算中
//childDimension:通过子View的LayoutParams获取到的width height (即通过xml layout_width / layout_height 或者 代码设置的layoutparams.width layout.height设置的值)
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec); //获取父容器的测量模式 mode 
        int specSize = MeasureSpec.getSize(spec);  //获取父容器的测量大小 size
        
       //获取父容器的剩余可用空间大小
        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;
        //通过switch语句判断父容器的mode ,来生成要测量的子view的mode 和 size
        switch (specMode) {
       //父容器的specMode为EXACTLY时子view的mode 和 size的生成情况  即父容器的layout_width = 具体值 比如 122dp   或者  layout_width = match_parent
        case MeasureSpec.EXACTLY:
               //表示子的layout_width 为一个具体的值 比如:100dp  50dp 等
            if (childDimension >= 0) {
                //子view的size就是 childDimension 即子view设置的值
                resultSize = childDimension;
                //子view的mode为 EXACTLY
                resultMode = MeasureSpec.EXACTLY;
                //子View的layout_width为 match_parent
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                //子view的size为 父容器在width 或者height方向上的可用的空间大小
                resultSize = size;
                //子view的mode为,EXACTLY
                resultMode = MeasureSpec.EXACTLY;
                //子view的layout_width 为 wrap_content
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                //子view的size为 父容器在width 或者height 方向上的可用的空间大小
                resultSize = size;
                //子view的mode 为 AT_MOST
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

       //父容器的mode为AT_MOST的情况 即父容器的layout_width = wrap_content
        case MeasureSpec.AT_MOST:
                //子view的layout_width 为 具体值 比如 50dp  100dp ..等的情况
            if (childDimension >= 0) {
                //子view的size为子view自己设置的layout_width值
                resultSize = childDimension;
                //子view的mode为EXACTLY
                resultMode = MeasureSpec.EXACTLY;
                //子view的layout_width为match_parent的情况
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
               //子view的size为 父容器在width 或者 height 方向上可用的剩余空间 并且子view的size 不能超过父容器在width 或者 height 方向上可用的剩余空间
                resultSize = size;
                //子view的mode为AT_MOST
                resultMode = MeasureSpec.AT_MOST;
                //子view的layout_width = wrap_content
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                //子view的size为 父容器在width 或者 height 方向上可用的剩余空间 并且子view的size 不能超过父容器在width 或者 height 方向上可用的剩余空间
                resultSize = size;
                //子view的mode为AT_MOST
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        //父容器的mode为UNSPECIFIED
        case MeasureSpec.UNSPECIFIED:
                //子view的layout_width 为具体值 如layout_width = 100dp
            if (childDimension >= 0) {
                //子view的size为子view自己设置的值
                resultSize = childDimension;
                //子view的mode为EXACTLY
                resultMode = MeasureSpec.EXACTLY;
                //子view的layout_width 为match_parent
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                 //子View的size 为 父容器的可用剩余空间或者为0
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                //子view的mode 为 UNSPECIFIED
                resultMode = MeasureSpec.UNSPECIFIED;
                //子view的layout_width = wrap_content
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                //子view的size为父容器的可用剩余空间或者为0
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                //子view的mode为 UPSPECIFIED
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //通过MeasureSpec将测量计算出的size 和 mode组合成一个完整的MeasureSpec 返回
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

通过以上的getChildMeasureSpec()计算出一个要测量的子view的宽度或者高度的MeasureSpec有以下几个步骤:

1.获取父容器测量规格的mode 和 size

2.获取父容器在width 或者 height方向上的可用空间

3.通过switch语句判断父容器的MeasureSpec的mode来决定要测量子view的mode和size

由此我们知道子view的MeasuerSpec的生成是由其父容器的MeasureSpec和子view自身的LayoutParams来决定的

而至于这个决定的规则,我总结出了一个一下的表格:

Android自定义view之View的测量过程全解析_第4张图片

从以上总结的这张表中我们总结出了以下的这几个结论(从左往右看,同时UNSPECIFIED模式不在考虑范围内):

1.如果子View的width 或者height为具体值的时候比如 50dp那么此时不管父容器的specMode是什么子view的specMode一定是EXACTLY并且子view的width 或者height的specSize就是我们设置的具体值

2.当子View的size为MATCH_PARENT的时候,子view的测量模式specMode跟随父容器的specMoe,即父容器的specMode是什么子view的测量模式specMode和其相同,但是子view的测量大小分为以下两种情况:

        a.父容器的specMode为EXACTLY时,子view的size为父容器在width 或者height方向上可用的剩余空间

        b.父容器的specMode为AT_MOST时,子View的size不能超过父容器在width 或者height方向上可用剩余空间的大小

3.当子view的size为WRAP_CONTENT时,子View的测量模式specMode一定为AT_MOST,并且子View的测量大小size一定不能超过父容器在width或者height方向上的可用剩余空间大小.

你可能感兴趣的:(自定义View)