前言
布局的作用是为 View 的绘制 和触摸反馈提供支持,具体点,就是让 View 知道在哪绘制,以及确定用户可触摸的范围。
布局过程会去获取 View 的位置和尺寸,具体来说就是:让每一个 View 计算出相对父 View 的位置和自己的尺寸。完成了这两步,布局就确定下来了。
我们现在就来说说布局的流程。
流程
整体
从整体上来看,布局分为测量和布局两个核心阶段。
-
测量流程(Measure)
从根 View 递归调用每一级子 View 的
measure()
方法,让这些子 View 进行自我测量,得到一个期望的尺寸。父 View 根据所有子 View 返回的期望尺寸,结合上自己的布局规则,最终计算并确定每个子 View 的实际尺寸。同时,父 View 的实际尺寸也会被确定。
-
布局流程(Layout)
首先每个父 View 会根据测量结果,计算出其直接子 View 的摆放位置。
然后从根 View 开始递归调用每一级子 View 的
layout()
方法,将实际尺寸和计算出的位置传入,子 View 会将这些信息保存下来。
测量流程可能会执行多次,但布局过程只会执行一遍,将最终的结果应用到界面上。
就比如下面这个例子:
xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<View
android:layout_width="match_parent"
android:layout_height="80dp" />
<View
android:layout_width="240dp"
android:layout_height="80dp" />
<View
android:layout_width="160dp"
android:layout_height="80dp" />
</LinearLayout>
在上述布局中,LinearLayout
的宽度是适应内容的(wrap_content
),但第一个子 View 的宽度却是填满父 View(match_parent
),这就会产生矛盾:LinearLayout
必须要知道自己的宽度,才能得出第一个子 View 的宽度。
为此,LinearLayout
的策略是进行两次测量。
第一遍测量时,将第一个 View
的宽度视为 0
,先测量其他的子 View
。测量后得知,最宽的子 View 是 240dp
,LinearLayout
会将其作为自己的宽度。
接着第二遍测量,现在,LinearLayout
的宽度已经确定,它会再对所有子 View 进行测量。这一次,第一个子 View
就能够根据要求,将自己的宽度设置为 240dp
。
当然,还可能会有更复杂的情况,比如所有子 View 都是 match_parent
,但父布局却是 wrap_content
,具体策略需要看每个布局自身的实现规则。
个体
从单个 View 上来看,流程是这样的:
-
运行前,我们在 XML 布局文件中通过
layout_Xxx
属性(如layout_width
),指定 View 的布局要求。 -
父 View 在自己的
onMeasure
方法中,根据子 View 的布局要求以及自己的可用空间,得出对子 View 的具体尺寸要求。这个要求被称为MeasureSpec
(测量规范)。然后,父 View 会将这个
MeasureSpec
传给子 View 的measure()
方法,让子 View 在这个规范下进行自我测量。可用空间是一块矩形区域,它的大小和位置是由父 View 的布局规则以及当前已测量过的子 View 决定的。例如,可以是这样的:
其中灰色区域为可用空间。
-
子 View 开始测量,在
onMeasure()
方法中,根据父 View 对自己的要求(MeasureSpec
)以及自己的特性,得出自己的期望尺寸。如果当前子 View 是
ViewGroup
,还必须在该方法中,调用每一个子 View 的measure
方法进行测量,这样才能得到自己最终的尺寸。特性指的是在自由测量下计算出的尺寸(比如
TextView
根据文字内容计算出的尺寸),父 View 的要求高于子 View 自身的特性。比如父 View 要求宽度最大为 150dp,而子 View 需要 200dp,那么子 View 也只能将自己的期望宽度设置为 150dp。为什么呢?因为父 View 的要求,其实结合了我们开发者传入的要求以及它自身的实际情况。比如我们要求子 View 为
match_parent
,而父 View 实际为300dp
宽,那么父 View 的要求就是300dp
。子 View 遵循这个要求,才能确保整体布局的正确。
我们来演示一下子 View 不听话的情况,有如下布局:
xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#E91E63" />
</androidx.constraintlayout.widget.ConstraintLayout>
在 MyView
中,我们直接无视了父 View 的要求。
kotlin
class MyView(context: Context?, attrs: AttributeSet?) :
View(context, attrs) {
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// 强行将自己的宽高设置为 100dp
setMeasuredDimension(100.dp.toInt(), 100.dp.toInt())
}
}
运行效果:
发现 MyView
还是占满了屏幕。这是因为 ConstraintLayout
在 onMeasure
中发现子 View 的测量结果不符合要求,会再次测量,强行修正结果。
如果将根布局换成 LinearLayout
,因为它会保持子 View 的测量结果,效果就会变为这样:
-
父 View 在所有子 View 测量完毕后,在自己的
onLayout
方法中,计算出每一个子 View 相对于自己的位置(左、上、右、下四个坐标)。 -
子 View 布局,父 View 调用子 View 的
layout()
方法,将刚刚计算出的位置坐标和测量好的实际尺寸传给子 View,子 View 会将这些值保存到内部。如果我们重写
layout()
方法,你会发现这次连ConstraintLayout
也拿我们没办法了。不过,在实际中,不推荐这样做。如果我们要自定义布局逻辑,会去继承 ViewGroup 并重写
onLayout()
方法。kotlinclass MyView(context: Context?, attrs: AttributeSet?) : View(context, attrs) { override fun layout(l: Int, t: Int, r: Int, b: Int) { // 强行改变自己的宽高 super.layout(l, t, l + 100.dp.toInt(), t + 100.dp.toInt()) } }
如果当前 View 是一个
ViewGroup
,它还会对每个子 View 进行布局,也就是调用每一个子 View 的layout()
方法,传入它们的位置和尺寸,来完成整个布局过程。