View 测量算法我知道

View 宽高是怎么测量的?相信这是 Android 开发选择性遗忘的问题,也一直记在我的 TODO 小本子上,本文准备挑战一下,给这一栏打个勾。

本文尝试从"难在哪里" ➤ "实测找规律" ➤ "源码懂原理" ➤ "问题验理解" ➤ "有什么用"推演,带给你一张看得懂的 View 测量流程图。

难在哪里

在回答这个问题前,我想了一个喜闻乐见的场景:到饭店吃饭,正常情况每人点个硬菜,最后加个汤和几个小菜就差不多了,全程不会超过 5 分钟。但总会有人会点一道神菜--"随便",最麻烦的是大家都点这道菜,然后就"你点啥我吃啥","你吃啥我点啥"...死锁了。这顿饭吃得反正大家都纠结:"我随便点的不知道是不是你真想要的","你这也太随便了","凑合着吃吧"...

View 高度测量也有类似问题。如果每个 View 都知道自己的宽高,问题就像 5 分钟点菜那么简单,难就难在 View 的宽高与父子 View 宽高有关联,和子 View 一样高(wrap_content),或者和父 View 一样高(match_parent),像不像"随便",但玩的却是"你猜",把决策权抛给别人,但自己却又非常在乎结果。

更有甚者,View 指定高度和父/子 View 一样高时,父/子 View 也可能这么想的,甚至会整个 View 树宽高都随便(wrap_contentmatch_parent),这下宽高到底是多少?不知道,太难了...

听着有点绕,等等,想把逻辑搅浑,但就这种程度想忽悠我放弃,还差点意思。

实测找规律

众所周知,我们耳熟能详的设置 View 宽高的 API 就是 View.setLayoutParams(),它决定了我们对控件控制力的若干种可能。如果我穷举出所有,是不是也就找到了成功的可能。

先画张表格盘点一下爷孙三代 View 宽高指定为 match_parentwrap_content 混排组合情况,试试水深几何。实测结果如下:

万万没想到,结果竟然出奇一致,就这么简单粗暴又欢乐的吗?我们先让"为什么"飞一会,继续往下走。

如果这些相互关联情况都只有一种结果,好像问题又回归简单了,但实际情况是 match_parentwrap_content 和固定值()的混排组合。

爷孙三代 View,宽/高取值有 5 种(match_parentwrap_content)可能,有 5(爷)× 5(父)× 5(子) 种组合,受不了了。

要整就整二年级的!

通过上面表格,可以进一步简化场景,只需要父子两代 View 就能模拟爷孙三代甚至子子孙孙无穷尽的情况,决定自身宽高就是父 View 宽高和自己宽高(此时子 View 高度可视为内容宽高,即自身宽高的一部分),然后通过递归不断重复就能无穷无尽。

为了便于理解,我们下面将只探讨高度如何测量,因为宽度测量逻辑类似。

终于,父子二代 View,高取值有 5 种可能,有 5(父)× 5(子) 种组合。

做个简易 Demo 实测验证一下,同时也方便直接 Debug 源码回答问什么?

大家可以尝试自己填写表格结果,我猜错了 5 个(对应③、⑯、⑲、⑳、㉔,上图序号标紫红色)。

总结一下规律:

  • View 指定高度为固定值时,其中负数等同于 0,实际高度为指定高度(对应①、②、⑥、⑦、⑧、⑪、⑫、⑬)。但有一个特例,就是父 View 指定高度为负数,子 View 指定高度为非负数,此时父 View 实际高度不是为 0,而是为子 View 实际高度(对应③)。
  • View 指定高度为 wrap_content,如果其子 View 高度为固定值(其中负数等同于 0),则实际高度为子 View 实际高度(对应④、⑨、⑭),否则结果同 match_parent,为父 View 实际高度(对应⑯、⑰、⑱、⑲、⑳、㉔)。
  • View 指定高度为 match_parent,实际高度为父 View 实际高度(对应⑤、⑩、⑮、⑳、㉑、㉒、㉓、㉔、㉕)。

上述总结中,子控件指定高度为 wrap_contentmatch_parent 在各情形下实际高度一样,有点和我的认知不太一样。有必要使用 FrameLayoutTextViewImageView 这几个常用控件试一试。

一试果然有惊喜,子控件为 FrameLayoutTextViewImageView 时,指定高度为 wrap_content 时,实际高度等于最小高度,这才是海的味道我知道。

通过上面实测看到的表象,给我们探索源码本质提了三个问题:

  1. View 指定高 -300px,子 View 指定高 100px,父 View 实际高度为什么是 100px 不是 0px
  2. 父控件指定高度为 wrap_content,子控件指定高度为 match_parent 如何计算?为什么 ViewFrameLayout 等常用控件不一样?
  3. 子控件为 View,为什么指定高度为 match_parentwrap_content 时结果都一样?

如果再通过尝试各种可能推断规律,不免把工程师这行搞 Low 了,直接看源码分析逻辑,并解答上述问题。

源码懂原理

重写父子控件的 View.onMeasure() 方法,打上断点,切换指定高度将会断住代码。

整体算法逻辑并不复杂,核心围绕着父控件指定的测量规格 measureSpec(高 2 位的测量模式 specMode 和低 30 位的测量大小 specSize 的复合值)和自身的内容大小 size 针对几种情况分别给出计算规则。在父控件期望约束和自身内容大小下做权衡。值得注意的是,每个控件基本都重新写 View.onMeasure() 方法,最终直接调用 View.setMeasuredDimension() 而不是调用 super.onMeasure()设置最终宽高。

View 控件使用的是简单粗暴的 View.getetDefaultSize() 计算高度:

  1. 测量模式为 UNSPECIFIED 时高度为 size
  2. 其他情况高度为 specSize

其他常规控件遵循更符合常识的 View.resolveSizeAndState() 计算高度 :

  1. 测量规格为 AT_MOST,高度为 sizespecSize 的最小值。
  2. 测量规格为 EXACTLY,高度为 specSize
  3. 测量规格为 UNSPECIFIED,高度为 size

这里需要注意的是,如果是容器控件,会先调用 ViewGroup.measureChildWithMargins() 计算子控件高度,这是自身内容高度的一部分。内部核心方法为 ViewGroup.getChildMeasureSpec() :

  1. 指定了非负值,则直接为该值。
  2. 指定为 match_parent,则为父控件测量规格。
  3. 指定为 wrap_content,除 EXACTLY 时修正为 AT_MOST,其他同父控件测量规格。
  4. 其他数对应默认测量规格(0,UNSPECIFIED)

整体只是一堆映射关系,使用表格一览无余,如下图所示。

▲ 测量规格算法表格图

至此,逻辑基本涵盖,停下来总结一下。

先按函数调用画一幅调用关系图:

上图我自己也思索了一阵,发现还是不够清晰,尝试摆脱方法调用关系,从逻辑维度画一幅 View 测量流程图:

问题验理解

回过头来回答上述提到的 3 个问题。

问题一:View 指定高 -300px,子 View 指定高 100px,父 View 实际高度为什么是 100px 不是 0px

在回答这个问题前,父 View 的父 View 指定的测量规格又是什么呢?无外乎两种方法,要么看看根 View 被指定的测量规格是什么,要么穷举出所有可能。

先看看根 View 被指定的测量规格,通过断点很容易找出根 View 测量规格逻辑在 ViewRootImpl.getRootMeasureSpec() 中,源码如下:

实测根 View 调用为 ViewRootImpl.getRootMeasureSpec(windowSize, -1),执行上图第 15 行逻辑,返回测量规格为(windowSize, EXACTLY)。正常 Activity.setContentView(contentView) 传入的 contentView 被调用 View.measure() 方法时,对应的测量规格也为 (windowSize1, EXACTLY),其中 windowSize1windowSize 减去一些边角大小。

那么,父 View 的测量规格又有几种可能呢?

因为父 View 指定高度为 -300px,通过上面"测量规格算法表格图"可知,无论父 View 的父 View 指定测量规格如何,计算出的测量规格均为(0,UNSPECIFIED)

哦豁,传说中的 UNSPECIFIED 竟然出现了,有点小激动。

View 指定高度为 100px,同理,因为指定高度为非负数,无论父 View 指定测量规格如何,计算出的测量规格均为(100,EXACTLY)

接着,父 ViewView.resolveSizeAndState() 计算出最终高度,对应"测量规格为 UNSPECIFIED,高度为 size",而 size 就是子控件高度,即证父 View 实际高度为 100px 而不是 0px

问题二: 父控件指定高度为 wrap_content ,子控件指定高度为 match_parent 如何计算?为什么 ViewFrameLayout 等常用控件不一样?

父控件的父控件被指定的测量模式,通常情况下不会是 UNSPECIFIED,剩下情况通过上面"测量规格算法表格图"可知,父控件被指定的测量规格均为(leftSpecSize,AT_MOST)。接着递归计算子控件。

子控件计算测量模式入参为 AT_MOST,自身指定高度 match_parent,则获得测量规格为(leftSpecSize,AT_MOST)

接下来会区分不同控件计算子控件高度。

  • View 则调用 View.getDefaultSize() 获取实际高度为父控件指定的 leftSpecSize,也就会出现和屏幕等高的结果。
  • 其他常用控件则调用 View.resolveSizeAndState() ,在 AT_MOST 模式下返回最小高度。

最终,父控件在 AT_MOST 模式下高度也取 leftSpecSize 和子控件高度最小值。

问题三: 子控件为 View,为什么指定高度为 match_parentwrap_content 时结果都一样?

View 指定高度为 wrap_contentmatch_parent,在父控件计算子 View 测量规格时,对应"测量规格算法表格图"第一、二行,结果是(leftSpecSize,EXACTLY)(leftSpecSize,AT_MOST),但因为 View 通过用 View.getDefaultSize() 获取大小,EXACTLYAT_MOST 规格下都是取 leftSpecSize,即两种情况下返回值一样。即证。

有什么用

这是一个现实的问题,"为用而学,学以致用"。

  1. 符合复现 UNSPECIFIED 这种测量模式,设置为小于 -2 的数即可做到,能够测试覆盖自定义控件逻辑的鲁棒性。
  2. 自由设置若干子控件和自适应高度子控件一样高,曾经有个需求就是服务端下发文案和图片 url,要求达到图片作为文案的背景效果。因为 TextView 高度是自适应,需要将 ImageViewTextView 设置成一样高。最佳实践就是重写父控件 View.onMeasure() 方法,TextView 计算出高度后直接设置 ImageView 高度 (文本控件高度,EXACTLY)即可。
  3. 继承 View 自定义控件时,记得重写 View.onMeasure() 方法处理 wrap_contentmatch_parent 默认一样效果的反常识情况。
  4. 方便其他自定义控件实现优雅交互,和技术大牛过几招。
相关推荐
逊嘘4 分钟前
【Java语言】抽象类与接口
java·开发语言·jvm
帅得不敢出门9 分钟前
Gradle命令编译Android Studio工程项目并签名
android·ide·android studio·gradlew
morris13111 分钟前
【SpringBoot】Xss的常见攻击方式与防御手段
java·spring boot·xss·csp
我要洋人死17 分钟前
导航栏及下拉菜单的实现
前端·css·css3
科技探秘人28 分钟前
Chrome与火狐哪个浏览器的隐私追踪功能更好
前端·chrome
科技探秘人29 分钟前
Chrome与傲游浏览器性能与功能的深度对比
前端·chrome
JerryXZR34 分钟前
前端开发中ES6的技术细节二
前端·javascript·es6
七星静香36 分钟前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
Jacob程序员37 分钟前
java导出word文件(手绘)
java·开发语言·word
ZHOUPUYU37 分钟前
IntelliJ IDEA超详细下载安装教程(附安装包)
java·ide·intellij-idea