「测量流程」到底是怎么一回事?真的需要设计的这么复杂么?

人云亦云的「MeasureSpec」

说到Android View的测量流程,绝对都绕不开MeasureSpec,相信大部分人都能说上一两句:32位整形,高两位表示测量模式SpecMode ,分为EXACTLYAT_MOSTUNSPECIFIED三种,低30位表示对应的大小 SpecSize ...父View通过自身的测量模式,结合子View的LayoutParams确定其子View的测量模式,像上图一样,父View测量模式为 EXACTLY 、子View的 LayoutParams 的宽(或高)为具体的 dp/px 时,子View的测量模式为 EXACTLY ,大小为 childSize;如果父View的测量模式为AT_MOST...

其实这块的逻辑并不复杂,MeasureSpec这个类总共也就一百多行的代码,但很多人对这部分的就是存在困惑:

  • 三种测量模式的应用场景是什么?
  • EXACTLYAT_MOST还好理解,UNISPECIFIED可真让人一头雾水了。
  • 不就是测量个 View 大小么,还专门整个 MeasureSpec 类出来,直接传递大小不行么?过度设计,绝对是过度设计!

网上各种各样的文章对MeasureSpec的说法也众说纷纭,但差别不大,对于SpecMode,大家普遍保持以下观点:

模式 含义 应用场景
EXACTLY 父View已确定子View 的确切大小,子View的大小为父View测量所得的值 具体数值
AT_MOST 父View 指定一个子View可用的最大尺寸值,View大小 不能超过该值。 wrap_content
UNSPECIFIED 父View不对子View 施加任何约束,子View可以是它想要的任何尺寸 系统内部使用

其实上面的两个表格 并没有什么大问题,从某种意义上讲也是非常清晰的,一目了然。问题在于如果你之前对MeasureSpec知之甚少,很容易囿于前人定死的条条框框,忽略了这块代码设计的初衷,陷入囫囵吞枣甚至只会死记硬背的境地,一旦实际业务开发需要使用时,仍是两手一摊,大脑空白,大呼:

这也是很多人学习新知识时「能看懂,但过一段时间就忘了」的根本原因。

今天我将带着我的理解,站在产品设计的角度,尝试从「零」开始,设计出一套合理的测量模式,或许能让你对View测量流程有一个全新的理解。

tips:接下来的实现基于Android 源码中 View 体系的测量流程,为方便理解,可能会酌情减少一些代码,请放心食用。

开工造轮子

既然觉得Android源码中 MeasureSpec 那一套流程太复杂,那我们先尝试一下轻装上阵,假定View在构建时就直接由开发者制定对应的宽高,说干就干。

v0.1:view的宽高构造时确定

kotlin 复制代码
open class MView(width: Int, height: Int) {
​
}
​
class MViewGroup(width: Int, height: Int): MView(width, height) {
​
    private val childrenView: ArrayList<MView> = ArrayList()
​
    /**
     * 添加子视图
     */
    fun addView(view: MView) {
        childrenView.add(view)
    }
​
  /**
     * 获取所有子View
     */
    fun getChildren(): List<MView> {
        // 伪代码,此处直接返回空,不影响测量流程的实现
        return emptyList()
    }
  
}
  • MView的构造方法要求传入width、height
  • MViewGroup 继承自 MView,比MView多了一个addView方法以及一个getChildren方法,第一个用来添加子View,另一个用来获取所有的子view。

这也很符合直觉,父母在为人父母前也曾经是孩子,至于为什么不反过来让MView 继承自 MViewGroup ------ 因为MView并不需要处理子View相关逻辑。

通过让 ViewGroup 继承自 View,Android 整个View体系在整个视图层次结构方面保持了高度的一致性,同时这个设计提供了很强的灵活性和代码重用性,使得开发者能够更轻松地构建复杂的用户界面,并实现定制化的布局和交互效果。

v0.2:剥离出测量方法

很快我们就能发现v0.1版本的缺陷:实际的业务开发过程中,View的宽高往往并不是构造View时就能确定的 ------ View的宽高往往随着View内容的变化而变化。

拿 显示文本内容的TextView 举例,其文本内容往往并不是构造 TextView 时就能固定不变的,所以其宽、高也不是一成不变。再比如 MViewGroup,每次添加 子View 后,都会影响到其自身的宽高,在这样的需求背景下,宽、高在 View 构建时直接传入显然就无法满足了,所以需要我们对其加以改造:

kotlin 复制代码
open class MView {
​
    private var width = 0
    private var height = 0
​
    open fun setDimension(width: Int, height: Int) {
        this.width = width
        this.height = height
    }
​
    /**
     * 获取宽度
     */
    public fun getWidth() = width
    /**
     * 获取高度
     */
    public fun getHeight() = height
}
​
class MViewGroup: MView() {
​
  // 省略不相关代码...
​
    override fun setDimension(width: Int, height: Int) {
        super.setDimension(width, height)
    }
​
    // 继续省略不相关代码...
}

对比v0.1版本的代码:

  • 将View的宽高属性从构造方法中剥离,通过对应的setter/getter方法更灵活的设置宽高;
  • 调用setDimension()即意味着View的宽高已确定。

经过改造后的代码,宽高的设置更灵活,开发者只需在 可能影响View宽高的属性发生变化时 ,调用setDimension更新宽高即可。

这段代码其实还可以再优化下,按照现在的测量流程,开发者需要在可能影响View宽高的属性发生变化后 ,重新计算出变化后的宽高, 再调用setDimension更新对应View的宽高 ------ 这是一个相当高频的操作,如果不对 "重新计算出变化后的宽高" 这一行为进行收敛,相似代码可能会在项目里满天飞。基于提升代码的内聚性关注点分离的设计原则考虑,我们需要抽象出测量方法内置于MView类内:

kotlin 复制代码
open class MView {
​
    private var measuredWidth = 0
    private var measuredHeight = 0
​
    /**
     * 执行测量流程,根据View自身特性,测量大小,最终调用{@link #setMeasuredDimension(int, int)}保存测量结果
     */
    open fun measure() {
        // 根据View自身内容大小计算宽高
        val width = ...
        val height = ...
        // 保存测量结果
        setMeasuredDimension(width, height)
    }
​
    open fun setMeasuredDimension(measuredWidth: Int, measuredHeight: Int) {
        this.measuredWidth = measuredWidth
        this.measuredHeight = measuredHeight
    }
​
    /**
     * 获取测量后的宽度
     */
    public fun getMeasuredWidth() = measuredWidth
    /**
     * 获取测量后的高度
     */
    public fun getMeasuredHeight() = measuredHeight
}
​
class MViewGroup: MView() {
​
    // 省略不相关代码...
​
    /**
     * 添加子视图,
     */
    fun addView(view: MView) {
        childrenView.add(view)
    // 添加完子View后,自身的宽高可能会受其影响产生变化
    // 调用measure()方法重新进行测量流程,确定变化后的宽高
    measure()
    }
​
  // 继续省略...
}

经过改造后

  • MView增加measure()方法,负责具体的测量流程;
  • 当View的宽高可能发生变化时,调用measure()方法触发View的测量流程:例如调用ViewGroup的addView()方法添加子View后,调用measure方法触发ViewGroup的测量流程。

v0.3:既当爹又当妈的MViewGroup

上面两个版本,我们迭代出了一套针对单个View的测量过程,实际的业务开发中,View(or ViewGroup)大多数都是被添加在其他ViewGroup中,针对这种成体系的View,测量流程要怎么用代码描述? 思考:

  • 测量流程的发起方是谁?

一定是从外层的ViewGroup发起的,因为子View被包裹于ViewGroup中------它是没办法与外界直接交互的。

  • ViewGroup的大小需受到子View大小的影响

为了保证包裹其内的子View完整、合理的展示,ViewGroup需要先测量其子View,待子View完成测量后,ViewGroup根据子View大小决定自己的大小

父母之爱子,则为之计深远。

所以我们为MViewGroup添点料:

kotlin 复制代码
class MViewGroup: MView() {
​
  /**
     * Android ViewGroup源码中,并未对measure()相关方法进行重写
   * 因为不同的ViewGroup的布局特性不同,需要具体的布局容器才能根据子View的宽高得到自身合理的宽高
   * 此处为了方便理解,使用伪代码大致体现ViewGroup的测量流程
     */
    override fun measure() {
    // 主动测量子View
    measureChildren()
        // 拿到子View测量后的宽高
        val childWidth = ...
        val childHeight = ...
        // 结合自身的布局特性以及子view的宽高得到自身的宽高
    val measuredWidth = ...
    val measuredHeight = ...
        setMeasuredDimension(measuredWidth, measuredHeight)
    }
​
    // 省略不相关代码...
​
    /**
     * 测量子View
     */
    fun measureChildren() {
        val childrenView = getChildren()
        for (child in childrenView) {
            child.measure()
        }
    }
​
    // 继续省略...
}

对比之前的代码:

  • 新增measureChildren()方法,用来发起对子view的测量工作
  • MViewGroup的measure()方法中会先对子view进行测量,然后结合自身的布局特性以及子view的宽高得到MViewGroup自身的宽高。

tips:Android ViewGroup源码中,并未对measure()相关方法进行重写,这也很好理解 ------ 不同的ViewGroup的布局特性不同,ViewGroup根本无法统一这种特性,需要具体的布局容器才能根据子View的宽高得到自身合理的宽高。此处为了方便读者理解,使用伪代码大致体现ViewGroup的测量流程。

整个测量流程大概是这样的:

v0.4:内容大小?控件大小?剪不断理还乱

kotlin 复制代码
open class MView {
​
  // 省略不相关代码...
​
    /**
     * 执行测量流程,根据View自身特性,测量大小,最终调用{@link #setMeasuredDimension(int, int)}保存测量结果
     */
    open fun measure() {
        // 根据View自身内容大小计算宽高
        val width = ...
        val height = ...
        // 保存测量结果
        setMeasuredDimension(width, height)
    }
​
  // 省略不相关代码...
}

回过头来看我们对MView的处理,里面有一句描述:根据View自身内容大小计算宽高,这句话意味着,我们的view大小总是等于自身内容大小。看到这,有人就又有点懵逼了,什么是view大小?什么是内容大小?这俩还有什么不一样的么?view大小难道不应该和内容大小一样么?

请看下图:

TextVIew大小我们称之为view大小(控件大小),即图上灰色区域; "这是文本内容的大小"所占据的空间大小,我们称之为内容大小,即图中紫色区域。

这里TextView的内容大小与TextView控件本身的大小并不相等,实际业务开发中也很常见,许多app的登录按钮大体都是这样的展示逻辑,例如: 这也是UI设计上一种常用的布局方式,用以突出重要操作的按钮。按照我们之前设计的测量流程,显然满足不了这样的布局要求。所以,我们需要一套机制用来表示控件基于布局要求之下的宽、高

不仅如此,每一个 ViewGroup 子类都有可能需要不同的布局参数来描述其子视图应该如何布局。 例如:

  • 线性布局LinearLayout需要会根据知道其子视图的权重(weight)进行子View得测量布局;
  • 相对布局RelativeLayout需要知道子视图相对于彼此的位置来进行测量布局;

而这正是我们上面测量流程缺少的。

所以,我们需要一套用来表示基于布局考虑的并且决能定最终view大小的参数,对于MViewGroup来说,其并不包含具体的布局逻辑,所以只有宽、高两个参数:

kotlin 复制代码
open class MView() {
​
  // 省略不相关代码...
  
    private var layoutParams: MViewGroup.MLayoutParams? = null
    
  fun getLayoutParams(): MViewGroup.MLayoutParams? = layoutParams
    
  fun setLayoutParams(params: MViewGroup.MLayoutParams?) {
        this.layoutParams = params
        // 设置LayoutParams后,需重新测量View
        measure()
    }
​
    open fun measure() {
        // 根据layoutParams和View自身内容大小计算宽高
    // 如果layoutParams 宽/高 为WRAP_CONTENT,View的宽高即为内容宽高
    // 如果layoutParams 宽/高 为MATCH_PARENT,View的宽高即为父View的宽/高
    // 如果layoutParams 宽/高 为 >= 0 的准确数字,那View得宽高即为这个准确的数字
        val width = ...
        val height = ...
        // 保存测量结果
        setMeasuredDimension(width, height)
    }
​
  // 继续省略...
    
}
​
class MViewGroup(): MView() {
​
  // 省略不相关代码...
​
    fun addView(view: MView) {
        childrenView.add(view)
    // 确保view有对应的LayoutParams,没有的话给个默认值
        val layoutParams = view.getLayoutParams()
        if (layoutParams == null) {
            view.setLayoutParams(generateDefaultLayoutParams())
        }
        measure()
    }
    
    open fun generateDefaultLayoutParams(): MLayoutParams {
        return MLayoutParams(MLayoutParams.WRAP_CONTENT, MLayoutParams.WRAP_CONTENT)
    }
  
    class MLayoutParams(width: Int, height: Int) {
​
        companion object {
      public const val MATCH_PARENT = -1
            public const val WRAP_CONTENT = -2
        }
    }
  // 继续省略...
}

由于是基于布局原因考虑的,所以我们将其命名为MLayoutParams

  • MLayoutParams包含两个基础属性,width 和 height;
  • MLayoutParams中的width和height包含了两个特殊的值:WRAP_CONTENTMATCH_PARENT,这也是日常业务开发中经常使用的两个值;
  • WRAP_CONTENT表示View想要足够大,以适应它自己的内容,MATCH_PARENT表示View想要和父View一样大;
  • MView新增了layoutParams属性,同时新增其getter/setter方法,调用setLayoutParams()后会调用measure()方法触发测量流程;
  • MViewGroup 在addView时会校验 子View 的MLayoutParams属性,如果为空会给默认值。

为什么Android源码中将 LayoutParamas 作为 ViewGroup 的内部类而不是View的内部类?其作为View必不可少的属性,作为View得内部类不是更合理么?

LayoutParams 是用来指定视图(View)在其父视图(ViewGroup)中的布局参数的。每一个 ViewGroup 子类都有可能需要不同的布局参数来描述其子视图应该如何布局。 例如,线性布局LinearLayout需要知道其子视图的权重(weight),而相对布局RelativeLayout需要知道子视图相对于彼此的位置。LayoutParams 作为 ViewGroup 的内部类,就是因为它们是针对父 ViewGroup 的布局行为而设计的。这种设计允许每种类型的 ViewGroup 定义自己的布局参数类,这些类继承自 ViewGroup.LayoutParams。这样,每个 ViewGroup 都可以添加它需要的特定布局信息,而不会影响到其他类型的 ViewGroup。

如果 LayoutParams 是 View 的内部类,那么每个 View 都会包含所有不同 ViewGroup 所需的布局参数,这显然违反了封装的原则,本末倒置了。 因此,LayoutParams 作为 ViewGroup 的内部类,是为了确保布局参数与使用它们的具体 ViewGroup 类型紧密相关,而不是与 View 本身相关,这样可以保持类结构的清晰和逻辑性。

至此,我们的测量流程已完成了大半:

  • MViewGroup 继承自 MView,保持视图结构层次的统一性,最大限度的复用代码逻辑。
  • MView通过measure()方法测量自己大小,并调用setMeasuredDimension(measuredWidth, measuredHeight)方法完成测量,确定自身大小。MViewGroup 则需要先帮助 其子View完成测量后,再进行自身的测量流程。
  • 抽像出MLayoutParams,包含两种特殊值:WRAP_CONTENTMATCH_PARENT

再回过头来看我们所设计的MView的测量流程:

kotlin 复制代码
open class MView() {
​
  // 省略不相关代码...
​
    open fun measure() {
        // 根据layoutParams和View自身内容大小计算宽高
    // 如果layoutParams 宽/高 为WRAP_CONTENT,View的宽高即为内容宽高
    // 如果layoutParams 宽/高 为MATCH_PARENT,View的宽高即为父View的宽/高
    // 如果layoutParams 宽/高 为 >= 0 的准确数字,那View得宽高即为这个准确的数字
        val width = ...
        val height = ...
        // 保存测量结果
        setMeasuredDimension(width, height)
    }
​
  // 继续省略...
    
}

我们能发现,MView的大小存在三种情况:

  • MLayoutParams 宽/高 为 >= 0 的准确数字,那View得宽高即为这个准确的数字
  • MLayoutParams 宽/高 为MATCH_PARENT,View的宽高即为父View的宽/高
  • MLayoutParams 宽/高 为WRAP_CONTENT,View的宽高即为内容宽高

第一种情况没什么好讲的,我们来仔细思考下其余的两种场景:

  1. MATCH_PARENT

要实现MATCH_PARENT的效果,子View就需要知道父View的宽/高。问题是,在我们实现的测量流程中,子View无法知道父View的宽/高。

  1. WRAP_CONTENT

WRAP_CONTENT 的效果是 View的宽高即为内容宽高,看起来没什么大问题,仔细想想,不对,有问题。 想象一下这样的场景,MViewGroup的宽度是 固定的值,例如100dp,其子View的 MLayoutParams 的宽度设定为 WRAP_CONTENT,内容宽度为 200dp,按照我们现有的实现,子View的最终宽度即为200dp,这显然是有问题的。举个不恰当的例子:王思聪问王健林要一辆劳斯莱斯,他老爸啪唧给他买了;你羡慕坏了,也问你老爸要劳斯莱斯,你老爸啪唧给你一个大逼斗子。

扯远了,没错,按我们目前的设计,子View目前明显缺乏来自其父View的指导和约束。

所以,我们需要让 子view 受到 父view 的约束,同时让 子view 能够获取到父view 的宽高。

v0.5:为MView添加约束

明确我们的期望:

  • 子View 能够获取 父View 的宽/高
  • 父View 能够基于自身的大小约束 子View

第一点很简单,回想下我们的测量流程,子View 的measure()方法由父VIew调用,我们只需在 父View 调用 子View 的measure方法时,将自己的宽高传递过去。 第二点需要动动脑筋思考了,子View应该受到父View的哪些约束? 分析一下:

  1. 父View的大小能够确定

    • 子View的宽高设置为 具体的数值 或者 MATCH_PARENT ,此时父View能够确定子View的确切大小,子View必须按照这种精确模式进行自己的测量工作;
    • 子View的宽高设置为WRAP_CONTENT ,此时父View无法确定子View的大小,但是可以将自己的大小传递给子View,子View的大小最大不能超过父View的大小,也就是子View在测量过程中尽可能的大
  2. 父View的大小暂无法确定

    • 子View的宽高设置为 具体的数值,那子View就按照这种精确模式进行测量;
    • 子View的宽高设置为MATCH_PARENT ,子View的大小想要跟随父View的大小,但是父View的大小此时不确定,所以将自己从父View处受到的约束的最大的大小继续传递给自己的子View,子View可以在这个范围内尽可能的大
    • 子View的宽高设置为WRAP_CONTENT ,子View的大小想要包裹住自己的内容,但是父View的大小此时不确定,所以将自己从父View处受到的约束的最大的大小继续传递给自己的子View,子View可以在这个范围内尽可能的大

结合以上分析,我们修改一下measure()方法,使 MView 的测量流程能够受到自己 父View MViewGroup 的指导和约束。

kotlin 复制代码
open class MView() {
​
  // 省略不相关代码...
  
    open fun measure(widthMeasureSpec: MeasureSpec, heightMeasureSpec: MeasureSpec) {
​
        // 根据widthMeasureSpec和heightMeasureSpec进行测量,计算出合适的宽/高
        // 以宽度为例
        // 1.获取宽度的测量模式和测量大小
        val widthSpecMode = widthMeasureSpec.mode
        val widthSpecSize = widthMeasureSpec.size
        // 2.根据widthSpecMode、widthSpecSize以及View自身的内容宽度计算最终的宽度,两种情况:
        //  - specMode 为 AT_MOST, MView 的宽高取 内容宽度 和 最小值,即MView尽可能的宽,但不能超出widthSpecSize
        //  - specMode 为 EXACTLY,MView 的宽直接取 widthSpecSize
        val width = ...
        val height = ..
        // 保存测量结果
        setMeasuredDimension(width, height)
    }
​
  /**
   * 测量规格,包含测量模式mode和测量大小两个值
   */
    class MeasureSpec(val mode: Int, val size: Int) {
        companion object {
      // 精准测量模式
            const val EXACTLY = 1
      // 至多测量模式
            const val AT_MOST = 2
        }
    }
​
}
​
open class MViewGroup(): MView() {
​
  // 省略不相关代码...
  
  /**
     * 父View帮助子View测量的重要方法‼️
     * 结合MViewGroup的MeasureSpec与子View的MLayoutParams,计算出适合子View的MeasureSpec
     *
     * @param spec MViewGroup的测量模式
     * @param childDimension 子View期望大小,通过getLayoutParams获取
     */
    open fun getChildMeasureSpec(spec: MeasureSpec, childDimension: Int): MeasureSpec {
        val specMode = spec.mode
        val specSize = spec.size
        var resultSpecMode = 0
        var resultSpecSize = 0
        when {
      // 父布局的测量模式是准确的,即父View的大小是准确的
            specMode == MeasureSpec.EXACTLY -> {
                if (childDimension >= 0) {
          // 子View有自己指定的大小,随他去
                    resultSpecMode = MeasureSpec.EXACTLY
                    resultSpecSize = childDimension
                } else if (childDimension == MLayoutParams.MATCH_PARENT) {
                    // 子View想要和父View大小一样,将我们的大小传给他
          resultSpecMode = MeasureSpec.EXACTLY
                    resultSpecSize = specSize
                } else if (childDimension == MLayoutParams.WRAP_CONTENT) {
                    // 子View想要根据自身内容决定自己的大小,随他去,但是不能比父View的大小还大
          resultSpecMode = MeasureSpec.AT_MOST
                    resultSpecSize = specSize
                }
            }
      // 父布局有尺寸上限
            specMode == MeasureSpec.AT_MOST -> {
                if (childDimension >= 0) {
          // 子View有自己指定的大小,随他去
                    resultSpecMode = MeasureSpec.EXACTLY
                    resultSpecSize = childDimension
                } else if (childDimension == MLayoutParams.MATCH_PARENT) {
                    // 子View想要和父View大小一样,将父View的尺寸上限传递给子View
          resultSpecMode = MeasureSpec.AT_MOST
                    resultSpecSize = specSize
                } else if (childDimension == MLayoutParams.WRAP_CONTENT) {
                    // 子View想要根据自身内容决定自己的大小,随他去,但是不能比父View的大小还大
          resultSpecMode = MeasureSpec.AT_MOST
                    resultSpecSize = specSize
                }
            }
        }
        return MeasureSpec(resultSpecMode, resultSpecSize)
    }
​
    fun measureChildren(widthMeasureSpec: MeasureSpec, heightMeasureSpec: MeasureSpec) {
        val childrenView = getChildren()
        for (child in childrenView) {
            // MViewGroup在测量子View时,会根据自身的MeasureSpec与子View的MLayoutParams,计算出适合子View的MeasureSpec
            child.measure(
                getChildMeasureSpec(widthMeasureSpec, child.getLayoutParams().width),
                getChildMeasureSpec(heightMeasureSpec, child.getLayoutParams().height)
            )
        }
    }
​
  // 继续省略...
}
  • MView 新增了MeasureSpec内部类,表示测量规格,测量规格包含两个字段:mode和size。MViewGroup 通过 MeasureSpec 来指导和约束MView的测量工作。
  • MView 的 测量方法 measure() 新增了两个参数:widthMeasureSpecheightMeasureSpec,由 父View 测量时传入。
  • MViewGroup 在协助子View测量时,会结合自身的 MeasureSpec 以及 子View的LayoutParams参数,得到子View合理的MeasureSpec,具体生成逻辑在getChildMeasureSpec方法中,这也是测量流程中最重要的一环。

v0.6:继续完善MeasureSpec

基于上面我们所设计的测量流程,MViewGroup通过geChildMeasureSpec()方法确保其子View的测量流程受到自身的指导和约束。 MeasureSpec的测量模式mode有两种:AT_MOSTEXACTLY。仔细研究一下getChildMeasureSpec()方法,我们不难总结出一个结论:子View大小总是受到父View大小的限制的,即子View的大小不能超过其父View的大小,除非子View的宽高指定为一个大于父View宽高的固定的值。 那会不会有这么一种情况:子View的宽高不是具体的值,而是包裹内容大小(即WRAP_CONTENT),但是可以不受父View大小的限制? 会有这样的情况么? 答案当然是肯定的,而且这种情况更加常见:

如图中这种情况,用户的设备空间是有限的,feed流的长度显然超过了用户的设备空间。按照我们设计的测量流程,显然实现不了这种既要包裹住布局内容又不受父View大小限制测量要求。 一般这种布局,我们会将feed流使用可滑动的View来展现,例如ScrollView或者RecyclerView等。按照我们的设计,ScrollView的子View受限于ScrollView大小的限制,是有可能不能完整的展示的。

当然是不合理的,将feed流设计成可滑动的View,本身就是为了用户通过滑动,浏览更多的、完整的信息流,结果却因为测量模式的限制,子View无法完整展示。

那怎么办?简单啊,现有的测量模式满足不了,我们就新造一个嘛:

kotlin 复制代码
open class MView() {
	// 省略不相关代码...
	class MeasureSpec(val mode: Int, val size: Int) {
        companion object {
			// 精准测量模式
            const val EXACTLY = 1
			// 至多测量模式
            const val AT_MOST = 2
			// 未指定模式
			const val UNSPECIFIED = 3
        }
    }
	// 继续省略...
}

MeasureSpec新增了一种测量模式UNSPECIFIED,中文直译为未指定模式,即父View不限制子View的测量,既然新增了一种测量模式,那其他涉及到测量模式处理的方法也要同步修改:

ini 复制代码
open class MView() {

	// 省略不相关代码...

	/**
	 * 测量视图及其内容以确定测量的宽度和高度。此方法由measure(int, int)调用,并且应该被子类覆盖,以提供对其内容的准确和有效的测量。
	 */
	open fun measure(widthMeasureSpec: MeasureSpec, heightMeasureSpec: MeasureSpec) {
        // contentWidthSize、contentHeightSize为View本身的内容宽、高
		setMeasuredDimension(
            getDefaultSize(contentWidthSize, widthMeasureSpec),
            getDefaultSize(contentHeightsize, heightMeasureSpec)
        )
    }

	/**
	 * 根据MeasureSpec获取默认大小
	 */
    fun getDefaultSize(size: Int, measureSpec: MeasureSpec): Int {
        var result = size
        val specMode = measureSpec.mode
        val specSize = measureSpec.size
        when (specMode) {
			// 如果测量模式为UNSPECIFIED,View想要多大就给它多大,不限制它
            MeasureSpec.UNSPECIFIED -> result = size
            MeasureSpec.AT_MOST, MeasureSpec.EXACTLY -> result = specSize
        }
        return result
    }
	// 继续省略...
}

open class MViewGroup() : MView() {

	// 省略不相关代码...
	
	/**
     * 父View帮助子View测量的重要方法‼️
     * 结合MViewGroup的MeasureSpec与子View的MLayoutParams,计算出适合子View的MeasureSpec
     *
     * @param spec MViewGroup的测量模式
     * @param childDimension 子View期望大小,通过getLayoutParams获取
     */
    open fun getChildMeasureSpec(spec: MeasureSpec, childDimension: Int): MeasureSpec {
        val specMode = spec.mode
        val specSize = spec.size
        var resultSpecMode = 0
        var resultSpecSize = 0
        when {
			// 父布局的测量模式是准确的,即父View的大小是准确的
            specMode == MeasureSpec.EXACTLY -> {
                if (childDimension >= 0) {
					// 子View有自己指定的大小,随他去
                    resultSpecMode = MeasureSpec.EXACTLY
                    resultSpecSize = childDimension
                } else if (childDimension == MLayoutParams.MATCH_PARENT) {
                    // 子View想要和父View大小一样,将我们的大小传给他
					resultSpecMode = MeasureSpec.EXACTLY
                    resultSpecSize = specSize
                } else if (childDimension == MLayoutParams.WRAP_CONTENT) {
                    // 子View想要根据自身内容决定自己的大小,随他去,但是不能比父View的大小还大
					resultSpecMode = MeasureSpec.AT_MOST
                    resultSpecSize = specSize
                }
            }
			// 父布局有尺寸上限
            specMode == MeasureSpec.AT_MOST -> {
                if (childDimension >= 0) {
					// 子View有自己指定的大小,随他去
                    resultSpecMode = MeasureSpec.EXACTLY
                    resultSpecSize = childDimension
                } else if (childDimension == MLayoutParams.MATCH_PARENT) {
                    // 子View想要和父View大小一样,将父View的尺寸上限传递给子View
					resultSpecMode = MeasureSpec.AT_MOST
                    resultSpecSize = specSize
                } else if (childDimension == MLayoutParams.WRAP_CONTENT) {
                    // 子View想要根据自身内容决定自己的大小,随他去,但是不能比父View的大小还大
					resultSpecMode = MeasureSpec.AT_MOST
                    resultSpecSize = specSize
                }
            }

			// 父布局有尺寸上限
            specMode == MeasureSpec.UNSPECIFIED -> {
                if (childDimension >= 0) {
					// 子View有自己指定的大小,随他去
                    resultSpecMode = MeasureSpec.EXACTLY
                    resultSpecSize = childDimension
                } else if (childDimension == MLayoutParams.MATCH_PARENT) {
                    // 子View想要和父View大小一样,将我们的大小传给他
					resultSpecMode = MeasureSpec.UNSPECIFIED
                    resultSpecSize = specSize
                } else if (childDimension == MLayoutParams.WRAP_CONTENT) {
                    // 子View想要根据自身内容决定自己的大小,随他去
					resultSpecMode = MeasureSpec.UNSPECIFIED
                    resultSpecSize = specSize
                }
            }
        }
        return MeasureSpec(resultSpecMode, resultSpecSize)
    }

	// 继续省略...
}
  • getChildMeasureSpec()方法中新添了对 MeasureSpec.UNSPECIFIED 的处理;
  • MView的measure方法提供了一套获取默认大小的实现,重点就是当MeasureSpec的mode为UNSPECIFIED时,不对View大小作限制。

总结一下三种测量模式对应的场景(size为MeasureSpec中的size):

  • EXACTLY:View的大小就应该是size这么大, 不管它内容大小是多大;
  • AT_MOST:View的大小最多是size,如果内容大小没这么大的话,就取内容大小。
  • UNSPECIFIED:为了完整展示内容大小,View的大小可以是任意大小,不受size的限制。

Android 6.0 之前和之后的版本 getChildMeasureSpec 方法对UNSPECIFIED模式的处理有区别,6.0之前,UNSPECIFIED 模式下size会指定为0,6.0之后会指定为MViewGroup自身MeasureSpec的size。

v1.0:神功已成

经过上面6个版本的迭代,View的测量流程已设计完毕,它足以应对我们日常开发中的各种各样的场景,我们来继续看下有没有可以优化的点。

MeasureSpec的优化

上面的设计中,我们将MeasureSpec设计成了一个拥有mode和size两个字段的类,MViewGroup 在 measureChildren 时,会调用 getChildMeasureSpec方法生成MeasureSpec,而measure又是一个高频操作,每次都new一个新的MeasureSpec对象显然会增加额外的内存开销,想办法优化下:

kotlin 复制代码
open class MView {

    open fun measure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        setMeasuredDimension(
            getDefaultSize(contentWidthSize, widthMeasureSpec),
            getDefaultSize(contentHeightsize, heightMeasureSpec)
        )
    }

    fun getDefaultSize(size: Int, measureSpec: Int): Int {
        var result = size
        val specMode = MeasureSpec.getMode(measureSpec)
        val specSize = MeasureSpec.getSize(measureSpec)
        when (specMode) {
            MeasureSpec.UNSPECIFIED -> result = size
            MeasureSpec.AT_MOST, MeasureSpec.EXACTLY -> result = specSize
        }
        return result
    }

    object MeasureSpec {
        private const val MODE_SHIFT = 30
        private val MODE_MASK = 0x3 shl MODE_SHIFT

    
        val UNSPECIFIED = 0 shl MODE_SHIFT
        val EXACTLY = 1 shl MODE_SHIFT
        val AT_MOST = 2 shl MODE_SHIFT
     
        fun makeMeasureSpec(
            size: Int,
            mode: Int
        ): Int {
            return size and MODE_MASK.inv() or (mode and MODE_MASK)
        }
       
        fun getMode(measureSpec: Int): Int {
            return measureSpec and MODE_MASK
        }
       
        fun getSize(measureSpec: Int): Int {
            return measureSpec and MODE_MASK.inv()
        }
    }


}

open class MViewGroup: MView() {

    open fun getChildMeasureSpec(spec: Int, childDimension: Int): Int {
        val specMode = MeasureSpec.getMode(spec)
        val specSize = MeasureSpec.getSize(spec)
        var resultSpecMode = 0
        var resultSpecSize = 0
        // 省略不相关代码...
        return MeasureSpec.makeMeasureSpec(resultSpecSize, resultSpecMode)
    }

    fun measureChildren(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        val childrenView = getChildren()
        for (child in childrenView) {
            // MViewGroup在测量子View时,会根据自身的MeasureSpec与子View的MLayoutParams,计算出适合子View的MeasureSpec
            child.measure(
                getChildMeasureSpec(widthMeasureSpec, child.getLayoutParams().width),
                getChildMeasureSpec(heightMeasureSpec, child.getLayoutParams().height)
            )
        }
    }
}

我们将MeasureSpec从class改为了object,将之前的mode和size统一用一个32位的int来表示,高两位表示测量模式mode,低30位来表示测量大小size,这样极大程度的减少了measure()过程中的对象分配。

到这儿,我们的测量流程已设计完毕。

整个View体系就是按照这个测量流程,递归的完成每一个View的测量流程,得到测量结果。至于最外层的ViewGroup,它的测量规格MeasureSpec由android.view.ViewRootImpl#getRootMeasureSpec方法指定。

上面我们迭代出的v1.0版本的测量流程代码,和Android源码里的大体思路和流程如出一辙,只是为了方便理解精简了些许代码,有兴趣的读者可以按照下面的索引自行比照源码加深一下理解:

Android源码中将measure方法拆分成了measure和onMeasure两个方法:

  1. measure 方法:

    • measure 是 View 类中的一个 final 方法,开发者不能覆盖它。
    • 它负责调用 onMeasure 方法,并且处理与测量相关的其他逻辑,如考虑 padding,以及设置测量的宽度和高度。
    • measure 方法还负责保存测量结果(宽度和高度),这样它们就可以在布局阶段被使用。
    • 该方法会处理 MeasureSpec,并将它们传递给 onMeasure 方法。
    • 它确保 onMeasure 不会被多次不必要地调用,从而优化性能。
  2. onMeasure 方法:

    • onMeasure 是一个 protected 方法,不同于measure方法,它可以被重写。
    • 开发者应该重写这个方法来定义自定义 View 或 ViewGroup 的大小规则。
    • 在这个方法中,开发者需要使用传入的 widthMeasureSpec 和 heightMeasureSpec 来决定当前视图的大小,并且通过 setMeasuredDimension 方法来设置测量的宽度和高度。

将测量过程分成 measure 和 onMeasure 两个方法的设计,反映了面向对象设计原则中的单一职责原则(SRP)。measure 方法处理测量流程的管理,而 onMeasure 方法则专注于具体的测量逻辑。这样的设计使得视图的测量逻辑更加清晰,并且更容易通过重写 onMeasure 方法来自定义视图的大小。

总结

回到文章开头,我们不难发现,表格里的内容,不过就是对getChildMeasureSpec方法的总结,至于UNSPECIFIED的应用场景,也不是简简单单的系统内部使用而已。

相关推荐
小比卡丘1 小时前
C语言进阶版第17课—自定义类型:联合和枚举
android·java·c语言
前行的小黑炭2 小时前
一篇搞定Android 实现扫码支付:如何对接海外的第三方支付;项目中的真实经验分享;如何高效对接,高效开发
android
落落落sss3 小时前
MybatisPlus
android·java·开发语言·spring·tomcat·rabbitmq·mybatis
代码敲上天.4 小时前
数据库语句优化
android·数据库·adb
GEEKVIP6 小时前
手机使用技巧:8 个 Android 锁屏移除工具 [解锁 Android]
android·macos·ios·智能手机·电脑·手机·iphone
model20058 小时前
android + tflite 分类APP开发-2
android·分类·tflite
彭于晏6898 小时前
Android广播
android·java·开发语言
与衫9 小时前
掌握嵌套子查询:复杂 SQL 中 * 列的准确表列关系
android·javascript·sql
500了15 小时前
Kotlin基本知识
android·开发语言·kotlin
人工智能的苟富贵16 小时前
Android Debug Bridge(ADB)完全指南
android·adb