概述
ConstraintLayout 是 Android 官方推出的布局管理器,通过**约束(Constraint)**描述每个 View 的位置关系,实现扁平化布局结构,解决传统布局(LinearLayout、RelativeLayout)的嵌套性能问题。
设计目标
- 减少布局嵌套:降低视图层次结构的复杂性
- 提升性能:减少测量和布局的复杂度
- 灵活的定位系统:支持复杂的响应式布局
- 可视化编辑友好:Android Studio 布局编辑器的主要支持布局
核心思想
每个 View 有 4 条边和一个基线:
top
+-----------+
start | | end
(left) | baseline | (right)
|-----------+
+-----------+
bottom
基本规则:每个 View 至少需要一个水平约束 + 一个垂直约束,否则运行时会跑到 (0,0)。
约束目标可以是:
- Parent:父容器的边缘
- Other View:其他视图的边缘
- Guideline:辅助线
- Barrier:屏障
基础位置约束
1. 约束语法
app:layout_constraint[自己的边]_to[对方的边]Of="[目标ID或parent]"
2. 完整约束方向
<!-- 水平方向 -->
app:layout_constraintStart_toStartOf <!-- 左边对齐左边 -->
app:layout_constraintStart_toEndOf <!-- 左边挨着右边(在它右侧) -->
app:layout_constraintEnd_toStartOf <!-- 右边挨着左边(在它左侧) -->
app:layout_constraintEnd_toEndOf <!-- 右边对齐右边 -->
<!-- 垂直方向 -->
app:layout_constraintTop_toTopOf <!-- 顶部对齐 -->
app:layout_constraintTop_toBottomOf <!-- 在它下方 -->
app:layout_constraintBottom_toTopOf <!-- 在它上方 -->
app:layout_constraintBottom_toBottomOf <!-- 底部对齐 -->
<!-- 基线对齐(文字对齐很有用) -->
app:layout_constraintBaseline_toBaselineOf3. 相对于 parent
<!-- 四边都约束到 parent → 居中 -->
<View
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />+--------------------+
| |
| [View] |
| |
+--------------------+
4. 相对于其他 View
<!-- A 左上角 -->
<TextView android:id="@+id/A"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- B 在 A 右边,顶部对齐 -->
<TextView android:id="@+id/B"
app:layout_constraintStart_toEndOf="@id/A"
app:layout_constraintTop_toTopOf="@id/A" />
<!-- C 在 A 下面,左边对齐 -->
<TextView android:id="@+id/C"
app:layout_constraintStart_toStartOf="@id/A"
app:layout_constraintTop_toBottomOf="@id/A" />+----------------------+
| [A] [B] |
| [C] |
+----------------------+
尺寸控制
1. 三种宽高模式
| 值 | 含义 | 说明 |
|---|---|---|
固定值(如 100dp) | 固定尺寸 | 与常规相同 |
wrap_content | 包裹内容 | 与常规相同 |
0dp | 匹配约束(MATCH_CONSTRAINT) | 根据约束自动计算尺寸 |
重点:0dp 不是”0像素”,而是”让约束来决定宽度”:
<!-- 填满整个父布局宽度 -->
<TextView
android:layout_width="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<!-- 从 A 右边到父布局右边 -->
<TextView
android:layout_width="0dp"
app:layout_constraintStart_toEndOf="@id/A"
app:layout_constraintEnd_toEndOf="parent" />注意:不推荐在 ConstraintLayout 中使用
match_parent,用0dp+ 约束代替。
2. 宽高比(Aspect Ratio)
<ImageView
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="16:9"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- 宽度填满,高度按 16:9 自动计算 -->指定以哪边为基准:
app:layout_constraintDimensionRatio="H,16:9" <!-- 以宽度为基准,计算高度 -->
app:layout_constraintDimensionRatio="W,16:9" <!-- 以高度为基准,计算宽度 -->3. 最大/最小尺寸与百分比
android:layout_width="0dp"
app:layout_constraintWidth_min="100dp" <!-- 最小宽度 -->
app:layout_constraintWidth_max="300dp" <!-- 最大宽度 -->
app:layout_constraintHeight_max="300dp" <!-- 最大高度 -->
<!-- 百分比 -->
app:layout_constraintWidth_percent="0.5" <!-- 占 parent 宽度的 50% -->
app:layout_constraintHeight_percent="0.6" <!-- 占 parent 高度的 60% -->4. 限制 wrap_content
wrap_content 默认内容多大就多大,可能超出约束范围。加 constrainedWidth 限制:
android:layout_width="wrap_content"
app:layout_constrainedWidth="true" <!-- 内容再长也不超出约束范围 -->Bias(偏移)
当一个 View 两侧都有约束时,默认居中(bias=0.5)。用 bias 调整位置,取值 0~1:
app:layout_constraintHorizontal_bias="0.3" <!-- 水平方向,0=最左,1=最右 -->
app:layout_constraintVertical_bias="0.2" <!-- 垂直方向,0=最上,1=最下 -->bias=0 bias=0.3 bias=0.5 bias=1
[V]........ ...[V]..... .....[V].... .........[V]
示例:
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.3" /> <!-- 水平位置偏向左边30% -->Margin 和 Gone Margin
1. 普通 margin
和其他布局一样,但 margin 只在有约束的方向上生效。没有 constraintEnd 就设 marginEnd 是无效的。
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp"2. Gone Margin(ConstraintLayout 独有特性)
当约束目标变成 GONE 时使用的 margin:
app:layout_goneMarginStart="0dp"
app:layout_goneMarginTop="0dp"
app:layout_goneMarginEnd="0dp"
app:layout_goneMarginBottom="0dp"场景:A 在 B 左边,间距 16dp。当 A 隐藏后,B 不希望左边还空 16dp:
<TextView android:id="@+id/B"
app:layout_constraintStart_toEndOf="@id/A"
android:layout_marginStart="16dp"
app:layout_goneMarginStart="0dp" />A 可见: [A]--16dp--[B]
A gone: [B] <- margin 变成 0
Chain(链)
1. 什么是 Chain
Chain 是 ConstraintLayout 中两个或多个 View 互相约束形成的一组关系。
2. 形成条件
关键:元素之间双向约束 + 首尾约束到 parent,形成闭环:
<A app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/B" /> <!-- A 指向 B -->
<B app:layout_constraintStart_toEndOf="@id/A" <!-- B 指向 A(双向) -->
app:layout_constraintEnd_toStartOf="@id/C" />
<C app:layout_constraintStart_toEndOf="@id/B"
app:layout_constraintEnd_toEndOf="parent" />parent <-> [A] <-> [B] <-> [C] <-> parent
^ 双向约束 ^
如果只有单向约束,就不是 chain:
<!-- 这不是 chain,B 只是跟在 A 后面 -->
<TextView android:id="@+id/A"
app:layout_constraintStart_toStartOf="parent" />
<TextView android:id="@+id/B"
app:layout_constraintStart_toEndOf="@id/A" />
<!-- 没有 End 约束,也没有 A 指向 B 的约束 -->parent -> [A] [B] <- 单向,不是 chain,自然左对齐
3. 三种 chainStyle
当 chain 形成后,系统要决定多余的空间怎么分配。chainStyle 就是控制这个的,设在 chain 的第一个元素上:
spread(默认):空间均分
+----------------------------+
| [A] [B] [C] |
+----------------------------+
spread_inside:两端贴边,中间均分
+----------------------------+
|[A] [B] [C] |
+----------------------------+
packed:全部挤在一起
+----------------------------+
| [A][B][C] |
+----------------------------+
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintHorizontal_chainStyle="packed"
<!-- 垂直方向同理 -->
app:layout_constraintVertical_chainStyle="packed"4. packed + bias 实现左对齐
packed 模式下,元素挤在一起后,用 bias 控制这一坨放在哪:
<!-- 第一个元素上加这两行 -->
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintHorizontal_bias="0"bias=0 -> 靠左 |[A][B][C] |
bias=0.5 -> 居中 | [A][B][C] |
bias=1 -> 靠右 | [A][B][C]|
5. 权重(配合 chain 使用)
类似 LinearLayout 的 weight,需要宽度设为 0dp:
<A android:layout_width="0dp"
app:layout_constraintHorizontal_weight="1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/B" />
<B android:layout_width="0dp"
app:layout_constraintHorizontal_weight="2"
app:layout_constraintStart_toEndOf="@id/A"
app:layout_constraintEnd_toStartOf="@id/C" />
<C android:layout_width="0dp"
app:layout_constraintHorizontal_weight="1"
app:layout_constraintStart_toEndOf="@id/B"
app:layout_constraintEnd_toEndOf="parent" />+----------------------------+
| [ A ] [ B ] [ C ] | 1:2:1 分配
+----------------------------+
6. 完整的 chain 示例
<!-- 三个按钮组成水平链 -->
<Button
android:id="@+id/btn1"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/btn2"
app:layout_constraintHorizontal_chainStyle="spread" />
<Button
android:id="@+id/btn2"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toEndOf="@id/btn1"
app:layout_constraintEnd_toStartOf="@id/btn3" />
<Button
android:id="@+id/btn3"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toEndOf="@id/btn2"
app:layout_constraintEnd_toEndOf="parent" />圆形定位(Circular Positioning)
可以以另一个 View 为圆心,指定角度和半径来定位:
<Button android:id="@+id/center" ... />
<Button
app:layout_constraintCircle="@id/center"
app:layout_constraintCircleRadius="100dp"
app:layout_constraintCircleAngle="45" />
<!-- 以 center 为圆心,半径 100dp,角度 45 度(12点方向为0度,顺时针) -->辅助工具
1. Guideline(辅助线)
不可见的参考线,用于对齐。不会显示在界面上,只作为约束目标。
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.3" />定位方式三选一:
app:layout_constraintGuide_percent="0.3" <!-- 百分比定位,在父容器30%处 -->
app:layout_constraintGuide_begin="100dp" <!-- 距 start/top 的固定距离 -->
app:layout_constraintGuide_end="50dp" <!-- 距 end/bottom 的固定距离 -->+---------------------+
| | 30%
|- - - guideline - - -|
| |
| | 70%
+---------------------+
其他 View 约束到它:
<Button
app:layout_constraintTop_toBottomOf="@id/guideline"
app:layout_constraintStart_toStartOf="@id/guideline" />2. Barrier(屏障)
根据多个 View 的实际边界动态生成的参考线。不管哪个 View 更大,barrier 都在最大的那个边界上:
<Button android:id="@+id/button1" ... />
<Button android:id="@+id/button2" ... />
<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="button1,button2" />
<TextView
app:layout_constraintStart_toEndOf="@id/barrier" />+-------------------------------+
| [名字] | [详情] |
| [手机号码很长] | [详情] |
| barrier |
+-------------------------------+
不管”名字”还是”手机号码”谁更长,barrier 都在最长的那个右边,后面的 View 约束到 barrier 就不会重叠。
barrierDirection 取值:
start— 取最左end— 取最右top— 取最上bottom— 取最下
3. Group(组)
批量控制一组 View 的可见性:
<androidx.constraintlayout.widget.Group
android:id="@+id/group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="view1,view2,view3" />代码中一行搞定:
group.visibility = View.GONE // view1, view2, view3 全部隐藏4. Flow(流式布局)
自动排列 + 换行,替代 FlexboxLayout:
<androidx.constraintlayout.helper.widget.Flow
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:flow_wrapMode="chain"
app:flow_horizontalStyle="packed"
app:flow_horizontalBias="0"
app:flow_horizontalGap="8dp"
app:flow_verticalGap="8dp"
app:constraint_referenced_ids="tag1,tag2,tag3,tag4,tag5" />+---------------------------+
| [tag1] [tag2] [tag3] | <- 放不下自动换行
| [tag4] [tag5] |
+---------------------------+
wrapMode 取值:
| 模式 | 说明 |
|---|---|
none | 不换行,超出也不管 |
chain | 换行,每行是一个 chain |
aligned | 换行,网格对齐 |
5. Layer(层)
对多个 View 做整体变换(旋转、缩放、平移):
<androidx.constraintlayout.helper.widget.Layer
android:id="@+id/layer"
app:constraint_referenced_ids="view1,view2,view3" />layer.rotation = 45f // 整组旋转 45 度
layer.scaleX = 1.5f // 整组放大 1.5 倍
layer.translationY = 20f // 整组下移 20px属性速查表
| 类别 | 属性 | 说明 |
|---|---|---|
| 位置 | constraintStart_toStartOf | 左对左 |
constraintStart_toEndOf | 左接右 | |
constraintEnd_toStartOf | 右接左 | |
constraintEnd_toEndOf | 右对右 | |
constraintTop_toTopOf | 顶对顶 | |
constraintTop_toBottomOf | 顶接底 | |
constraintBottom_toTopOf | 底接顶 | |
constraintBottom_toBottomOf | 底对底 | |
constraintBaseline_toBaselineOf | 基线对齐 | |
| Bias | constraintHorizontal_bias | 水平偏移 0~1 |
constraintVertical_bias | 垂直偏移 0~1 | |
| 尺寸 | layout_width="0dp" | 匹配约束 |
constraintWidth_percent | 百分比宽度 | |
constraintHeight_percent | 百分比高度 | |
constraintWidth_min/max | 最小/最大宽度 | |
constraintHeight_min/max | 最小/最大高度 | |
constraintDimensionRatio | 宽高比 | |
constrainedWidth | 限制 wrap_content 不超出约束 | |
constrainedHeight | 限制 wrap_content 不超出约束 | |
| Chain | constraintHorizontal_chainStyle | 链样式(spread/spread_inside/packed) |
constraintVertical_chainStyle | 垂直链样式 | |
constraintHorizontal_weight | 链内水平权重 | |
constraintVertical_weight | 链内垂直权重 | |
| Gone | layout_goneMarginStart | 目标 gone 时的左 margin |
layout_goneMarginEnd | 目标 gone 时的右 margin | |
layout_goneMarginTop | 目标 gone 时的上 margin | |
layout_goneMarginBottom | 目标 gone 时的下 margin | |
| 圆形 | constraintCircle | 圆形定位目标 |
constraintCircleRadius | 圆形半径 | |
constraintCircleAngle | 圆形角度(0=12点,顺时针) | |
| Guideline | layout_constraintGuide_percent | 百分比定位 |
layout_constraintGuide_begin | 距 start/top 固定距离 | |
layout_constraintGuide_end | 距 end/bottom 固定距离 | |
| Barrier | barrierDirection | 屏障方向(start/end/top/bottom) |
constraint_referenced_ids | 引用的 View ID 列表 |
实战常见模式
1. 左图右文,文字占剩余空间
<ImageView android:id="@+id/icon"
android:layout_width="48dp"
android:layout_height="48dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toEndOf="@id/icon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/icon"
android:layout_marginStart="12dp" />2. 等分 3 列
<!-- 三个 View 形成 chain,宽度都为 0dp,自动均分 -->
<View android:id="@+id/c1" android:layout_width="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/c2" />
<View android:id="@+id/c2" android:layout_width="0dp"
app:layout_constraintStart_toEndOf="@id/c1"
app:layout_constraintEnd_toStartOf="@id/c3" />
<View android:id="@+id/c3" android:layout_width="0dp"
app:layout_constraintStart_toEndOf="@id/c2"
app:layout_constraintEnd_toEndOf="parent" />3. 底部固定按钮 + 上方列表填满
<RecyclerView
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/btn"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<Button android:id="@+id/btn"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />4. 适配不同屏幕(权重分配)
<Button android:id="@+id/btn1"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintHorizontal_weight="1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/btn2" />
<Button android:id="@+id/btn2"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintHorizontal_weight="2"
app:layout_constraintStart_toEndOf="@id/btn1"
app:layout_constraintEnd_toEndOf="parent" />
<!-- btn2 占 btn1 两倍宽度 -->5. 登录界面布局
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 标题 -->
<TextView android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="64dp" />
<!-- 用户名输入框 -->
<EditText android:id="@+id/username"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_margin="24dp" />
<!-- 密码输入框 -->
<EditText android:id="@+id/password"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/username"
app:layout_constraintStart_toStartOf="@id/username"
app:layout_constraintEnd_toEndOf="@id/username" />
<!-- 登录按钮 -->
<Button android:id="@+id/login_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/password"
app:layout_constraintStart_toStartOf="@id/password"
app:layout_constraintEnd_toEndOf="@id/password"
android:layout_marginTop="32dp" />
<!-- 底部链接 -->
<TextView android:id="@+id/forgot_password"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginBottom="32dp" />
</androidx.constraintlayout.widget.ConstraintLayout>性能最佳实践
- 避免嵌套:所有视图直接放在 ConstraintLayout 中,不要再嵌套 LinearLayout 等
- 合理使用 0dp:需要匹配约束时用
0dp,固定尺寸用具体数值 - 不用 match_parent:用
0dp+ 两侧约束代替 - 使用
<include>重用布局组件
常见问题解决
1. 视图重叠
- 检查约束是否冲突
- 确保使用了正确的约束关系
- 使用
android:layout_margin添加间距
2. 视图不可见
- 检查约束是否完整(水平和垂直各至少一个)
- 确认视图没有被其他视图覆盖
- 检查 visibility 设置
3. 元素分散(想要左对齐)
- 检查是否形成了 chain(双向约束),默认 chain 是 spread 模式
- 方案一:chain 第一个元素加
chainStyle="packed"+bias="0" - 方案二:去掉双向约束,只保留单向的
Start_toEndOf,打破 chain
4. 布局错乱
- 确保所有视图都有足够的约束
- 避免混合使用
match_parent和0dp - 检查边距设置
调试技巧
- 在布局编辑器中点击 “Show Constraints” 按钮显示约束
- 启用 “Autoconnect” 或 “Infer Constraints” 快速创建约束
- 确保每个视图都有足够的约束(水平和垂直各至少一个)