A
第 5 章JAVA30 分钟

布局基础:LinearLayout / RelativeLayout / FrameLayout

掌握 Android 三大经典布局的用法、嵌套优化与复用技巧,为现代布局打下根基。

学习目标

  • 理解 ViewGroup 与 View 的层级关系,能写出可运行的布局 XML
  • 熟练使用 LinearLayout 的 orientation / weight / layout_gravity
  • 掌握 RelativeLayout 的相对位置规则与 FrameLayout 的层叠效果
  • 了解布局嵌套的性能代价,会用 include / merge / ViewStub 优化

学习目标

  • 理解 View 树结构,知道布局 XML 是如何被解析成屏幕上的像素的
  • 能用 LinearLayout、RelativeLayout、FrameLayout 独立完成一个登录页、列表项、引导浮层
  • 知道 layout_width / layout_height / weight / gravity 等核心属性的真正含义,而不是「抄了就能跑」
  • 学会用 include、merge、ViewStub 三件套来减少重复、降低层级、延迟加载
  • 建立「布局层级越浅越好」的性能直觉,为后续 ConstraintLayout 学习做准备

布局基础:从 View 树讲起

1. View 与 ViewGroup 是什么关系

Android 屏幕上一切可见的东西都是 View。按钮是 Button(继承自 TextView),文字是 TextView,图片是 ImageView。而能装下这些控件、负责给它们排位置的容器,叫做 ViewGroup,它本身也是 View 的子类——所以容器也能装容器。

这样一个「父容器装子控件、子控件又能继续装孙控件」的结构,就是 View 树。我们写的每份 XML 布局,最终都会被 LayoutInflater 解析成一棵 View 树,挂在 Activity 的 DecorView 下面。

DecorView
└── LinearLayout (屏幕根容器)
    ├── FrameLayout (状态栏背景)
    └── FrameLayout (内容区,setContentView 装在这里)
        └── 你写的布局根节点

关键认知:布局 XML 的根节点不一定是 Activity 内容区的直接子节点,系统会再套一层。这能解释为什么有时候你设了 layout_marginTop 却没生效——被外层容器「吃掉」了。

2. 必须先搞懂的三个通用属性

不论用哪种布局,下面三个属性每个子控件都要写,不写就会有「为什么我的按钮只占了左上角一小块」的困惑。

<!-- 这两个是 LayoutParams 的属性,写在自己身上,但由父布局解析 -->
android:layout_width="match_parent"
android:layout_height="wrap_content"

<!-- 决定自己在父容器中的位置,对 LinearLayout 尤其重要 -->
android:layout_gravity="center"
  • match_parent:撑满父容器剩余空间
  • wrap_content:刚好包住自己的内容
  • 固定值如 100dp:写死大小,不推荐多用(不同屏幕密度下表现不一致,且无法自适应)

注意区分两个容易混淆的属性:

属性 作用对象 含义
android:gravity 自己内部的内容 我的内容(文字、子控件)在我自己内部怎么放
android:layout_gravity 父容器中的自己 我在父容器里怎么放

记忆口诀:带 layout_ 前缀的都是给父容器看的,因为 LayoutParams 是父容器的规则集。

3. LinearLayout:线性排列

LinearLayout 让子控件按水平或垂直方向依次排列,是最直觉、也是新手最常用的布局。核心属性只有两个:orientation(方向)和 weight(权重)。

3.1 orientation:方向

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="第一行"
        android:textSize="16sp" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="第二行"
        android:textSize="16sp" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="第三行"
        android:textSize="16sp" />

</LinearLayout>

orientation="vertical" 让三个 TextView 从上到下排列;改成 horizontal 它们就并排。默认是 horizontal,新手常踩的坑是忘了写 orientation,结果横向排不下被挤出屏幕。

3.2 weight:按比例分配剩余空间

weight 是 LinearLayout 最强大的特性。计算规则是:先把所有子控件的「基础尺寸」算出来(按 layout_width/layout_height 给的值),剩余空间按 weight 比例再分配。

经典的「上下两块各占一半」写法:

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <View
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:background="#FFC107" />

    <View
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:background="#03A9F4" />

</LinearLayout>

为什么 layout_height 要写 0dp?因为垂直方向时,先把每块按 0dp 算基础高度,剩余空间就是整个父高度,再按 1:1 分配,恰好各占一半。如果写成 wrap_content,系统会先量一遍内容高度,再分配剩余空间,结果会略微偏离 1:1,浪费一次测量。

经验法则:用 weight 分配的那个方向上的尺寸,统一写 0dp,能让测量更高效、比例更精确。

3.3 layout_gravity:在父容器里的对齐

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:gravity="center_vertical">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="top"
        android:text="顶部" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:text="居中" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:text="底部" />

</LinearLayout>

水平 LinearLayout 里,子控件能控制自己在垂直方向上的对齐(top/center_vertical/bottom),水平方向则由排列顺序决定。垂直 LinearLayout 同理,能控制的是水平方向(left/center_horizontal/right)。

3.4 一个完整的登录表单

把上面三个属性综合起来,做一个登录页:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center"
    android:padding="24dp">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="欢迎登录"
        android:textSize="24sp"
        android:textStyle="bold"
        android:layout_marginBottom="32dp" />

    <EditText
        android:id="@+id/etAccount"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="请输入账号"
        android:inputType="text" />

    <EditText
        android:id="@+id/etPassword"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="请输入密码"
        android:inputType="textPassword"
        android:layout_marginTop="12dp" />

    <Button
        android:id="@+id/btnLogin"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="登录"
        android:layout_marginTop="24dp" />

</LinearLayout>

对应的 Activity:

package com.example.layoutdemo;

import android.os.Bundle;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

public class LoginActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);

        EditText etAccount = findViewById(R.id.etAccount);
        EditText etPassword = findViewById(R.id.etPassword);
        Button btnLogin = findViewById(R.id.btnLogin);

        btnLogin.setOnClickListener(v -> {
            String account = etAccount.getText().toString().trim();
            String password = etPassword.getText().toString().trim();
            if (account.isEmpty() || password.isEmpty()) {
                Toast.makeText(this, "账号和密码不能为空", Toast.LENGTH_SHORT).show();
                return;
            }
            // 实际项目这里走网络请求或本地校验
            Toast.makeText(this, "登录成功:" + account, Toast.LENGTH_SHORT).show();
        });
    }
}

4. RelativeLayout:相对位置

LinearLayout 解决不了「A 在 B 右边、C 贴底」这类需求,这时就要请出 RelativeLayout。它的核心是一组「相对某个参照物」的属性:

类型 属性
相对父容器 layout_alignParentTop / layout_alignParentBottom / layout_alignParentStart / layout_alignParentEnd / layout_centerInParent
相对其他控件 layout_above / layout_below / layout_toStartOf / layout_toEndOf / layout_alignTop / layout_alignBottom
边距 layout_marginTop / layout_marginBottom / layout_marginStart / layout_marginEnd

来一个经典布局:标题居中、左右两个按钮贴边、底部一条横线。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="16dp">

    <Button
        android:id="@+id/btnBack"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentStart="true"
        android:layout_alignParentTop="true"
        android:text="返回" />

    <TextView
        android:id="@+id/tvTitle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_alignTop="@id/btnBack"
        android:text="标题"
        android:textSize="18sp" />

    <Button
        android:id="@+id/btnMore"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true"
        android:layout_alignParentTop="true"
        android:text="更多" />

    <View
        android:id="@+id/divider"
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:layout_below="@id/btnBack"
        android:layout_marginTop="12dp"
        android:background="#CCCCCC" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/divider"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="24dp"
        android:text="内容区" />

</RelativeLayout>

写法顺序很重要:被参照的控件(如 btnBack)必须先声明,因为 layout_below="@id/btnBack" 在解析时需要这个 id 已经存在。如果你不想受顺序限制,可以在 id 上加 +:@+id/btnBack,让系统先声明它。但注意,强制的依赖关系不能循环——A 在 B 上方、B 在 A 上方会导致测量崩溃。

5. FrameLayout:层叠

FrameLayout 最简单也最常被低估——它把所有子控件默认堆在左上角同一位置,后写的盖在前面写的上面。layout_gravity 能调整每层的对齐方式。

最常见的用法是做容器骨架(Activity 的 setContentView 默认根就是 FrameLayout)和做覆盖层(引导蒙层、加载中浮层、空状态占位):

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:text="我是底层内容" />

    <ProgressBar
        android:id="@+id/progress"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:visibility="gone" />

</FrameLayout>

切换「加载中」和「内容」时,只要 progress.setVisibility(View.VISIBLE) 就能盖在内容上面。这就是为什么很多空状态、网络错误页都用 FrameLayout 当根。

6. 布局嵌套与性能:层级越浅越好

每个 ViewGroup 在绘制时都要做 onMeasure(测量)→ onLayout(摆放)→ onDraw(绘制),而测量是会传染的:父布局测量一次,会触发所有子控件测量,子控件如果是容器就继续往下传染。所以层级每多一层,最坏情况下的测量次数会指数级增长。

业界经验是 View 树层级控制在 8 层以内,单页 View 数量控制在 80 个以内(不含列表项)。检查层级的工具:

  1. Layout Inspector:Android Studio 的 Tools → Layout Inspector,能可视化整棵树
  2. GPU 呈现模式分析:开发者选项里打开,能看到每帧绘制耗时
  3. Systrace / Perfetto:抓 trace 看具体卡在哪一步

优化方向就一句话:能用一个 ConstraintLayout 解决的,不要用三层嵌套 LinearLayout+RelativeLayout 凑出来。这会在下一章重点讲。

7. include / merge / ViewStub:复用与延迟加载

7.1 include:抽公共布局

<!-- res/layout/item_toolbar.xml -->
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
    <Button
        android:id="@+id/btnBack"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="返回" />
    <TextView
        android:id="@+id/tvTitle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="标题" />
</merge>

在另一个布局里复用:

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal">

    <include
        layout="@layout/item_toolbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</LinearLayout>

<include> 会把目标布局整个塞进来。注意 include 上的 layout_* 属性会覆盖被包含布局根节点的对应属性,可以借此调整外边距。

7.2 merge:消除多余层级

merge 标签不对应任何真实 View,它在 inflate 时会把自己的子元素直接挂到父容器上,从而避免「外层套了一层没用的容器」。

include + merge 是天生一对:被复用的布局如果根节点只是个 LinearLayout 起容器作用,就直接换成 merge,include 进来后子控件直接挂到调用方父容器,少一层。

// 用 merge 时 inflate 必须传父容器,且 attachToRoot 必须为 true
View view = getLayoutInflater()
    .inflate(R.layout.item_toolbar, parent, true);

7.3 ViewStub:按需加载

ViewStub 是一个零尺寸、零绘制的占位符,它持有一个 layout 引用但不解析。等到真正需要时调用 setVisibility(View.VISIBLE) 或 inflate(),它才会把目标布局加载进来,然后把自己从 View 树里替换掉。适合默认不显示、偶尔才显示的引导浮层、错误页:

<ViewStub
    android:id="@+id/stubGuide"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:inflatedId="@+id/guideContent"
    android:layout="@layout/view_guide_overlay" />
ViewStub stub = findViewById(R.id.stubGuide);
View guide = stub.inflate(); // 第一次调用才真正加载
// 之后再 findViewById(R.id.stubGuide) 会返回 null,因为它已被替换
// 应该用 inflatedId 拿到的 guide 或 inflate() 返回的 view 操作

和 visibility=gone 的区别:gone 的 View 已经走完了测量和布局,只是不绘制,对象都在内存里;ViewStub 是连对象都没创建,性能开销真正为零。

常见坑与最佳实践

  1. 忘记写 orientation:LinearLayout 默认 horizontal,子控件横向排不下会被挤到屏幕外或者干脆不显示。养成「LinearLayout 必带 orientation」的习惯。
  2. RelativeLayout 中循环依赖:A layout_below B、B 又 layout_below A,会导致 onMeasure 抛 StackOverflowError。设计时先画草图确定参照链,别让两个控件互相参照。
  3. match_parent 滥用:FrameLayout 里多个子控件都 match_parent 会互相完全盖住,调试时根本看不出谁是谁。开发期临时给每层加个背景色或 padding 帮助区分。
  4. 用 weight 时基础尺寸没归零:垂直方向上 layout_height 写 wrap_content 会让系统多量一次,且比例会有微小偏差。统一写 0dp 既快又准。
  5. 忽视 layoutDirection:left/right 会被 RTL(阿拉伯语等从右向左阅读)语言镜像,但 start/end 不会。国际化项目务必用 start/end 系列,否则在阿拉伯语机型上左右会反。
  6. include 后 id 取不到:被 include 的布局如果根节点也是带 merge 的,子控件 id 直接挂在父容器下,需要用 findViewById(R.id.xxx) 从 Activity 取,而不是从 include 返回的 view 取。

章节小结

  • Android 屏幕上所有元素都是 View,容器是 ViewGroup,整个屏幕是一棵 View 树
  • LinearLayout 用 orientation 和 weight 处理线性排列,weight 分配方向写 0dp
  • RelativeLayout 用一组相对属性处理复杂对齐,依赖链不能成环
  • FrameLayout 把子控件层叠堆放,适合做容器骨架和覆盖层
  • 布局层级越浅越好,超过 8 层要警惕测量性能
  • include 复用、merge 去层、ViewStub 延迟加载,是布局优化的三件套

下一章预告

经典三大布局虽然能用,但稍微复杂的界面就要嵌套三四层,代码冗长、性能也堪忧。下一章我们引入 Android 推荐的现代化布局 ConstraintLayout——一个容器干掉一层嵌套,配合 chains、Guideline、Barrier,能让你写出扁平、灵活、可视化的界面。