Android知识体系之布局(Layout)
Android知识体系
布局
布局过程,就是程序在运行时利用布局文件的代码来计算出实际尺寸的过程。
两个阶段:测量阶段和布局阶段。
- 测量阶段
从上到下递归地调用每个 View 或者 ViewGroup 的 measure() 方法,测量他们的尺寸并计算它们的位置;
- 布局阶段
从上到下递归地调用每个 View 或者 ViewGroup 的 layout() 方法,把测得的它们的尺寸和位置赋值给它们。
测量阶段
测量阶段,measure() 方法被父 View 调用,在 measure() 中做一些准备和优化工作后,调用 onMeasure() 来进行实际的自我测量。 onMeasure() 做的事,View 和 ViewGroup 不一样:
- View
View 在 onMeasure() 中会计算出自己的尺寸然后保存;
- ViewGroup
ViewGroup 在 onMeasure() 中会调用所有子 View 的 measure() 让它们进行自我测量,并根据子 View 计算出的期望尺寸来计算出它们的实际尺寸和位置(实际上 99.99% 的父 View 都会使用子 View 给出的期望尺寸来作为实际尺寸,原因在下期或下下期会讲到)然后保存。同时,它也会根据子 View 的尺寸和位置来计算出自己的尺寸然后保存;
布局阶段
布局阶段,layout() 方法被父 View 调用,在 layout() 中它会保存父 View 传进来的自己的位置和尺寸,并且调用 onLayout() 来进行实际的内部布局。onLayout() 做的事, View 和 ViewGroup 也不一样:
- View
由于没有子 View,所以 View 的 onLayout() 什么也不做。
- ViewGroup
ViewGroup 在 onLayout() 中会调用自己的所有子 View 的 layout() 方法,把它们的尺寸和位置传给它们,让它们完成自我的内部布局。
自定义布局过程
重写布局过程中的相关方法:
- 测量过程:onMeasure()
- 布局过程:onLayout()
重写方式,可以分为三类:
- 重写 onMeasure() 来修改已有的 View 的尺寸;
- 重写 onMeasure() 来全新定制自定义 View 的尺寸;
- 重写 onMeasure() 和 onLayout() 来全新定制自定义 ViewGroup 的内部布局。
重写 onMeasure() 来修改已有的 View 的尺寸
也就是重写 onMeasure() 来修改已有的 View 的尺寸的具体做法:
- 重写 onMeasure() 方法,并在里面调用 super.onMeasure(),触发原有的自我测量;
- 在 super.onMeasure() 的下面用 getMeasuredWidth() 和 getMeasuredHeight() 来获取到之前的测量结果,并使用自己的算法,根据测量结果计算出新的结果;
- 调用 setMeasuredDimension() 来保存新的结果。
比如说,我们想写一个正方形的ImageView,就可以基于ImageView来把图片修改为正方形:
重写 onMeasure() 来全新定制自定义 View 的尺寸
全新定制尺寸和修改尺寸的最重要区别:
需要在计算的同时,保证计算结果满足父 View 给出的的尺寸限制
父 View 的尺寸限制
由来:开发者的要求(布局文件中 layout_ 打头的属性)经过父 View 处理计算后的更精确的要求;
限制的分类:
- UNSPECIFIED:不限制
- AT_MOST:限制上限
- EXACTLY:限制固定值
方式
- 重新 onMeasure(),不用调用super.onMeasure(),并计算出 View 的尺寸;
- 使用 resolveSize() 来让子 View 的计算结果符合父 View 的限制(当然,如果你想用自己的方式来满足父 View 的限制也行)。
重写 onMeasure() 和 onLayout()自定义 ViewGroup 的内部布局
定制 Layout 内部布局的方式
- 重写 onMeasure() 来计算内部布局
- 重写 onLayout() 来摆放子 View
重写 onMeasure() 的三个步骤:
- 调用每个子 View 的 measure() 来计算子 View 的尺寸
- 计算子 View 的位置并保存子 View 的位置和尺寸
- 不是所有的Layout都需要保存位置,因为有的layout可以再布局阶段实时推导出子View的位置,例如LinearLayout
- 有的时候对某些子View需要重复测量两次或者多次才能得到正确的尺寸和位置
举一个例子,一个纵向的LinearLayout,它的宽度是wrap_content,而它的一个子View的宽度是match_parent,按照上面我们说的,这个子View应该是顶满父布局的可用宽度,但是LinearLayout在这种情况下不是这样处理的,它又自己的规则:首先,使用LinearLayout自己的MeasureSpec来测量宽度是match_parent的子View,其次,在把其他所有的子View都测量一遍,得到最大的宽度,最后,再次测量宽度是match_parent的子View,mode使用EXACTLY,size就是刚才的宽度最大值,确定宽度是match_parent的子View的最终宽度
- 计算自己的尺寸并用 setMeasuredDimension() 保存
计算子 View 尺寸的关键
计算子 View 的尺寸,关键在于 measure() 方法的两个参数——也就是子 View 的两个 MeasureSpec 的计算。
子 View 的 MeasureSpec 的计算方式:
- 结合开发者的要求(xml 中 layout_ 打头的属性)和自己的可用空间(自己的尺寸上限 - 已用尺寸)
- 尺寸上限根据自己的 MeasureSpec 中的 mode 而定
- EXACTLY / AT_MOST:尺寸上限为 MeasureSpec 中的 size
- UNSPECIFIED:尺寸无上限
重写 onLayout() 的方式
在 onLayout() 里调用每个子 View 的 layout() ,让它们保存自己的位置和尺寸。
参考
https://hencoder.com/ui-2-1/
https://hencoder.com/ui-2-2/
https://hencoder.com/ui-2-3/