Constrainlayout使用解析

666 阅读36分钟

居中

让一个控件居中于⽗容器

app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"

居中于控件中⼼

⽔平⽅向居中

app:layout_constraintStart_toStartOf="@id/view"
app:layout_constraintEnd_toEndOf="@id/view

垂直⽅向居中

app:layout_constraintTop_toTopOf="@id/view"
app:layout_constraintBottom_toBottomOf="@id/view"

居中于控件的边

例如这样的效果,猫头在上边图片下边的中间:

image.png

app:layout_constraintTop_toBottomOf="@id/view"
app:layout_constraintBottom_toBottomOf="@id/view"

填充

⽔平⽅向填充⽗容器(通过 match_constraint )

app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_width="0dp"

上边的代码就很适合下边的逻辑,比如我们需要将下边图片中的按钮变成和 ImageView 的高度一样大的话,就可以使用match_constraint或者是使用android:layout_height="0dp"他们的意思都表示铺满约束的意思。

image.png

改完之后就变成了这样:

image.png

如果使用 match_parent,这个是铺满父布局,不一定会有这样的效果。

权重

为⽔平⽅向的控件设置权重,⼤⼩为 2:1:1

<!-- (view-1) -->

android:layout_width="0dp"
app:layout_constraintHorizontal_weight="2"

<!-- (view-2) -->

android:layout_width="0dp"
app:layout_constraintHorizontal_weight="1"

<!-- (view-3) -->

android:layout_width="0dp"
app:layout_constraintHorizontal_weight="1"

⽂字基准线对⻬

例如这样的效果,即便两个字体的大小99和%不一致,也可以实现效果:

image.png

app:layout_constraintBaseline_toBaselineOf

直接使用底部对齐是无法实现的:

image.png

圆形(⻆度)定位

通过「圆⼼」「⻆度」「半径」设置圆形定位

app:layout_constraintCircle="@id/view"
app:layout_constraintCircleAngle="90"
app:layout_constraintCircleRadius="180dp"

可以实现类似于这样的效果:

image.png

约束限制

限制控件⼤⼩不会超过约束范围。

限制宽度

app:layout_constrainedWidth="true"

限制高度

app:layout_constrainedHeight="true

image.png

例如这个图中,我们可以通过限制文本的宽度来实现其显示长度不超过图片的宽度,完整源码:

<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:ignore="HardcodedText">

    <ImageView
        android:id="@+id/avatar"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:layout_marginTop="40dp"
        android:src="@mipmap/ic_launcher_round"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="12dp"
        android:background="@color/colorPrimary"
        android:text="长文本长文本长文本长文本长文本文本长文本长文本"
        android:textColor="@android:color/white"
        app:layout_constrainedWidth="true"
        app:layout_constraintEnd_toEndOf="@+id/avatar"
        app:layout_constraintHorizontal_bias="0.508"
        app:layout_constraintStart_toStartOf="@+id/avatar"
        app:layout_constraintTop_toBottomOf="@+id/avatar" />

</androidx.constraintlayout.widget.ConstraintLayout>

偏向 (bias)

控制控件在垂直⽅向的0.0~1.0的位置的位置,让一个控件出现在指定方向的百分比位置上。这个 bias(偏向)属性,是“在它约束的范围内”的百分比位置——不是整个屏幕,而是“由你的 top/bottom 约束确定的那一段区间”。水平方向当然就是“start/end”的区间了。

例如:控制控件在垂直⽅向的 30%的位置:

app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintVertical_bias="0.3

image.png

该值默认值是0.5,同时除了Vertical外还有layout_constraintHorizontal_bias类型的。

垂直⽅向居顶部

app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constrainedHeight="true"
app:layout_constraintVertical_bias="0.0"

layout_goneMarginStart

当使用这个属性的控件所依赖的控件GONE掉之后,这个属性可以增加一个默认的边距

使用示例:

<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:ignore="HardcodedText">

    <TextView
        android:id="@+id/textview"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@color/colorPrimary"
        android:text="长文本长文本"
        android:textColor="@android:color/white"
        android:textSize="28sp"
        android:visibility="gone"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ImageView
        app:layout_goneMarginStart="16dp"
        android:id="@+id/avatar"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:src="@mipmap/ic_launcher_round"
        app:layout_constraintStart_toEndOf="@id/textview"
        app:layout_constraintTop_toTopOf="@id/textview" />
</androidx.constraintlayout.widget.ConstraintLayout>

约束链

在约束链上的第⼀个控件上加上 chainStyle ,⽤来改变⼀组控件的布局⽅式。只有使用在第一个水平或者垂直的控件上边才会有效果。

三种 chainStyle 含义

  • spread(扩散):
    控件均匀分布在链的两端,各自间隔平均

image.png

  • spread_inside(内部扩散):
    首尾两个控件贴边,中间控件均匀分布在链中间

image.png

  • packed(打包):
    全部控件紧紧贴在一起,整体可以左右/上下偏移(bias)

image.png

垂直⽅向 packed:

app:layout_constraintVertical_chainStyle="packed

典型宽高比写法

假设你要让一个 ImageView 保持 16:9 宽高比,且要自动撑满屏幕宽度,高度自适应

<ImageView
    android:id="@+id/iv"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintDimensionRatio="16:9"
/>

解释:

  • layout_width="0dp":宽度由约束决定(就是 match constraints),代表让宽度自动拉满两侧
  • layout_constraintDimensionRatio="16:9":宽高比 16:9。
  • layout_height="wrap_content":系统会根据宽度和比例自动算出高度。

反过来,如果你想让高度撑满父容器,宽度自适应:

<ImageView
    android:layout_width="wrap_content"
    android:layout_height="0dp"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintDimensionRatio="16:9"
/>
  • layout_height="0dp":高度被约束拉满,宽度系统帮你算出来。

一些容易踩坑的点

  1. 只有 match_constraints(0dp)那一边才会根据 ratio 参与计算
    你随便写 wrap_contentmatch_parent,系统就直接测量内容或父布局,不用 ratio。
  2. 配合其它约束一起用(比如左右/上下都贴 parent),ratio 才会生效。
  3. 宽高都 0dp 也可以,但要有其它辅助约束来给最终确定形状

正确原理:

  • wrap_content 或 固定值
    这一边的尺寸是“已知的”,就是系统能直接测量(内容多大就多大,或者就是写死的数值)。
  • 0dp(match constraints)
    这一边是“未知的”,会根据比例(ratio)+ 已知那一边的尺寸,算出来

Ratio 的进阶写法

  • app:layout_constraintDimensionRatio="H,2:1"
    让“高度 = 2 × 宽度”,并且用高度来反推宽度(H = height based)。
  • W,3:4"
    用宽度为主导,宽:高 = 3:4。

其实大部分时候写 "16:9""1:1" 就够用。

为什么要设置 0dp?

  • 在 ConstraintLayout 中,使用 wrap_content 或者固定值(如 100dp)就意味着这个尺寸是已经“定死”或由内容大小决定的,没有可伸缩的余地
  • match_constraints(也就是设置为 0dp)表示这个边可以被“约束规则”所支配,它本身不主动决定大小。因此,如果想通过某个规则(例如长宽比 ratio)去计算出宽/高,就必须让它处于可计算、可被约束的状态,这时候设置为 0dp 就是关键。
<ImageView
    android:layout_width="0dp"            <!-- match_constraints也可以 -->
    android:layout_height="wrap_content"
    app:layout_constraintDimensionRatio="16:9"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent" />
  • 这里 android:layout_width="0dp" 表示宽度由 ConstraintLayout 的约束来决定(即 match constraints)。
  • app:layout_constraintDimensionRatio="16:9" 表示宽高比为 16:9。
  • 当运行时,会先根据高度计算出宽度,或者根据宽度计算出高度,具体计算方式和约束条件有关,一般会取决于其他约束以及哪边是 0dp(match constraints)。

注意:如果我们想让“宽度”基于“高度”来计算,需要把“高度”设置为 match constraints 并且“宽度”可以是 wrap_content 或 match constraints,反之亦然。

在 ConstraintLayout 的 1.1.0+ 版本中,可以用前缀 W,H, 来显式指定谁是参考边。

  • W 代表 width

    • "W,16:9" 表示「宽:高 = 16:9」
  • H 代表 height

    • "H,16:9" 表示「高:宽 = 16:9」

比如:

<ImageView
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    app:layout_constraintDimensionRatio="W,16:9"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent" />

这里等同于前面用法中 "16:9",因为默认其实就是宽:高。

注意事项

  • 至少有一个维度(宽或者高)要设置成 0dp(match constraints)
    只有这样,ConstraintLayout 才能根据另一个维度以及比例去计算当前维度。

  • 避免与固定值/wrap_content 错配

    • 如果你想让一个组件“宽高都随父布局变化”,那就需要同时把 width / height 设成 0dp 并有至少一个方向能被计算出来(通常还需要配合其他约束如 start_toStartOf, end_toEndOf, top_toTopOf, bottom_toBottomOf 等)。

    • 如果你写成类似

    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_constraintDimensionRatio="16:9"
    

    这时系统会尝试先测量内容,再按照宽高比去调节,但可能会导致比例被忽略,或宽高约束无法生效。

  • 与其他约束配合使用
    比如你想让 View 填充宽度,那么需要给它 app:layout_constraintStart_toStartOf="parent"app:layout_constraintEnd_toEndOf="parent",然后宽度设为 0dp,再通过 layout_constraintDimensionRatio 算出高度。

百分⽐布局

  • 需要对应⽅向上的值为 match_constraint
  • 百分⽐是 parent 的百分⽐,⽽不是约束区域的百分⽐

例如:宽度是⽗容器的 30%

android:layout_width="0dp"
app:layout_constraintWidth_percent="0.3"

辅助控件

GuideLine(引导线)

概述

GuideLine 是一种虚拟的辅助线,可以水平或垂直地放置在布局中,用于对齐和定位其他视图。它本身不会显示在屏幕上,但可以在设计布局时进行参考。

使用场景

  • 对齐多个视图:通过在相同的位置使用 GuideLine,可以确保多个视图在相同的位置对齐。
  • 响应式设计:在不同屏幕尺寸下,通过百分比位置的 GuideLine 保持布局的一致性。

示例

<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">

    <!-- 垂直 GuideLine,位置为父宽度的 50% -->
    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline_vertical"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintGuide_percent="0.5" />

    <!-- 水平 GuideLine,位置为父高度的 30% -->
    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline_horizontal"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_percent="0.3" />

    <!-- 使用 GuideLine 对齐的视图 -->
    <Button
        android:id="@+id/button1"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="Button 1"
        app:layout_constraintStart_toStartOf="@id/guideline_vertical"
        app:layout_constraintEnd_toEndOf="@id/guideline_vertical"
        app:layout_constraintTop_toTopOf="@id/guideline_horizontal" />

</androidx.constraintlayout.widget.ConstraintLayout>

注意事项

  • 百分比定位layout_constraintGuide_percent 可以使用百分比(0.0 到 1.0)定位 GuideLine。
  • 固定偏移:也可以使用 layout_constraintGuide_beginlayout_constraintGuide_end 来设置固定的偏移量。

Group(组)

概述

Group 用于将多个视图组织在一起,便于同时控制它们的可见性(visibility)和启用状态(enabled)。它不会影响布局,只是一个逻辑上的分组工具。

使用场景

  • 批量控制视图属性:例如,当需要同时隐藏或显示一组视图时,可以使用 Group 来简化操作。
  • 提高代码可读性:将相关视图分组,便于管理和维护。

示例

<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">

    <!-- 一组视图 -->
    <Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Button 1"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <Button
        android:id="@+id/button2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Button 2"
        app:layout_constraintTop_toBottomOf="@id/button1"
        app:layout_constraintStart_toStartOf="parent" />

    <!-- Group 定义 -->
    <androidx.constraintlayout.widget.Group
        android:id="@+id/group_buttons"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:constraint_referenced_ids="button1,button2" />

</androidx.constraintlayout.widget.ConstraintLayout>
val group = findViewById<Group>(R.id.group_buttons)
group.visibility = View.GONE  // 隐藏 group 内所有视图

注意事项

  • 属性限制:Group 只能控制 visibilityenabled 属性,不能控制位置或尺寸。
  • 引用视图:通过 app:constraint_referenced_ids 属性引用需要分组的视图,多个 ID 用逗号分隔。

Layer(层)

概述

Layer 是 ConstraintLayout 的一个功能,允许将多个视图组合在一起,并为这些组合视图设置统一的属性或进行动画操作。它类似于图层的概念,可以帮助更好地管理复杂的视图层级。

使用场景

  • 组合动画:对一组视图同时应用动画效果。
  • 统一属性设置:例如,统一调整一组视图的透明度、旋转等属性。

示例

假设有三个按钮,想要将它们组合在一起,并对整个组合应用旋转和缩放效果。

<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/constraintLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- 按钮 1 -->
    <Button
        android:id="@+id/button1"
        android:layout_width="100dp"
        android:layout_height="50dp"
        android:text="按钮1"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <!-- 按钮 2 -->
    <Button
        android:id="@+id/button2"
        android:layout_width="100dp"
        android:layout_height="50dp"
        android:text="按钮2"
        app:layout_constraintStart_toEndOf="@id/button1"
        app:layout_constraintTop_toTopOf="parent"
        android:layout_marginStart="16dp" />

    <!-- 按钮 3 -->
    <Button
        android:id="@+id/button3"
        android:layout_width="100dp"
        android:layout_height="50dp"
        android:text="按钮3"
        app:layout_constraintStart_toEndOf="@id/button2"
        app:layout_constraintTop_toTopOf="parent"
        android:layout_marginStart="16dp" />

    <!-- 定义 Layer -->
    <androidx.constraintlayout.helper.widget.Layer
        android:id="@+id/layer_buttons"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:constraint_referenced_ids="button1,button2,button3"
        app:rotation="15"
        app:scaleX="1.2"
        app:scaleY="1.2"
        app:translationX="10dp"
        app:translationY="20dp" />

</androidx.constraintlayout.widget.ConstraintLayout>

import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.helper.widget.Layer
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

    private lateinit var layer: Layer

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        layer = findViewById(R.id.layer_buttons)
        
        // 示例:点击某个按钮时,改变 Layer 的透明度
        val button1 = findViewById<View>(R.id.button1)
        button1.setOnClickListener {
            layer.alpha = 0.5f  // 设置 Layer 透明度为 50%
        }

        // 示例:点击另一个按钮时,恢复 Layer 的透明度
        val button2 = findViewById<View>(R.id.button2)
        button2.setOnClickListener {
            layer.alpha = 1.0f  // 设置 Layer 透明度为 100%
        }
    }
}

Barrier(障碍)

概述

Barrier 用于在布局中根据一组视图的实际位置动态创建一条参考线(或参考边)。这条参考线能够帮助其他视图进行对齐或排列。相比 GuideLine(在布局初始化时位置就固定),Barrier 会根据被引用视图尺寸的变化而动态移动,从而满足更灵活的布局需求。

Barrier 可以理解为一条“动态的约束线”。它具有以下特征:

  1. 自身不可见:和 Guideline 一样,Barrier 并不会在界面上显示。
  2. 动态定位:Barrier 的位置由它所引用的一组视图共同决定。例如,当你设置 barrierDirection="end" 并引用若干文本视图时,Barrier 的位置会始终位于那几个文本视图最右边界的最大值之后(再加上一点点偏移),无论这些文本视图的内容或长度如何变化,Barrier 都会动态更新位置。
  3. 可作为对齐基准:在其他视图的约束中,可以使用 Barrierstart, end, top, bottom 来对齐或排布,达到自适应布局的效果。

使用场景

  • 动态宽度对齐:如果有多个视图的宽度不确定(例如有些文本会根据内容长度变化),需要在这些不确定宽度的视图后面再放置一个按钮或图片,让该按钮/图片始终贴在最宽的视图之后,就可以使用 Barrier

  • 动态高度对齐:同理,如果有多个视图高度可能随着内容变化而改变,需要在这些视图下面对齐另一个视图,就可以使用 Barrier

  • 响应式布局:在多语言、多屏幕适配时,内容长度经常会发生变化。使用 Barrier 可以在布局层面自动处理这些变化,无需繁琐地在代码中计算位置或尺寸。

Barrier 的关键属性

  • app:barrierDirection

    • 表示 Barrier 的方向,可能的取值有 start, end, top, bottom
    • 例如,barrierDirection="end" 表示此 Barrier 会对齐一组视图的右边界中“最靠右”的那个点。
  • app:constraint_referenced_ids

    • 用于声明这个 Barrier 所引用的一组视图的 ID。可以引用一个或多个视图,多个视图 ID 之间使用逗号分隔。
    • 例如:app:constraint_referenced_ids="text1,text2,text3"
  • app:barrierAllowsGoneWidgets

    • 当被引用的视图是 GONE 状态时,该视图是否会影响 Barrier 的计算。
    • true 表示即使视图是 GONE,也会将其位置视为 0 宽或 0 高来参与计算;
    • false 表示视图处于 GONE 时,直接忽略它的大小。

示例

有三个 TextView,它们的文本长度不同,宽度也不一样。我们想在它们最右边放置一个按钮,让这个按钮始终贴在“最右的文本右边界(也可以理解为宽度最长的文本)”之后。

<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">

    <!-- 第一个文本,文本内容较短 -->
    <TextView
        android:id="@+id/text1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="短文本"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        android:layout_marginStart="16dp"
        android:layout_marginTop="16dp" />

    <!-- 第二个文本,文本内容较长 -->
    <TextView
        android:id="@+id/text2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="这是一个比较长的文本内容,用于演示 Barrier 的位置变化"
        app:layout_constraintTop_toBottomOf="@id/text1"
        app:layout_constraintStart_toStartOf="parent"
        android:layout_marginStart="16dp"
        android:layout_marginTop="16dp" />

    <!-- 第三个文本,中等长度 -->
    <TextView
        android:id="@+id/text3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="中等长度文本"
        app:layout_constraintTop_toBottomOf="@id/text2"
        app:layout_constraintStart_toStartOf="parent"
        android:layout_marginStart="16dp"
        android:layout_marginTop="16dp" />

    <!-- Barrier 定义,方向为 end -->
    <androidx.constraintlayout.widget.Barrier
        android:id="@+id/barrier_right"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:barrierDirection="end"
        app:constraint_referenced_ids="text1,text2,text3"
        app:barrierAllowsGoneWidgets="true" />

    <!-- 需要贴在最右文本之后的按钮 -->
    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="操作"
        app:layout_constraintTop_toTopOf="@+id/text1"
        app:layout_constraintStart_toEndOf="@id/barrier_right"
        android:layout_marginStart="16dp" />

</androidx.constraintlayout.widget.ConstraintLayout>

示例说明:

  1. TextView 的位置

    • 三个 TextView 的宽度各不相同(取决于文本内容),我们并不清楚它们在实际渲染后的宽度。
  2. Barrier 的方向

    • 代码中设置了 app:barrierDirection="end",表示这个 Barrier 会自动放在三个文本视图中最靠右的那个右边缘之后。
    • barrierAllowsGoneWidgets="true" 表示如果有视图被设置为 View.GONEBarrier 依然会考虑它的尺寸(此时 GONE 视图尺寸为 0),以免造成布局意外塌陷。根据实际需要可设置为 false
  3. 按钮的约束

    • 按钮的 Start 约束连接到 BarrierEndapp:layout_constraintStart_toEndOf="@id/barrier_right"
    • 这意味着按钮的左边缘始终贴着 Barrier 的右边缘。由此保证无论哪一个文本的宽度最大,按钮都会放在它的右边。

注意事项

  • 方向设置:通过 app:barrierDirection 属性设置 Barrier 的方向,可以是 startendtopbottom
  • 引用视图:通过 app:constraint_referenced_ids 属性引用需要计算 Barrier 位置的视图,多个 ID 用逗号分隔。
  • 动态调整:Barrier 会根据引用视图的当前位置和尺寸动态调整自身位置,适用于内容动态变化的场景。

Placeholder(占位符)

概述

本身在界面上并不可见,但能在布局中“预留”出一块空间,然后在运行时将其他视图“放”到这个 Placeholder 所在的位置。

  • 本质

    • Placeholder 是一个继承自 ConstraintHelper 的“虚拟视图”。它本身并不会渲染可见的内容,而是用来放置其他视图。
  • 核心功能

    • 可以在预先定义的布局位置(Placeholder 所在的区域),在运行时将某个视图移动或替换到这个位置上,以实现动态布局切换或过渡动画。
  • 布局特性

    • 可以像普通视图一样,为 Placeholder 设置约束和大小(通常为 match_constraints 或固定大小),以此确定它在布局中的位置和区域。
    • 当使用 placeholder.setContentId(R.id.some_view) 时,ConstraintLayout 会把 some_view 移动到 Placeholder 的约束位置,并继承 Placeholder 的约束和尺寸。

Placeholder 的常见使用场景

  1. 视图内容切换

    • 比如有时要在同一个容器区域里显示一个图片,切换到下一步后又显示一个文字说明,或者表单等;此时可以通过 Placeholder 动态地切换要显示的视图 ID,实现快速的内容替换。
  2. 动画过渡

    • 使用 MotionLayout(ConstraintLayout 的扩展)或其他动画时,可以把 Placeholder 作为目标位置,让一个视图在动画中从自己的原位置移动到 Placeholder 所在位置,使得视觉过渡更加流畅且代码更简洁。
  3. 占位示例 / 骨架屏

    • 部分应用会先显示一个“加载中”或“骨架屏”样式,在数据加载完成后再替换成真正的内容视图。Placeholder 可以很好地完成这个占位到真实内容的切换。

示例

<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">

    <!-- Placeholder 定义 -->
    <androidx.constraintlayout.widget.Placeholder
        android:id="@+id/placeholder"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent" />

    <!-- 要替换的视图 -->
    <ImageView
        android:id="@+id/imageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/sample_image"
        android:visibility="gone" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="动态文本"
        android:visibility="gone" />

</androidx.constraintlayout.widget.ConstraintLayout>
// 在 Kotlin 中动态替换 Placeholder 的内容
val placeholder = findViewById<Placeholder>(R.id.placeholder)
val imageView = findViewById<ImageView>(R.id.imageView)
val textView = findViewById<TextView>(R.id.textView)

// 替换 Placeholder 为 ImageView
placeholder.setContentId(R.id.imageView)

// 或者替换为 TextView
placeholder.setContentId(R.id.textView)

与动画或 MotionLayout 搭配

Placeholder 在日常使用时,也常常结合 MotionLayout 来做过渡动画。大致流程是:

  1. MotionScene 中定义不同的 ConstraintSet,让同一个 Placeholder 在不同状态中使用不同视图或不同的属性。
  2. 在动画过渡(Transition)中切换 setContentId 或者切换视图的可见性,从而实现平滑的视图内容变换。

这比传统的在代码中手动管理坐标或切换布局更加直观、简洁。

注意事项

  1. 占位尺寸

    • Placeholder 的尺寸/约束决定了被替换视图的最终大小和位置。
    • 如果 Placeholder 是 wrap_content,就需要确保你能准确给它一个大小;如果是 0dp(match_constraints),则要结合相应的约束(如 Start, End, Top, Bottom)或长宽比来确定它所占的空间。
  2. 视图初始可见性

    • 如果不想在“切换”发生前就看到视图的实际位置,可以让被替换视图初始状态为 GONE
    • 当你调用 setContentId 时,会自动将视图移动到占位区域,并将其可见性调为 VISIBLE(如果之前是 GONE)。
  3. 对约束的影响

    • 使用 setContentId 会让该视图忽略它原本在 XML 中的约束,转而继承 Placeholder 的约束。
    • 如果原本的视图有非常复杂的约束,记得再三确认切换后不会产生冲突。
  4. 动态替换多个视图

    • 可以在代码中根据业务逻辑随时调用 placeholder.setContentId(R.id.viewA)placeholder.setContentId(R.id.viewB) 等把不同视图移动到相同的位置上,从而做内容切换或空间共享。

Flow(流布局)

概述

用于在同一个 ConstraintLayout 中实现类似 LinearlayoutFlexboxLayout 的“流式”布局。它能够根据子视图(被引用的视图)的数量和大小,自动进行多行或多列的排列,同时支持对齐方式、间距设置、最大元素数限制等多种灵活配置。FlowConstraintHelper 的一个子类(Flow extends VirtualLayout extends ConstraintHelper),可以在同一个 ConstraintLayout 内对多视图进行自动排列。它在布局时,会根据配置的 布局模式间距对齐方式 等,将所有被引用的视图按照一定规则进行“流式”排布:

  • 可以按行或按列自动换行/换列;
  • 可以指定一行或一列中能容纳的 最大元素数量
  • 可以控制每个元素之间的 水平和垂直间距
  • 可以指定 对齐模式重心偏移 等。

Flow 的使用场景

  1. 标签云/流式按钮组

    • 当有一组标签或按钮,需要在空间不够的情况下自动换行,并设定标签或按钮之间的间距。
    • 例如电商APP中的商品标签、社交应用中的兴趣标签等。
  2. 自适应网格

    • 需要根据屏幕宽度或动态内容,自动将子元素按一定规则“流式”摆放。
    • Flow 可以指定每行(列)最大元素数,也可以指定高度或宽度限制,来模拟简单的网格布局。
  3. 相对复杂的动态布局

    • 当应用内容是动态生成或数量不固定时,Flow 能够在不改变布局层级(仍是单层 ConstraintLayout)的情况下,为子视图提供自动排列与换行功能。

Flow 的核心属性

Flow 在 XML 中常用的属性列表如下(大部分以 app:flow_ 开头):

  1. app:constraint_referenced_ids

    • 指定由 Flow 管理的视图 ID,多个视图以英文逗号分隔。
    • 示例app:constraint_referenced_ids="button1,button2,button3"
  2. app:flow_wrapMode

    • 决定 Flow 如何换行或换列,常见取值:

      • none:不换行,所有子视图均在同一行(或同一列)中排布。
      • chain:当到达 maxElementsWrap 或空间不足时,会自动换行/换列。
      • aligned:与 chain 类似,但会对行/列进行对齐,使各行/列在可见尺寸上保持对齐。
  3. app:flow_maxElementsWrap

    • 指定在自动换行/换列前,单行(或单列)最多可容纳的子元素个数。
    • 示例app:flow_maxElementsWrap="3" 表示每行最多放 3 个元素,之后自动换行。
  4. app:flow_horizontalGap / app:flow_verticalGap

    • 子视图之间的 水平/垂直间距(单位:dp)。
    • 示例app:flow_horizontalGap="8dp" 表示每个子视图之间有 8dp 的水平间距。
  5. app:flow_horizontalStyle / app:flow_verticalStyle

    • 子视图在水平方向 / 垂直方向的分配模式,可选值包括:

      • spread(默认,平均分配空白)
      • wrap(只占用实际宽度/高度)
      • packed(全部堆在一起,可通过 bias 再做偏移)
  6. app:flow_horizontalBias / app:flow_verticalBias

    • 当使用了 packed 模式或类似情况时,可以使用 bias 控制子视图在剩余空间里的对齐位置,取值区间 [0..1],0 表示靠“start”或“top”,1 表示靠“end”或“bottom”。
  7. app:flow_firstHorizontalStyle , app:flow_lastHorizontalStyle

    • 可以对第一行、最后一行使用不同的分配模式,适用于更复杂的场景。

此外,还有一些其他属性,如 flow_firstHorizontalBias, flow_lastHorizontalBias, flow_firstVerticalStyle 等,用于针对首行、尾行做特殊处理。大部分场景下,你只需要掌握最常用的 flow_wrapModeflow_maxElementsWrapflow_horizontalGapflow_verticalGap 等即可。

示例

有 5 个按钮,想要让它们在一行中摆放,当空间不足或视图数量超过某个限制时自动换行,并在水平方向和垂直方向上设置固定的间距。

<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/constraintLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- Flow 组件 -->
    <androidx.constraintlayout.helper.widget.Flow
        android:id="@+id/flow"
        android:layout_width="0dp"
        android:layout_height="wrap_content"

        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        
        app:constraint_referenced_ids="button1,button2,button3,button4,button5"

        app:flow_wrapMode="chain"
        app:flow_maxElementsWrap="3"
        app:flow_horizontalGap="8dp"
        app:flow_verticalGap="8dp"
        />

    <!-- 子视图们:5 个按钮 -->
    <Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="按钮1" />

    <Button
        android:id="@+id/button2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="按钮2" />

    <Button
        android:id="@+id/button3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="按钮3" />

    <Button
        android:id="@+id/button4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="按钮4" />

    <Button
        android:id="@+id/button5"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="按钮5" />

</androidx.constraintlayout.widget.ConstraintLayout>

示例说明:

  1. Flow 自身是一个 ConstraintHelper,本身并不绘制可见内容,也不需要放在特定的层级结构内;它只需要在同一个 ConstraintLayout 中即可。
  2. constraint_referenced_ids="button1,button2,button3,button4,button5" 表示 Flow 将管理这 5 个按钮的摆放位置。
  3. flow_wrapMode="chain" + flow_maxElementsWrap="3" 表示在一行最多放 3 个按钮,超过 3 个就自动换到下一行。
  4. flow_horizontalGap="8dp"flow_verticalGap="8dp" 表示相邻元素之间的间距。
  5. android:layout_width="0dp" + app:layout_constraintStart_toStartOfapp:layout_constraintEnd_toEndOf 表示 Flow 这个辅助组件的“占位宽度”会随父容器的宽度进行拉伸,以便在内部进行更灵活的子视图排布。

示例2: 想要在垂直方向上进行流式排列,每列最多放 2 个按钮,并让它们在垂直方向上居中对齐:

<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/constraintLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.constraintlayout.helper.widget.Flow
        android:id="@+id/flow_vertical"
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        app:constraint_referenced_ids="buttonA,buttonB,buttonC,buttonD"
        
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toTopOf="parent"

        app:flow_wrapMode="chain"
        app:flow_maxElementsWrap="2"
        app:flow_horizontalGap="16dp"
        app:flow_verticalGap="16dp"

        app:flow_orientation="vertical"   <!-- 垂直方向流式排列 -->
        
        app:flow_verticalStyle="packed"   <!-- 垂直方向子视图堆叠在一起 -->
        app:flow_verticalBias="0.5"       <!-- 垂直方向偏移量 0.5 表示居中 -->

        />

    <!-- 四个按钮 -->
    <Button
        android:id="@+id/buttonA"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="A" />

    <Button
        android:id="@+id/buttonB"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="B" />

    <Button
        android:id="@+id/buttonC"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="C" />

    <Button
        android:id="@+id/buttonD"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="D" />

</androidx.constraintlayout.widget.ConstraintLayout>

示例2说明:

  • app:flow_orientation="vertical":将 Flow 切换到 竖直方向的排布模式。默认值是 horizontal

  • app:flow_maxElementsWrap="2":表示每列最多容纳 2 个子视图。放入第三个子视图时,就会自动换到下一列。

  • app:flow_verticalStyle="packed" + app:flow_verticalBias="0.5":将子视图在垂直方向上“包裹”到一起,并在容器可用空间中居中显示。

  • android:layout_width="wrap_content" + android:layout_height="0dp" + 垂直的上下约束,意味着 Flow 的高度会“填满”父布局的可用高度,并允许子元素在内部根据 bias 做居中或其他排布。

关键属性

  • app:flow_wrapMode:定义换行模式,可选值有 none(不换行)、chain(链式换行)、aligned(对齐换行)。
  • app:flow_horizontalGapapp:flow_verticalGap:设置子视图之间的水平和垂直间距。
  • app:flow_maxElementsWrap:设置每行(或列)的最大元素数目,当达到限制时自动换行。
  • app:constraint_referenced_ids:指定 Flow 管理的子视图 ID,多个 ID 用逗号分隔。

注意事项

  • 被 Flow 管理的视图依旧需要在同一个 ConstraintLayout 中

    • Flow 只能引用同一 ConstraintLayout 下的子视图 ID。无法跨布局引用视图。
  • 不要给被 Flow 引用的子视图再添加相互矛盾的约束

    • 一旦视图被 Flow 管理,它们的 位置 就主要由 Flow 决定。你可以给子视图做一些如大小约束等,但尽量避免再指定大量相互冲突的 Start_toStartOfTop_toTopOf 等,否则可能导致布局无法正常测量或出现意外。
  • 动态增删视图

    • 如果要在运行时动态增删被引用的视图,你需要在 Java/Kotlin 代码里重新设置 Flow 的引用 IDs(setReferencedIds()),或通过 ConstraintSet 动态更新。如果只是隐藏某些视图(View.GONE),Flow 会自动把它们忽略,但需要注意布局刷新。
  • 性能

    • Flow 在大多数常见场景下性能都是足够的。只有当你需要频繁、大量地增删视图或进行动画时,才需要特别关注性能问题。一些极端复杂的场景下,可能还需手动优化或使用更专业的布局方案。

总结

ConstraintLayout 提供了多种辅助工具和组件,如 GuideLineGroupLayerBarrierPlaceholderFlow,以帮助构建灵活、高效和响应式的布局。了解并合理使用这些组件,可以显著提升布局的可维护性和适应性。

快速参考

组件用途描述关键属性
GuideLine虚拟辅助线,用于对齐和定位其他视图layout_constraintGuide_percentorientation
Group逻辑分组,用于批量控制视图的 visibilityenabledconstraint_referenced_ids
Layer组合视图,统一设置属性或应用动画具体属性依赖于实现
Barrier动态参考线,根据一组视图的位置动态调整barrierDirectionconstraint_referenced_ids
Placeholder占位符,用于动态替换或展示其他视图setContentId 方法
Flow流式布局,自动排列子视图flow_wrapModeflow_horizontalGapconstraint_referenced_ids

ConstraintHelper

ConstraintHelper 是一个非常核心且抽象的“辅助类”基类,许多常用的辅助组件(如 BarrierGroupLayerFlowPlaceholder)都是继承自 ConstraintHelper。它的主要作用是:在不直接参与可见视图的前提下,通过引用一组目标视图,对它们的布局或属性进行批量或动态的控制

一、ConstraintHelper 是什么?

  • 抽象基类ConstraintHelper 继承自 View(或者更准确地说是 ViewGroup 的子类,但它自身不显示内容),在 ConstraintLayout 体系下用作对“引用视图的批量处理”。
  • 引用视图:它有一个非常重要的属性 —— constraint_referenced_ids,通过它可以指定若干需要被“帮助”或“管理”的视图,多个视图的 ID 用逗号分隔。
  • 布局流程:ConstraintHelper 在布局流程(onMeasureonLayout 等)中会参与 ConstraintLayout 的约束解析,可以在这其中对被引用视图做批量的调整、计算或变换。

简单来说,你可以把 ConstraintHelper 想象成“非可视化的布局组件”,它不展示内容,却能集中管理一组可见视图,或对它们进行统一的逻辑处理。


二、ConstraintHelper 的常见子类

在日常开发中,我们或多或少都用过以下组件——它们都继承自 ConstraintHelper,只是实现了各自特殊的功能:

  1. Group

    • 主要用于批量控制一组视图的可见性(visibility)和启用状态(enabled)。
    • 不会影响视图的尺寸和位置,只是一个对若干视图的逻辑分组。
  2. Barrier

    • 动态参考线,用于根据被引用视图的位置(最左/最右/最上/最下)创建一条虚拟的对齐边。
    • 适用于视图宽高不确定时,让其他视图可以“紧贴”这条动态边界。
  3. Layer

    • 可将一组视图当成一个整体来处理,对这组视图应用统一的变换(旋转、缩放、透明度、位移等)。
    • 常见场景:对一组按钮或视图做整体动画。
  4. Flow

    • 继承自 VirtualLayout(也是 ConstraintHelper 的子类),可以在 ConstraintLayout 内部实现流式布局、自动换行、多行多列等功能。
    • 常见场景:动态标签云、复杂的网格排列等。
  5. Placeholder

    • 一个“占位符”视图,可在运行时用来“挂载”其他视图,常用于动态切换视图或做动画过渡。
  6. VirtualLayout

    • 其实 Flow 就是 VirtualLayout 的一个实现。“虚拟布局”可以在约束层面或绘制层面帮助管理一组视图,避免多重嵌套布局。

这些子类通过继承 ConstraintHelper 并实现或重写特定方法(如 updatePreLayout()updatePostLayout() 等),实现了各自的功能逻辑。


三、ConstraintHelper 的核心属性

  1. app:constraint_referenced_ids

    • 这是 ConstraintHelper 最常用的属性,用于指定需要被管理的视图。
    • 多个视图的 ID 用英文逗号分隔,例如 app:constraint_referenced_ids="view1,view2,view3"
    • 这些 ID 对应的视图必须在同一个 ConstraintLayout 中。
  2. 其他自定义属性

    • 不同的子类会增加自身需要的自定义属性。
    • 例如 BarrierbarrierDirectionbarrierAllowsGoneWidgetsFlowflow_wrapModeflow_horizontalGapflow_verticalGapLayerrotationscaleXscaleYalpha 等。

四、自定义 ConstraintHelper

如果内置的 BarrierGroupFlow 等已经满足需求,通常不需要自己去继承 ConstraintHelper
但如果想实现一些特殊的“批量控制”或“动态计算”,可以尝试自定义一个类,例如 MyCustomHelper,继承 ConstraintHelper,然后在其中重写以下方法(关键):

  1. init(AttributeSet attrs, int defStyleAttr)

    • 用于在构造函数中解析自定义属性(如果有的话),并初始化相关逻辑。
  2. updatePreLayout(ConstraintLayout container)

    • 布局前的更新。在这里可以拿到被引用视图的列表,通过 getViews(container) 获取具体的子视图引用,然后根据需要进行一些前置计算或处理。
  3. updatePostLayout(ConstraintLayout container)

    • 布局后的更新。如果需要在布局完成后根据实际尺寸或位置再次调整什么,可以在这里处理。
  4. onDraw(Canvas canvas) (或 dispatchDraw(Canvas canvas))

    • 如果你的 Helper 需要在屏幕上绘制一些辅助信息(多数情况不会这么做),可以在此处实现自定义绘制逻辑。
    • 一般的 ConstraintHelper 子类不会去绘制可见元素,除非像 Flow 需要绘制特殊的背景或分隔线。

一个示例的伪代码结构大致如下:

class MyCustomHelper @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : ConstraintHelper(context, attrs, defStyleAttr) {

    init {
        // 解析自定义属性(如果需要)
    }

    override fun updatePreLayout(container: ConstraintLayout?) {
        super.updatePreLayout(container)
        // 获取被引用的视图
        val views = getViews(container)
        // 对 views 做一些批量逻辑处理,例如改变它们的宽高、margin 等
    }

    override fun updatePostLayout(container: ConstraintLayout?) {
        super.updatePostLayout(container)
        // 在容器完成布局之后,如果需要再次根据实际位置进行微调,可以做在这里
    }
}

然后在布局 XML 中声明并使用这个自定义 Helper:

<com.example.MyCustomHelper
    android:id="@+id/my_helper"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:constraint_referenced_ids="view1,view2,view3" />

ConstraintHelper 就是可以在测量/布局阶段对被引用视图做“干预”或“额外计算”的关键所在。、

学后测验

一、单项选择题(每题1分)

1. 下列关于 ConstraintLayout 中的 0dp(match constraints)说法,正确的是:

A. 0dp 等同于 wrap_content
B. 0dp 表示控件宽/高由父容器撑满
C. 0dp 表示该边可由约束和其他规则决定实际大小
D. 0dp 不能和 ratio 属性一起用

【答案与解析】

答案:C

解析:0dp 在 ConstraintLayout 中表示 match_constraints,即该边的大小受限于其它约束和计算,可以搭配比例等使用;不是 wrap_content,也不是简单的 match_parent。


2. 以下哪个属性可以让 TextView 的文字基线与另一个控件对齐?

A. app:layout_constraintBottom_toBottomOf
B. app:layout_constraintBaseline_toBaselineOf
C. app:layout_constraintTop_toTopOf
D. app:layout_constraintStart_toStartOf

【答案与解析】

答案:B

解析:Baseline_toBaselineOf 使两个控件的文本基线对齐,特别适合对齐不同字号的文本控件。


3. 如果你需要让一个视图随另一组视图动态变化边界对齐,应优先用哪种辅助组件?

A. GuideLine
B. Barrier
C. Group
D. Layer

【答案与解析】

答案:B

解析:Barrier 动态参考一组控件的边界,无论这些控件内容怎么变化,Barrier 都能自动移动,是自适应复杂场景的利器。


4. 关于 ConstraintLayout 的 bias(偏向),下列说法正确的是:

A. bias 只能取 0 或 1
B. bias 影响约束范围内控件的位置百分比
C. bias 只能用于水平居中
D. bias 只影响宽度,不影响高度

【答案与解析】

答案:B

解析:bias 取值 [0,1],控制控件在约束范围内的位置,比如 0.3 就是在 30% 的地方。既有水平也有垂直 bias。


二、多项选择题(每题2分)

5. 以下哪些属于 ConstraintHelper 的子类?(多选)

A. Barrier
B. Layer
C. Guideline
D. Flow
E. Group
F. Placeholder

【答案与解析】

答案:A、B、D、E、F

解析:ConstraintHelper 的子类包括 Barrier、Layer、Flow、Group、Placeholder。GuideLine 不是,它有自己独立的实现。


6. 关于 Flow 布局组件,下列说法哪些是正确的?(多选)

A. Flow 支持横向和纵向流式布局
B. Flow 只能在 ConstraintLayout 中引用视图
C. Flow 可以设置元素之间的间距
D. Flow 必须放在被引用控件的父容器外部
E. Flow 会自动管理被引用视图的位置

【答案与解析】

答案:A、B、C、E

解析:Flow 只能引用同一 ConstraintLayout 下的视图,可以设置间距,会自动管理被引用视图的位置,支持横向纵向流式布局。Flow 必须和被管理视图同属于一个 ConstraintLayout。


三、判断题(每题1分)

7. ( ) ConstraintLayout 的 Group 组件可以直接控制分组视图的位置和尺寸。

【答案与解析】

答案:错误

解析:Group 只能控制分组视图的 visibility 和 enabled,不能直接影响布局属性如位置和尺寸。


8. ( ) Placeholder 可以让任意视图在运行时占用 Placeholder 预留的空间和约束。

【答案与解析】

答案:正确

解析:Placeholder 通过 setContentId(),可以让任意被管理视图移动到自身位置,并继承约束和尺寸。


9. ( ) Barrier 只适用于横向布局场景,无法用于垂直对齐。

【答案与解析】

答案:错误

解析:Barrier 可设置方向,支持 start/end/top/bottom,横向、纵向都可以。


四、简答题(每题4分)

10. 简述 ConstraintLayout 的 0dp(match constraints)和 match_parent 的区别?在什么场景应该用 0dp?

【答案与解析】

答案:

0dp 表示 match constraints,只在 ConstraintLayout 下才有意义,表示该方向的尺寸完全受限于其约束和相关规则,可以用来自适应空间(比如流式布局、配合 ratio、百分比宽高等)。

match_parent 表示直接撑满父布局剩余空间,不考虑任何约束,只和父容器尺寸相关。ConstraintLayout 中不建议用 match_parent,容易导致冲突或无效,推荐用 0dp + 约束表达拉伸或自适应需求。

常用场景

  • 需要控件自动撑满两侧(或上下)约束间剩余空间时
  • 需要和 ratio、百分比等结合做自适应宽高时
  • 用于 Flow/Barrier 这种辅助组件引用的控件宽高设置

11. Barrier 适合哪些场景?请简述实现原理和注意事项。

【答案与解析】

答案:

Barrier 适合一组控件尺寸、内容不确定,且需要让其他控件随其边界自动适配对齐的场景,比如多语言文本、动态列表、响应式按钮布局。

原理:Barrier 动态计算它所引用控件的最大(或最小)边界,形成一条虚拟线,其他控件可以对齐该线,实现自适应。

注意事项

  • 只能引用同一个 ConstraintLayout 下的视图
  • 注意 GONE 状态对 Barrier 计算的影响
  • 被引用视图尺寸频繁变化时,Barrier 会动态刷新,布局性能可能略受影响

五、编程/布局题(每题5分)

12. [XML] 设计如下布局:屏幕顶部放一个图片,图片正下方有一个横向排列的按钮组,按钮数量不定但需要自动换行,每行最多放3个按钮,并且按钮之间水平和垂直间隔8dp。图片和按钮组之间垂直间距为24dp,所有控件水平方向居中。请写出关键的 ConstraintLayout XML。

【答案与解析】

答案:

<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">

    <!-- 顶部图片 -->
    <ImageView
        android:id="@+id/top_image"
        android:layout_width="120dp"
        android:layout_height="120dp"
        android:src="@drawable/ic_launcher_foreground"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>

    <!-- Flow 负责流式按钮排列 -->
    <androidx.constraintlayout.helper.widget.Flow
        android:id="@+id/flow_buttons"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:constraint_referenced_ids="btn1,btn2,btn3,btn4,btn5,btn6"
        app:flow_wrapMode="chain"
        app:flow_maxElementsWrap="3"
        app:flow_horizontalGap="8dp"
        app:flow_verticalGap="8dp"
        app:layout_constraintTop_toBottomOf="@id/top_image"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_margin="24dp"
    />

    <!-- 六个按钮示例,实际数量可增减 -->
    <Button android:id="@+id/btn1" ... android:text="按钮1"/>
    <Button android:id="@+id/btn2" ... android:text="按钮2"/>
    <Button android:id="@+id/btn3" ... android:text="按钮3"/>
    <Button android:id="@+id/btn4" ... android:text="按钮4"/>
    <Button android:id="@+id/btn5" ... android:text="按钮5"/>
    <Button android:id="@+id/btn6" ... android:text="按钮6"/>

</androidx.constraintlayout.widget.ConstraintLayout>

解析:Flow 组件实现流式自动换行,maxElementsWrap 控制每行最大按钮数,按钮引用 id 灵活扩展即可。


13. [Kotlin] 动态实现如下需求:有两个视图 A/B,点击按钮可以让 A 或 B 动态切换到布局中某个占位符 Placeholder 的位置。写出简要实现代码。

【答案与解析】

答案:

val placeholder = findViewById<Placeholder>(R.id.placeholder)
val viewA = findViewById<View>(R.id.viewA)
val viewB = findViewById<View>(R.id.viewB)
val btnSwitch = findViewById<Button>(R.id.btnSwitch)

// 点击按钮时切换显示
btnSwitch.setOnClickListener {
    val showingA = placeholder.contentId == R.id.viewA
    placeholder.setContentId(if (showingA) R.id.viewB else R.id.viewA)
}

解析:通过 setContentId() 切换 Placeholder 的内容,原视图的约束失效,直接使用占位符的约束。