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

人云亦云的「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的应用场景,也不是简简单单的系统内部使用而已。

相关推荐
Jasonakeke2 分钟前
【重学 MySQL】九十二、 MySQL8 密码强度评估与配置指南
android·数据库·mysql
Mertrix_ITCH2 分钟前
在 Android Studio 中修改 APK 启动图标(2025826)
android·ide·android studio
荏苒追寻11 分钟前
Android OpenGL基础1——常用概念及方法解释
android
人生游戏牛马NPC1号22 分钟前
学习 Android (十七) 学习 OpenCV (二)
android·opencv·学习
恋猫de小郭1 小时前
谷歌开启 Android 开发者身份验证,明年可能开始禁止“未经验证”应用的侧载,要求所有开发者向谷歌表明身份
android·前端·flutter
用户091 小时前
Gradle声明式构建总结
android
用户092 小时前
Gradle插件开发实践总结
android
Digitally12 小时前
如何将视频从安卓设备传输到Mac?
android·macos
alexhilton14 小时前
Compose Unstyled:Compose UI中失传的设计系统层
android·kotlin·android jetpack
刘龙超16 小时前
如何应对 Android 面试官 -> 玩转 RxJava (基础使用)
android·rxjava