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

相关推荐
小墙程序员6 小时前
一文了解 Android 5 到 16 期间跨进程通信(IPC) 的使用
android·android studio
leon_teacher9 小时前
HarmonyOS权限管理应用
android·服务器·前端·javascript·华为·harmonyos
Just_Paranoid10 小时前
【AOSP】Android Dump 开发与调试指南
android·adb·service·dumpsys
独行soc12 小时前
2025年渗透测试面试题总结-38(题目+回答)
android·安全·网络安全·面试·职场和发展·渗透测试·求职
做一位快乐的码农13 小时前
原生安卓#基于Android的爱好者分享论坛的设计与实现/基于Android在线论坛系统app/基于Android的论坛系统的设计与实现的设计与实现
android
Amber_3713 小时前
深入理解Go 与 PHP 在参数传递上的核心区别
android·golang·php
_祝你今天愉快15 小时前
Android FrameWork - 开机启动 SystemServer 进程
android
洞见前行16 小时前
Android第一代加固技术原理详解(附源码)
android·安全
CYRUS_STUDIO16 小时前
深入解析 dex2oat:vdex、cdex、dex 格式转换全流程实战
android·源码·逆向