高级UI系列(一): 自定义UI理论篇

1,993 阅读20分钟

   自定义view是区分中级开发和初级开发的分水岭,虽说今年校招,工作三四年的老程序员一直在劝退客户端,作为职场打拼多年的老菜鸟,对android还是挺有信心的,虽说对view的知识也只是停留在纸上,很少真正落地做一些复杂高性能的ui控件,之前在akulaku确实见识了一群技术大牛,高级ui控件伸手就来,让我羡慕不已,这一次我也从基础到源码再到实战开始写几篇自定义view教程。大家有什么好的见解也欢迎到评论区多多交流。

一. 自定义UI的基本方法

  说到自定义view,我们就能联想到最基本的三个方法: onmeasure()onlayout()ondraw(); view能在activity显示出来。
  都要经历测量布局绘制三个步骤,而这三个步骤分别对应三个动作:measurelayoutdraw。那他们分别代表什么作用呢? 基本方法

  • 测量onmeasure()决定view的大小;
  • 布局onlayout()决定viewviewgroup中的位置;
  • 绘制ondraw()决定绘制这个view

二. 自定义UI分类

  按照ui继承属性来说,其实自定义view大概可分为两种,一种是自定义单view,一种是自定义viewgroup,自定义单view又根据重写方法不同分为,自绘view和继承view,他们的区别是:自绘view仅仅设置特定ui,不会涉及交互,继承view是基于系统特殊控件,保留原有功能,加以扩展。自定义viewgroup直接让组合控件成为一个新的父布局控件,这种方式难度有点大。 UI分类

  1. 自定义View

    • 只需要重写onmeasure()ondraw()
  2. 自定义viewgroup

    • 则只需要重写onmeasure()onlayout()

三. 自定义UI基础

3.1 view的分类

  刚刚说了,视图View主要分为两类,这边我再次总结一下:

类别解释特点
自定义单view即一个view,如textview不包含子view
自定义viewgroup即多个view组成的viewgroup,如LinearLayout包含子view

3.2 view类简介

  学习自定义view,我们首先得知道,它是干什么用的,为什么会有这个东西?然后了解他里面是如何实现的?这边先聊聊他为什么会在android里出现,总结原因有两个:

  • view类是android中各种组件的基类,如viewviewgroup基类
  • view表现为显示在屏幕上的各种视图

android中的ui组件都由viewviewgroup组成。

点开view源码,我们发现里面有四个关键的构造函数,下面我就详细讲解一下,每个构造方法所代表的现实意义吧。

View的构造函数

源码分析:

// 1. 如果View是在Java代码里面new的,则调用第一个构造函数
 public CarsonView(Context context) {
        super(context);
    }

//  2. 如果View是在.xml里声明的,则调用第二个构造函数
// 自定义属性是从AttributeSet参数传进来的
    public  CarsonView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

//  不会自动调用
// 3. 一般是在第二个构造函数里主动调用
//  如View有style属性时
    public  CarsonView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    // API21之后才使用
    // 不会自动调用
    // 4. 一般是在第二个构造函数里主动调用
    // 如View有style属性时
    public  CarsonView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

3.3 attributeset与自定义属性

  系统自带的View可以在xml中配置属性,对于写的好的自定义view同样可以在xml中配置属性,为了使自定义的view的属性可以在xml中配置,需要以下4个步骤:

  1. 通过<declare-styleable>为自定义view添加属性

  2. xml中为相应的属性声明属性值

  3. 在运行时(一般为构造函数)获取属性值

  4. 将获取到的属性值应用到view

自定义属性

3.4 view视图结构

  对于多view的视图,结构是树形结构:最顶层是viewgroupviewgroup下可能有多个viewgroupview,如下图:

ViewGroup结构图

  1. phonewindowandroid系统中最基本的窗口系统,继承自windows类,负责管理界面显示以及事件响应。它是activityview系统交互的接口

  2. decorviewphonewindow中的起始节点view,继承于view类,作为整个视图容器来使用。用于设置窗口属性。它本质上是一个framelayout

  3. viewrootactivtiy启动时创建,负责管理布局渲染窗口UI等等

  一定要记住: 无论是measure过程、layout过程还是draw过程,永远都是从view树的根节点开始测量或计算(即从树的顶端开始),一层一层、一个分支一个分支地进行(即树形递归),最终计算整个view树中各个view,最终确定整个view树的相关属性。

view的递归

3.5 android坐标系

android的坐标系定义为:

  • 屏幕的左上角为坐标原点
  • 向右为x轴增大方向
  • 向下为y轴增大方向

  ❌ 注意事项: 不要把他和的数学坐标系搞混了

数学坐标系

3.6 view位置(坐标)描述

  view的位置由「4个顶点」决定的「4个顶点的位置」描述分别由4个值决定:

请记住:view的位置是相对于父控件而言的

view位置参数

  1. Top:子view上边界到父view上边界的距离
  2. Left:子view左边界到父view左边界的距离
  3. Bottom:子view下边距到父view上边界的距离
  4. Right:子view右边界到父view左边界的距离

3.7 view位置获取方式

  view的位置是通过view.getxxx()函数进行获取:(以Top为例)

// 获取Top位置
public final int getTop() {

return mTop;

}

// 其余如下:
getLeft(); // 获取子View左上角距父View左侧的距离
getBottom(); // 获取子View右下角距父View顶部的距离
getRight(); // 获取子View右下角距父View左侧的距离

  与MotionEventget()getRaw()的区别

//get() :触摸点相对于其所在组件坐标系的坐标
event.getX();

event.getY();

//getRaw() :触摸点相对于屏幕默认坐标系的坐标
event.getRawX();

event.getRawY();

3.8 android中颜色相关内容

3.8.1 Android支持的颜色模式:

3.8.2 以ARGB8888为例介绍颜色定义:

四. view树的绘制流程

view树的绘制流程

4.1 view树的绘制流程是谁负责的?

  view树的绘制流程是通过viewroot去负责绘制的,viewroot这个类的命名有点坑,最初看到这个名字,翻译过来是view的根节点,
  但是事实完全不是这样,viewroot其实不是View的根节点,它连view节点都算不上,它的主要作用是View树的管理者,负责将decorviewphonewindow“组合”起来,
  而View树的根节点严格意义上来说只有decorview;每个decorview都有一个viewroot与之关联,这种关联关系是由windowmanager去进行管理的;

4.2 view的添加

  记住每添加一次view都会刷新一次ui,这里面可能出现卡顿问题,之前在做周年庆活动的时候往里flowlayout添加item,measure,layout, draw方法会被执行多次。

4.3 measure

  话说自定义ui执行的方法中:measurelayoutdraw,我们第一个接触方法就是measure了,今天来就说说measure执行过程。 那么问题来了:

  1. view为什么要有view过程 ?
    • 因为在androidview有自适应尺寸的机制,在用自适应尺寸来定义view大小的时候,view的真实尺寸还不能确定,这时候就需要根据view的宽高匹配规则,经过计算,得到具体的像素值,view过程就是干这件事
  2. measure过程都干了点什么事 ?
      由于上面提到的自适应尺寸的机制,所以在用自适应尺寸来定义view大小的时候,view的真实尺寸还不能确定。但是view尺寸最终需要映射到屏幕上的像素大小,所以measure过程就是干这件事,自适应各种尺寸值,

      经过计算,得到具体的像素值。measure过程会遍历整棵view树,然后依次测量每个view真实的尺寸。具体是每个viewgroup会向它内部的每个子view发送measure命令,然后由具体子viewonmeasure()来测量自己的尺寸。

      最后测量的结果保存在viewmmeasuredwidthmmeasuredheight中,保存的数据单位是像素。 measure执行流程
  3. 对于自适应的尺寸机制,如何合理的测量一棵view树 ?

      系统在遍历完布局文件后,针对布局文件,在内存中生成对应的view树结构,这个时候,整棵view树种的所有view对象,都还没有具体的尺寸,因为measure过程最终是要确定每个view打的准确尺寸,也就是准确的像素值。

      但是刚开始的时候,viewlayout_widthlayout_height两个属性的值,都只是自适应的尺寸,也就是match_parentwrap_content,这两个值在系统中为负数,所以系统不会把它们当成具体的尺寸值。

      所以当一个view需要把它内部的match_parent或者wrap_content转换成具体的像素值的时候,他需要知道两个信息。
    • a. 针对于match_parent,父布局当前具体像素值是多少,因为match_parent就是子View想要和父布局一样大。
    • b. 针对wrap_content,子View需要根据当前自己内部的content,算出一个合理的能包裹所有内容的最小值。但是如果这个最小值比当前父布局还大,那不行,父布局会告诉你,我只有这么大,你也不应该超过这个尺寸。
        也就是说,在measure过程中,ViewGroup会根据自己当前的状况,结合子View的尺寸数据,进行一个综合评定,然后把相关信息告诉子View,然后子View在onMeasure自己的时候,

      一边需要考虑到自己的content大小,一边还要考虑的父布局的限制信息,然后综合评定,测量出一个最优的结果
  4. 那么viewgroup是如何向子view传递限制信息的 ?

 谈到传递限制信息,那就是measurespec类了,该类贯穿于整个measure过程,用来传递父布局对子view尺寸测量的约束信息。简单来说,该类就保存两类数据。

  1. view当前所在父布局的具体尺寸。
  2. 父布局对子view的限制类型。
      那么限制类型又分为三种类型:
      1. UNSPECIFIED,不限定。
      • view想要多大,我就可以给你多大,你放心大胆的measure吧,不用管其他的。也不用管我传递给你的尺寸值。(其实Android高版本中推荐,只要是这个模式,尺寸设置为0)
      1. EXACTLY,精确的。
      • 根据我当前的状况,结合你指定的尺寸参数来考虑,你就应该是这个尺寸,具体大小在MeasureSpec的尺寸属性中,自己去查看吧,你也不要管你的content有多大了,就用这个尺寸吧。
      1. AT_MOST,最多的。
      • 根据我当前的情况,结合你指定的尺寸参数来考虑,在不超过我给你限定的尺寸的前提下,你测量一个恰好能包裹你内容的尺寸就可以了。
  3. scrollview嵌套listview问题 ?
     只存在滑动卡顿这一问题,可以采用如下两种简单方式快速解决,利用RecyclerView内部方法
rV.setHasFixedSize(true);
rV.setNestedScrollingEnabled(false);

4.4 layout

    1. 系统为什么要有layout过程?
    • view框架在经过第一步的measure过程后,成功计算了每一个view的尺寸。但是要成功的把view绘制到屏幕上,只有view的尺寸还不行,还需要准确的知道该view应该被绘制到什么位置。除此之外,对一个viewgroup而言,还需要根据自己特定的layout规则,来正确的计算出子view的绘制位置,已达到正确的layout目的。这也就是layout过程的职责。
    • 该位置是view相对于父布局坐标系的相对位置,而不是以屏幕坐标系为准的绝对位置。这样更容易保持树型结构的递归性和内部自治性。而view的位置,可以无限大,超出当前viewgroup的可视范围,这也是通过改变view位置而实现滑动效果的原理。
    1. layout过程都干了点什么事?!
    • 由于view是以树结构进行存储,所以典型的数据操作就是递归操作,所以,view框架中,采用了内部自治的layout过程。

    • 每个叶子节点根据父节点传递过来的位置信息,设置自己的位置数据,每个非叶子节点,除了负责根据父节点传递过来的位置信息,设置自己的位置数据外(如果有父节点的话),还需要根据自己内部的layout规则(比如垂直排布等),计算出每一个子节点的位置信息,然后向子节点传递layout过程。

    • 对于viewgroup,除了根据自己的parent传递的位置信息,来设置自己的位置之外,还需要根据自己的layout规则,为每一个子view计算出准确的位置(相对于子view的父布局的位置)。

    • 对于view,根据自己的parent传递的位置信息,来设置自己的位置。view对象的位置信息,在内部是以4个成员变量的保存的,分别是mLeftmRightmTopmBottom。他们的含义如图所示

4.5 draw

    1. 系统为什么要有draw过程?
    • View框架在经过了measure过程和layout过程之后,就已经确定了每一个view的尺寸和位置。那么接下来,也是一个重要的过程,就是draw过程,draw过程是用来绘制view的过程

      它的作用就是使用graphic框架提供的各种绘制功能,绘制出当前view想要的样子。
    1. draw过程都干了点什么事?
    • View框架中,draw过程主要是绘制View的外观。ViewGroup除了负责绘制自己之外,还需要负责绘制所有的子View。而不含子ViewView对象,就负责绘制自己就可以了。

    • draw过程的主要流程如下:

      1. 绘制 backgroud(drawBackground)

      2. 如果需要的话,保存canvaslayer,来准备fading(不是必要的步骤)

      3. 绘制viewcontentonDraw方法)

      4. 绘制childrendispatchDraw方法)

      5. 如果需要的话,绘制fading edges,然后还原layer(不是必要的步骤)

      6. 绘制装饰器、比如scrollBaronDrawForeground

draw过程

五. layoutparams

5.1 marginlayoutparams

  MarginLayoutParams是和外间距有关的。事实也确实如此,和LayoutParams相比,MarginLayoutParams只是增加了对上下左右外间距的支持。
  实际上大部分LayoutParams的实现类都是继承自MarginLayoutParams,因为基本所有的父容器都是支持子View设置外间距的

  • 属性优先级问题
       MarginLayoutParams主要就是增加了上下左右4种外间距。在构造方法中,先是获取了margin属性;如果该值不合法,就获取horizontalMargin
      如果该值不合法,再去获取leftMarginrightMargin属性(verticalMargintopMarginbottomMargin同理)。我们可以据此总结出这几种属性的优先级

margin > horizontalMargin和verticalMargin > leftMargin和RightMargin、topMargin和bottomMargin

  • 属性覆盖问题
      优先级更高的属性会覆盖掉优先级较低的属性。此外,还要注意一下这几种属性上的注释

Call {@link ViewGroup#setLayoutParams(LayoutParams)} after reassigning a new value

5.2 layoutparamsview如何建立联系

  • 在XML中定义View
  • 在Java代码中直接生成View对应的实例对象

5.3 addview

/**
 * 重载方法1:添加一个子View
 * 如果这个子View还没有LayoutParams,就为子View设置当前ViewGroup默认的LayoutParams
 */
public void addView(View child) {
    addView(child, -1);
}

/**
 * 重载方法2:在指定位置添加一个子View
 * 如果这个子View还没有LayoutParams,就为子View设置当前ViewGroup默认的LayoutParams
 * @param index View将在ViewGroup中被添加的位置(-1代表添加到末尾)
 */
public void addView(View child, int index) {
    if (child == null) {
        throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
    }
    LayoutParams params = child.getLayoutParams();
    if (params == null) {
        params = generateDefaultLayoutParams();// 生成当前ViewGroup默认的LayoutParams
        if (params == null) {
            throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
        }
    }
    addView(child, index, params);
}

/**
 * 重载方法3:添加一个子View
 * 使用当前ViewGroup默认的LayoutParams,并以传入参数作为LayoutParams的width和height
 */
public void addView(View child, int width, int height) {
    final LayoutParams params = generateDefaultLayoutParams();  // 生成当前ViewGroup默认的LayoutParams
    params.width = width;
    params.height = height;
    addView(child, -1, params);
}

/**
 * 重载方法4:添加一个子View,并使用传入的LayoutParams
 */
@Override
public void addView(View child, LayoutParams params) {
    addView(child, -1, params);
}

/**
 * 重载方法4:在指定位置添加一个子View,并使用传入的LayoutParams
 */
public void addView(View child, int index, LayoutParams params) {
    if (child == null) {
        throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
    }

    // 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();
    invalidate(true);
    addViewInner(child, index, params, false);
}

private void addViewInner(View child, int index, LayoutParams params,
        boolean preventRequestLayout) {
    .....
    if (mTransition != null) {
        mTransition.addChild(this, child);
    }

    if (!checkLayoutParams(params)) { // ① 检查传入的LayoutParams是否合法
        params = generateLayoutParams(params); // 如果传入的LayoutParams不合法,将进行转化操作
    }

    if (preventRequestLayout) { // ② 是否需要阻止重新执行布局流程
        child.mLayoutParams = params; // 这不会引起子View重新布局(onMeasure->onLayout->onDraw)
    } else {
        child.setLayoutParams(params); // 这会引起子View重新布局(onMeasure->onLayout->onDraw)
    }

    if (index < 0) {
        index = mChildrenCount;
    }

    addInArray(child, index);

    // tell our children
    if (preventRequestLayout) {
        child.assignParent(this);
    } else {
        child.mParent = this;
    }
    .....
}

5.4 自定义layoutparams

  • 1. 创建自定义属性
<resources>
    <declare-styleable name="xxxViewGroup_Layout">
        <!-- 自定义的属性 -->
        <attr name="layout_simple_attr" format="integer"/>
        <!-- 使用系统预置的属性 -->
        <attr name="android:layout_gravity"/>
    </declare-styleable>
</resources>
  • 2. 继承MarginLayout
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
    public int simpleAttr;
    public int gravity;

    public LayoutParams(Context c, AttributeSet attrs) {
        super(c, attrs);
        // 解析布局属性
        TypedArray typedArray = c.obtainStyledAttributes(attrs, R.styleable.SimpleViewGroup_Layout);
        simpleAttr = typedArray.getInteger(R.styleable.SimpleViewGroup_Layout_layout_simple_attr, 0);
        gravity=typedArray.getInteger(R.styleable.SimpleViewGroup_Layout_android_layout_gravity, -1);

        typedArray.recycle();//释放资源
    }

    public LayoutParams(int width, int height) {
        super(width, height);
    }

    public LayoutParams(MarginLayoutParams source) {
        super(source);
    }

    public LayoutParams(ViewGroup.LayoutParams source) {
        super(source);
    }
}
  • 3. 重写ViewGroup中几个与LayoutParams相关的方法
// 检查LayoutParams是否合法
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 
    return p instanceof SimpleViewGroup.LayoutParams;
}

// 生成默认的LayoutParams
@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() { 
    return new SimpleViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
}

// 对传入的LayoutParams进行转化
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 
    return new SimpleViewGroup.LayoutParams(p);
}

// 对传入的LayoutParams进行转化
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { 
    return new SimpleViewGroup.LayoutParams(getContext(), attrs);
}

  

5.5 layoutParams常见的子类

  在为View设置LayoutParams的时候需要根据它的父容器选择对应的LayoutParams,否则结果可能与预期不一致,这里简单罗列一些常见的LayoutParams子类:

  • ViewGroup.MarginLayoutParams
  • FrameLayout.LayoutParams
  • LinearLayout.LayoutParams
  • RelativeLayout.LayoutParams
  • RecyclerView.LayoutParams
  • GridLayoutManager.LayoutParams
  • StaggeredGridLayoutManager.LayoutParams
  • ViewPager.LayoutParams
  • WindowManager.LayoutParams

六. measurespec

6.1 measurespec定义


  测量规格,封装了父容器对 view 的布局上的限制,内部提供了宽高的信息( SpecModeSpecSize ),SpecSize是指在某种SpecMode下的参考尺寸,其中SpecMode 有如下三种:

  • UNSPECIFIED
      父控件不对你有任何限制,你想要多大给你多大,想上天就上天。这种情况一般用于系统内部,表示一种测量状态。(这个模式主要用于系统内部多次Measure的情形,并不是真的说你想要多大最后就真有多大)
  • EXACTLY
      父控件已经知道你所需的精确大小,你的最终大小应该就是这么大。
  • AT_MOST
      你的大小不能大于父控件给你指定的size,但具体是多少,得看你自己的实现。

  

6.2 measurespec 的意义

  通过将 SpecModeSpecSize 打包成一个 int 值可以避免过多的对象内存分配,为了方便操作,其提供了打包 / 解包方法

6.3 measurespec值的确定

MeasureSpec值到底是如何计算得来的呢?

  measurespeViewMeasureSpec值是根据子View的布局参数(LayoutParams)和父容器的MeasureSpec值计算得来的,具体计算逻辑封装在getChildMeasureSpec()里

  /**
     *
     * 目标是将父控件的测量规格和child view的布局参数LayoutParams相结合,得到一个
     * 最可能符合条件的child view的测量规格。  

     * @param spec 父控件的测量规格
     * @param padding 父控件里已经占用的大小
     * @param childDimension child view布局LayoutParams里的尺寸
     * @return child view 的测量规格
     */
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec); //父控件的测量模式
        int specSize = MeasureSpec.getSize(spec); //父控件的测量大小

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // 当父控件的测量模式 是 精确模式,也就是有精确的尺寸了
        case MeasureSpec.EXACTLY:
            //如果child的布局参数有固定值,比如"layout_width" = "100dp"
            //那么显然child的测量规格也可以确定下来了,测量大小就是100dp,测量模式也是EXACTLY
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } 

            //如果child的布局参数是"match_parent",也就是想要占满父控件
            //而此时父控件是精确模式,也就是能确定自己的尺寸了,那child也能确定自己大小了
            else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            }
            //如果child的布局参数是"wrap_content",也就是想要根据自己的逻辑决定自己大小,
            //比如TextView根据设置的字符串大小来决定自己的大小
            //那就自己决定呗,不过你的大小肯定不能大于父控件的大小嘛
            //所以测量模式就是AT_MOST,测量大小就是父控件的size
            else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // 当父控件的测量模式 是 最大模式,也就是说父控件自己还不知道自己的尺寸,但是大小不能超过size
        case MeasureSpec.AT_MOST:
            //同样的,既然child能确定自己大小,尽管父控件自己还不知道自己大小,也优先满足孩子的需求
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } 
            //child想要和父控件一样大,但父控件自己也不确定自己大小,所以child也无法确定自己大小
            //但同样的,child的尺寸上限也是父控件的尺寸上限size
            else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            //child想要根据自己逻辑决定大小,那就自己决定呗
            else if (childDimension == LayoutParams.WRAP_CONTENT) {
                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 = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

针对下表,这里再做一下具体的说明

  • 对于应用层 View ,其 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 来共同决定 对于不同的父容器和view本身不同的LayoutParamsview就可以有多种MeasureSpec
    • view采用固定宽高的时候,不管父容器的MeasureSpec是什么
      • viewMeasureSpec都是精确模式并且其大小遵循Layoutparams中的大小;
    • view的宽高是match_parent时,这个时候如果父容器的模式是精准模式,
      • 那么view也是精准模式并且其大小是父容器的剩余空间,如果父容器是最大模式,
      • 那么view也是最大模式并且其大小不会超过父容器的剩余空间;
    • view的宽高是wrap_content时,不管父容器的模式是精准还是最大化
      • view的模式总是最大化并且大小不能超过父容器的剩余空间。
    • Unspecified模式,这个模式主要用于系统内部多次measure的情况下
      • 一般来说,我们不需要关注此模式 (这里注意自定义View放到ScrollView的情况 需要处理)

七. 自定义UI原因

    1. android系统内置view无法实现我们的需求
    1. 处于性能考虑

你的 点赞、评论、收藏、转发,是对我的巨大鼓励!