A
第 6 章JAVA35 分钟

ConstraintLayout 现代布局

用一个 ConstraintLayout 干掉三层嵌套,掌握约束、chains、Guideline、Barrier 的现代布局思路。

学习目标

  • 理解 ConstraintLayout 相对传统布局的优势与设计哲学
  • 熟练编写 top/bottom/start/end 基础约束,能完成居中、贴边、链式排列
  • 掌握 chains 三种模式与 Guideline、Barrier、Group 的使用场景
  • 了解按百分比定位与可视化编辑器的基本工作流

学习目标

  • 理解 ConstraintLayout 为什么是 Android 现代布局的默认选择
  • 能用约束(constraint)表达「A 在 B 右边、距离 16dp」这类位置关系
  • 掌握 chains 的三种模式(spread / packed / spread_inside)和 Guideline、Barrier 的差异
  • 学会用 Group 批量控制可见性、用按百分比约束应对不同屏宽
  • 知道 ConstraintLayout 和 Layout Editor 的协作流程,能从可视化编辑反推 XML

ConstraintLayout 现代布局

1. 为什么需要 ConstraintLayout

上一章我们用 LinearLayout + RelativeLayout 写过一个登录页,看起来还行。但只要再加一点点要求——「密码框上方再插一个『忘记密码』链接,且整组内容在垂直方向居中」——你就得套一层 LinearLayout 装内容、外面再套一层 RelativeLayout 处理底部按钮,层级立刻涨到 3-4 层。

层级深的代价不只是「难看」,是真实的性能损失:每层 ViewGroup 都会触发一次完整的测量,嵌套场景下测量会指数级放大(双子布局 × 多层 = 2^n)。Android 团队调研过,把一个典型表单页压平到一层 ConstraintLayout 后,onMeasure 耗时能降低 30%-80%。

ConstraintLayout 给出的解法是:让任意两个控件之间能直接建立约束,而不需要靠嵌套来表达位置。这样大部分场景都能用一个 ConstraintLayout 作为根容器,整体平铺。

依赖配置(app/build.gradle,AndroidX 版本):

dependencies {
    implementation 'androidx.appcompat:appcompat:1.7.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
}

新建工程默认会带上 ConstraintLayout,但若你的项目是从老模板迁移,记得检查依赖。

2. 基础约束:top/bottom/start/end

约束的本质是「我的某条边,对齐到另一个目标的某条边」。四条边分别对应:

属性 含义
app:layout_constraintTop_toTopOf 我的顶边,对齐到目标的顶边
app:layout_constraintTop_toBottomOf 我的顶边,对齐到目标的底边
app:layout_constraintBottom_toTopOf 我的底边,对齐到目标的顶边
app:layout_constraintBottom_toBottomOf 我的底边,对齐到目标的底边
app:layout_constraintStart_toStartOf 我的左边,对齐到目标的左边
app:layout_constraintStart_toEndOf 我的左边,对齐到目标的右边
app:layout_constraintEnd_toStartOf 我的右边,对齐到目标的左边
app:layout_constraintEnd_toEndOf 我的右边,对齐到目标的右边

特殊目标值 parent 表示对齐到父容器。来个完整例子:

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

    <Button
        android:id="@+id/btnA"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="A 居中"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btnB"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="B 在 A 下方"
        android:layout_marginTop="16dp"
        app:layout_constraintStart_toStartOf="@id/btnA"
        app:layout_constraintTop_toBottomOf="@id/btnA" />

</androidx.constraintlayout.widget.ConstraintLayout>

居中的原理:当 A 的 start 和 end 都约束到 parent 的对应边时,两个「拉力」相互抵消,A 就居中。同理 top/bottom 都约束就垂直居中。这和 RelativeLayout 的 centerInParent 不一样,是「两边都拉」的结果。

2.1 偏移:bias

两边都约束时默认是 50:50,可以用 bias 改成任意比例:

<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="靠左 30%"
    app:layout_constraintHorizontal_bias="0.3"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

bias 取值 0-1,0 是最左/最上,1 是最右/最下。默认水平 bias 是 0.5。

2.2 边距与 GONE 的特殊处理

边距用 android:layout_margin*,和普通布局一致。但 ConstraintLayout 提供了一套 goneMargin*:当约束目标变成 View.GONE 时,当前控件用 goneMargin 代替原本的 margin。

这个特性的典型场景是:标题旁边有个可选的「×」关闭按钮,按钮隐藏后标题需要往右挪一些保持视觉平衡。用 goneMarginStart 就能让标题在按钮 gone 时自动获得额外边距:

<TextView
    android:id="@+id/tvTitle"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_marginStart="8dp"
    android:goneMarginStart="48dp"
    app:layout_constraintStart_toEndOf="@id/btnClose"
    app:layout_constraintEnd_toEndOf="parent"
    android:text="标题" />

<Button
    android:id="@+id/btnClose"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="×"
    android:visibility="gone" />

3. chains:链式排列

chain 是 ConstraintLayout 的「LinearLayout 替身」。当多个控件相互约束成一条链(A.start→B.end、B.start→C.end、首尾再约束到 parent),最左的那个叫 chain head,决定整条链的行为模式。

三种模式:

模式 表现
spread(默认) 均匀分布,控件之间有间距,两端贴约束边
spread_inside 两端贴约束边,中间均匀分布
packed 所有控件挤到一起,整组居中

链头设置模式:

<Button
    android:id="@+id/btn1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="1"
    app:layout_constraintHorizontal_chainStyle="packed"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toStartOf="@id/btn2"
    app:layout_constraintTop_toTopOf="parent" />

<Button
    android:id="@+id/btn2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="2"
    app:layout_constraintStart_toEndOf="@id/btn1"
    app:layout_constraintEnd_toStartOf="@id/btn3"
    app:layout_constraintTop_toTopOf="parent" />

<Button
    android:id="@+id/btn3"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="3"
    app:layout_constraintStart_toEndOf="@id/btn2"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

链内支持 weight(app:layout_constraintHorizontal_weight),效果和 LinearLayout 一致,但前提是被分配方向尺寸写 0dp:

<Button
    android:id="@+id/btnLeft"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:text="占 1 份"
    app:layout_constraintHorizontal_weight="1"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toStartOf="@id/btnRight"
    app:layout_constraintTop_toTopOf="parent" />

<Button
    android:id="@+id/btnRight"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:text="占 2 份"
    app:layout_constraintHorizontal_weight="2"
    app:layout_constraintStart_toEndOf="@id/btnLeft"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

4. Guideline:辅助线

Guideline 是一条看不见的辅助线,本身不绘制,只用来给其他控件做参照。它有三种定位方式:

<androidx.constraintlayout.widget.Guideline
    android:id="@+id/glLeft"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    app:layout_constraintGuide_begin="72dp" />   <!-- 距左边 72dp -->

<androidx.constraintlayout.widget.Guideline
    android:id="@+id/glRight"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    app:layout_constraintGuide_end="72dp" />      <!-- 距右边 72dp -->

<androidx.constraintlayout.widget.Guideline
    android:id="@+id/glMid"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    app:layout_constraintGuide_percent="0.5" />  <!-- 屏幕正中 -->

然后所有控件都约束到这些 Guideline,整页的左右内边距一改全改,比每个控件单独写 marginStart 维护成本低得多:

<TextView
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:text="内容"
    app:layout_constraintStart_toStartOf="@id/glLeft"
    app:layout_constraintEnd_toStartOf="@id/glRight"
    app:layout_constraintTop_toTopOf="parent" />

经验法则:任何「多控件都要对齐的边」都先建一条 Guideline。

5. Barrier:根据内容自适应的「动态 Guideline」

Guideline 是固定位置的,而 Barrier 的位置由一组参照控件决定——它会「贴」在这组控件最右(或最左、最上、最下)的那一个的边上。

典型场景:左边是头像和昵称两个控件(昵称可能很长),右边是详情。希望右边详情永远贴着「左边两个控件中更宽的那个」的右边。如果用固定 Guideline,昵称一变长就被截断;用 Barrier 就能动态调整。

<androidx.constraintlayout.widget.Barrier
    android:id="@+id/barrier"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:barrierDirection="end"
    app:constraint_referenced_ids="ivAvatar,tvName" />

<ImageView
    android:id="@+id/ivAvatar"
    android:layout_width="48dp"
    android:layout_height="48dp"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    android:src="@mipmap/ic_launcher" />

<TextView
    android:id="@+id/tvName"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="一个很长的昵称"
    app:layout_constraintTop_toBottomOf="@id/ivAvatar"
    app:layout_constraintStart_toStartOf="parent" />

<TextView
    android:id="@+id/tvDetail"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:text="详情贴在 Barrier 右边"
    android:layout_marginStart="16dp"
    app:layout_constraintStart_toEndOf="@id/barrier"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

constraint_referenced_ids 接受逗号分隔的多个 id。barrierDirection 取 start / end / top / bottom。

6. Group:批量控制可见性

Group 把一组控件捆绑在一起,改 Group 的可见性就能改这一组,省去一个个 setVisibility 的麻烦:

<androidx.constraintlayout.widget.Group
    android:id="@+id/groupLogin"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:visibility="visible"
    app:constraint_referenced_ids="etAccount,etPassword,btnLogin" />
Group group = findViewById(R.id.groupLogin);
group.setVisibility(View.GONE);  // 同时隐藏三个控件

常用于「登录态/未登录态」切换整个内容块。

7. 按百分比定位

ConstraintLayout 还支持用 0dp + layout_constraintWidth_percent 来按比例设宽,常用于响应式布局:

<Button
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    app:layout_constraintWidth_percent="0.5"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    android:text="占父宽一半" />

启用按百分比尺寸时,需要把 layout_width 写成 0dp(约束式),系统会按 percent 计算实际宽度。还可以叠加 app:layout_constraintWidth_min / max 做上下限保护。

8. 一个综合示例:仿登录页

把上面所有概念用一遍,做一个登录页:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/glStart"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintGuide_begin="24dp" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/glEnd"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintGuide_end="24dp" />

    <TextView
        android:id="@+id/tvTitle"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="64dp"
        android:text="欢迎登录"
        android:textSize="28sp"
        android:textStyle="bold"
        app:layout_constraintStart_toStartOf="@id/glStart"
        app:layout_constraintEnd_toStartOf="@id/glEnd"
        app:layout_constraintTop_toTopOf="parent" />

    <EditText
        android:id="@+id/etAccount"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="32dp"
        android:hint="请输入账号"
        android:inputType="text"
        app:layout_constraintStart_toStartOf="@id/glStart"
        app:layout_constraintEnd_toStartOf="@id/glEnd"
        app:layout_constraintTop_toBottomOf="@id/tvTitle" />

    <EditText
        android:id="@+id/etPassword"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="12dp"
        android:hint="请输入密码"
        android:inputType="textPassword"
        app:layout_constraintStart_toStartOf="@id/glStart"
        app:layout_constraintEnd_toStartOf="@id/glEnd"
        app:layout_constraintTop_toBottomOf="@id/etAccount" />

    <Button
        android:id="@+id/btnLogin"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="24dp"
        android:text="登录"
        app:layout_constraintStart_toStartOf="@id/glStart"
        app:layout_constraintEnd_toStartOf="@id/glEnd"
        app:layout_constraintTop_toBottomOf="@id/etPassword" />

</androidx.constraintlayout.widget.ConstraintLayout>

对应的 Activity(和上一章几乎一致,只换了布局):

package com.example.constraintdemo;

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();
        });
    }
}

整页只有一个 ConstraintLayout 根容器,没有任何嵌套——这就是它最大的价值。

9. 与 Layout Editor 的协作

Android Studio 内置的 Layout Editor 对 ConstraintLayout 支持最好。常见工作流:

  1. 拖拽约束:在 Design 视图选中控件,四角的圆点拖到目标边或 Guideline,自动生成 layout_constraintXxx 属性
  2. 自动生成约束:Tools → Layout → Auto Connect All 会让 IDE 根据当前位置猜约束并写回 XML
  3. 清理约束:Tools → Layout → Clear All Constraints 删除当前选中所有约束,方便重排
  4. ** inference**:菜单里的「Infer Constraints」会按当前控件的视觉位置自动推一组约束,对老项目迁移很有用

但别完全依赖自动生成:IDE 推断的约束经常会有冗余(比如已经能由其他约束推出的还会再加一条)。最终交付前过一遍 XML,把冗余约束删掉,保持简洁。

常见坑与最佳实践

  1. 约束不全导致控件跑到 (0,0):ConstraintLayout 默认从左上角 (0,0) 开始摆放。如果某控件只约束了水平方向、没约束垂直方向,它就会跑到顶部。每个控件至少要有水平和垂直各一组约束,否则就当默认没约束。
  2. match_parent 不推荐用:在 ConstraintLayout 里,子控件写 match_parent 会让该方向失去约束意义(因为它「撑满」)。要表达「撑满父容器」,应该把 start 和 end 都约束到 parent,再把 layout_width 写成 0dp(约束式)。
  3. chain 模式设置位置不对:chainStyle 必须写在链头(最左/最上的那个)。写在中间或末尾会被忽略。
  4. Barrier 不生效:constraint_referenced_ids 里 id 写错或多加了空格,Barrier 不会报错但会跑到 (0,0)。调试时切到 Layout Inspector 看 Barrier 实际位置。
  5. Group 不能改其他属性:Group 只接管 visibility,不能批量改 alpha、translationX 等。需要批量动画时考虑用代码遍历或 ConstraintSet。

章节小结

  • ConstraintLayout 让任意两个控件直接建立约束,把多层嵌套压平到一个根容器
  • 基础约束是「我的边对齐到目标的边」,两边都约束时通过 bias 调整偏移
  • chains 三种模式对应 LinearLayout 的不同排列风格,weight 同样需要尺寸写 0dp
  • Guideline 是固定辅助线,Barrier 是动态辅助线,Group 是批量可见性开关
  • 按百分比约束 + Guideline 是应对不同屏宽的轻量方案
  • Layout Editor 能反推 XML,但最终要人工审查去冗余

下一章预告

布局是骨架,控件才是血肉。下一章我们走进 Android 最常用的几个基础控件——TextView 显示文字、Button 响应点击、EditText 接收输入、ImageView 显示图片,并实战 selector 与 shape drawable,让按钮和文字不再「长得跟系统默认一样」。