布局基础: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 个以内(不含列表项)。检查层级的工具:
- Layout Inspector:Android Studio 的
Tools → Layout Inspector,能可视化整棵树 - GPU 呈现模式分析:开发者选项里打开,能看到每帧绘制耗时
- 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 是连对象都没创建,性能开销真正为零。
常见坑与最佳实践
- 忘记写
orientation:LinearLayout默认 horizontal,子控件横向排不下会被挤到屏幕外或者干脆不显示。养成「LinearLayout必带orientation」的习惯。 - RelativeLayout 中循环依赖:A
layout_belowB、B 又layout_belowA,会导致onMeasure抛StackOverflowError。设计时先画草图确定参照链,别让两个控件互相参照。 match_parent滥用:FrameLayout里多个子控件都match_parent会互相完全盖住,调试时根本看不出谁是谁。开发期临时给每层加个背景色或 padding 帮助区分。- 用
weight时基础尺寸没归零:垂直方向上layout_height写wrap_content会让系统多量一次,且比例会有微小偏差。统一写0dp既快又准。 - 忽视
layoutDirection:left/right会被 RTL(阿拉伯语等从右向左阅读)语言镜像,但start/end不会。国际化项目务必用start/end系列,否则在阿拉伯语机型上左右会反。 include后 id 取不到:被include的布局如果根节点也是带merge的,子控件 id 直接挂在父容器下,需要用findViewById(R.id.xxx)从Activity取,而不是从include返回的 view 取。
章节小结
- Android 屏幕上所有元素都是
View,容器是ViewGroup,整个屏幕是一棵 View 树 LinearLayout用orientation和weight处理线性排列,weight 分配方向写 0dpRelativeLayout用一组相对属性处理复杂对齐,依赖链不能成环FrameLayout把子控件层叠堆放,适合做容器骨架和覆盖层- 布局层级越浅越好,超过 8 层要警惕测量性能
include复用、merge去层、ViewStub延迟加载,是布局优化的三件套
下一章预告
经典三大布局虽然能用,但稍微复杂的界面就要嵌套三四层,代码冗长、性能也堪忧。下一章我们引入 Android 推荐的现代化布局 ConstraintLayout——一个容器干掉一层嵌套,配合 chains、Guideline、Barrier,能让你写出扁平、灵活、可视化的界面。