Android 自定义 View 基础:布局流程详解

前言

布局的作用是为 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 是 240dpLinearLayout 会将其作为自己的宽度。

接着第二遍测量,现在,LinearLayout 的宽度已经确定,它会再对所有子 View 进行测量。这一次,第一个子 View 就能够根据要求,将自己的宽度设置为 240dp

当然,还可能会有更复杂的情况,比如所有子 View 都是 match_parent,但父布局却是 wrap_content,具体策略需要看每个布局自身的实现规则。

个体

从单个 View 上来看,流程是这样的:

  1. 运行前,我们在 XML 布局文件中通过 layout_Xxx 属性(如 layout_width),指定 View 的布局要求。

  2. 父 View 在自己的 onMeasure 方法中,根据子 View 的布局要求以及自己的可用空间,得出对子 View 的具体尺寸要求。这个要求被称为 MeasureSpec(测量规范)。

    然后,父 View 会将这个 MeasureSpec 传给子 View 的 measure() 方法,让子 View 在这个规范下进行自我测量。

    可用空间是一块矩形区域,它的大小和位置是由父 View 的布局规则以及当前已测量过的子 View 决定的。例如,可以是这样的:
    其中灰色区域为可用空间。

  3. 子 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 还是占满了屏幕。这是因为 ConstraintLayoutonMeasure 中发现子 View 的测量结果不符合要求,会再次测量,强行修正结果。

如果将根布局换成 LinearLayout,因为它会保持子 View 的测量结果,效果就会变为这样:

  1. 父 View 在所有子 View 测量完毕后,在自己的 onLayout 方法中,计算出每一个子 View 相对于自己的位置(左、上、右、下四个坐标)。

  2. 子 View 布局,父 View 调用子 View 的 layout() 方法,将刚刚计算出的位置坐标和测量好的实际尺寸传给子 View,子 View 会将这些值保存到内部。

    如果我们重写 layout() 方法,你会发现这次连 ConstraintLayout 也拿我们没办法了。

    不过,在实际中,不推荐这样做。如果我们要自定义布局逻辑,会去继承 ViewGroup 并重写 onLayout() 方法。

    kotlin 复制代码
    class 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() 方法,传入它们的位置和尺寸,来完成整个布局过程。

相关推荐
小趴菜82276 小时前
安卓接入Kwai广告源
android·kotlin
2501_916013746 小时前
iOS 混淆与 App Store 审核兼容性 避免被拒的策略与实战流程(iOS 混淆、ipa 加固、上架合规)
android·ios·小程序·https·uni-app·iphone·webview
程序员江同学7 小时前
Kotlin 技术月报 | 2025 年 9 月
android·kotlin
码农的小菜园8 小时前
探究ContentProvider(一)
android
时光少年9 小时前
Compose AnnotatedString实现Html样式解析
android·前端
hnlgzb10 小时前
安卓中,kotlin如何写app界面?
android·开发语言·kotlin
jzlhll12311 小时前
deepseek kotlin flow快生产者和慢消费者解决策略
android·kotlin
火柴就是我11 小时前
Android 事件分发之动态的决定某个View来处理事件
android
一直向钱11 小时前
FileProvider 配置必须针对 Android 7.0+(API 24+)做兼容
android
zh_xuan11 小时前
Android 消息循环机制
android