约束布局

URL
date
Aug 18, 2022
slug
约束布局 详解
status
Published
tags
summary
约束布局
type
Post
 

What & Why

Google在2016年IO大会推出的一种新的布局,通过灵活的约束来完成各种布局操作,而且在Android Studio2.3之后就内置为Layout的默认模板。可让开发者使用扁平视图层次结构(无嵌套视图组)创建复杂的大型布局。它与 RelativeLayout 相似,其中所有的视图均根据同级视图与父布局之间的关系进行布局,但其灵活性要高于 RelativeLayout并且更易于可见即所得完成布局,也支持拖拽式的创建布局,不过我感觉有点不习惯,见仁见智了。
众所周知在拿到一个稍微比较复杂的设计稿,用相对布局的话不能很好的实现效果,那就常规套路进行嵌套布局,而约束布局的出现可以很大程度的减少这种情况的发生,可以很好的完成相对和线性布局的任务,从而改善布局渲染的速度,而且它还有很灵活的自适应性,通过约束协议来定义UI布局,而不是生硬的写死。
比如这样一个用户信息墙,完全单独使用相对布局的话完成不了其中有关中心对称的部分(如果完全写死偏移啥的话够呛),所以不可避免的需要嵌套线性布局。这个时候通过约束布局的话,完全能够平铺成一个层级而且很灵活。
但是,从复用性上考虑以及后期维护上考虑,约束布局最好还是看成相对布局加强版,不能堪称万能的,因为后续修改约束布局可能会牵一发动全身,此时可以适当嵌套服用,而不是一梭子
notion image
 

How

基本使用

一个控件至少要有两个约束

定位

  • 常规定位
  • 角度定位
  • 对齐
  • 基线定位
常规定位
通过layout_constraintX_toYOf的形式一共包括8组了:
layout_constraintLeft_toLeftOf layout_constraintLeft_toRightOf layout_constraintRight_toLeftOf layout_constraintRight_toRightOf layout_constraintTop_toTopOf layout_constraintTop_toBottomOf layout_constraintBottom_toTopOf layout_constraintBottom_toBottomOf layout_constraintBaseline_toBaselineOf layout_constraintStart_toEndOf layout_constraintStart_toStartOf layout_constraintEnd_toStartOf layout_constraintEnd_toEndOf
A_toBOf的形式,分别对应了矩形从哪一条边到约束矩形哪一条边的约束。
比如TopToBottom加上StatToEnd:
notion image
 
<androidx.constraintlayout.utils.widget.MockView
    android:id="@+id/mv_center"
    android:layout_width="100dp"
    android:layout_height="100dp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

<androidx.constraintlayout.utils.widget.MockView
    android:id="@+id/mv_center_2"
    android:layout_width="100dp"
    android:layout_height="100dp"
    app:layout_constraintTop_toBottomOf="@+id/mv_center"
    app:layout_constraintStart_toEndOf="@id/mv_center"/>
这样就完成了一个基本的定位,以此类推
 
角度定位
notion image
前面有个定位在左下角,类似的效果可以通过View中心点来进行角度定位
app:layout_constraintCircle设置目标
app:layout_constraintCircleAngle设置角度
app:layout_constraintCircleRadius设置中心到中心距离
 
比如:
notion image
<androidx.constraintlayout.utils.widget.MockView
        android:id="@+id/mv_center"
        android:layout_width="100dp"
        android:layout_height="100dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.utils.widget.MockView
        android:id="@+id/mv_center_3"
        app:layout_constraintCircle="@+id/mv_center"
        app:layout_constraintCircleAngle="45"
        app:layout_constraintCircleRadius="150dp"
        android:layout_width="100dp"
        android:layout_height="100dp"
        tools:ignore="MissingConstraints" />
 
基线定位
对于Text类的控件来说还存在基线对齐,通过基线来设置约束,此时基线可以看出另一条水平定位线,和Top,Bottom类似
notion image
 
比如:此时文本2是基于基线对齐文本1的
notion image
<TextView
    tools:text="文本1"
    android:id="@+id/tv_1"
    app:layout_constraintBottom_toTopOf="@id/mv_center"
    app:layout_constraintStart_toStartOf="parent"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>

<TextView
    tools:text="文本2"
    app:layout_constraintBaseline_toBaselineOf="@+id/tv_1"
    app:layout_constraintStart_toEndOf="@id/tv_1"
    android:textSize="30sp"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>
对齐
同样通过这种方式可以完成对齐操作:
比如:TopToTop,以此类推
notion image
 
<androidx.constraintlayout.utils.widget.MockView
        android:id="@+id/mv_center"
        android:layout_width="100dp"
        android:layout_height="100dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <androidx.constraintlayout.utils.widget.MockView
        app:layout_goneMarginStart="100dp"
        android:id="@+id/mv_center_2"
        android:layout_width="100dp"
        android:layout_height="100dp"
        app:layout_constraintTop_toTopOf="@+id/mv_center"
        app:layout_constraintStart_toEndOf="@id/mv_center"/>
通过对齐设置对象更改为parent加上StartToStart+TopToTop等操作就可以实现在父布局种上下左右的定位
notion image

边距

  • 普通边距
  • Gone边距
普通边距
普通边距和Android基本用法一样通过margin_Start等来添加,有一点要注意只有存在约束margin才会生效
notion image
Gone边距
约束布局独有的Gone_Margin当约束指向的目标可见性变为GONE的时候,目标View会变成一个点,而自身的app:layout_goneMarginStart="100dp" 就会发挥作用,android:layout_marginStart="10dp" 则会失效。
notion image
<androidx.constraintlayout.utils.widget.MockView
    android:visibility="gone"
    android:id="@+id/mv_center"
    android:layout_width="100dp"
    android:layout_height="100dp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

<androidx.constraintlayout.utils.widget.MockView
    app:layout_goneMarginStart="100dp"
    android:id="@+id/mv_center_2"
    android:layout_marginStart="10dp"
    android:layout_width="100dp"
    android:layout_height="100dp"
    app:layout_constraintTop_toTopOf="@+id/mv_center"
    app:layout_constraintStart_toEndOf="@id/mv_center"/>

居中

在约束布局中的居中只需要前后或者左右的约束线指向不同的方向,这样该View会自动在两端约束拉扯下保持居中,比如这个下面这些居中,只要确定两端约束指向目标就能保持居中
notion image
 

偏移

除了通过margin来产生偏移,约束布局提供另一种方式
app:layout_constraintHorizontal_bias="0.5" 来设置水平位置偏移
app:layout_constraintVertical_bias="0.2" 来设置垂直偏移
取值范围就是0-1
notion image
 
<androidx.constraintlayout.utils.widget.MockView
    android:id="@+id/mv_center"
    android:layout_width="100dp"
    android:layout_height="100dp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintHorizontal_bias="0.5"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintVertical_bias="0.2" />

尺寸约束

  • 0dp
  • 比例宽高
 
0dp
尺寸设置除了传统的match_parentwrap_parent和确定尺寸之外,约束布局定义了match_constraint 也就是0dp,自身不定义尺寸,通过约束来确定尺寸,这种尺寸下有三种模式
  • spread 默认取值,被约束拉伸
  • wrap 根据内容包裹
  • percent 按照父布局的百分比设置
<TextView
    tools:text="测试文本测试文本"
    tools:textSize="18sp"
    app:layout_constraintWidth_default="percent"
    app:layout_constraintWidth_percent="0.5"
    app:layout_constraintHeight_default="percent"
    app:layout_constraintHeight_percent="0.5"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    android:layout_width="0dp"
    android:layout_height="0dp"/>
notion image
这样就可以更具父布局设定尺寸了
 
 
比例高宽
notion image
通过设置图片高度为0dp,结合16:9比例,图片会自动确定高
<ImageView
        android:scaleType="centerCrop"
        tools:src="@tools:sample/backgrounds/scenic"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintDimensionRatio="16:9"
        android:layout_width="match_parent"
        android:layout_height="0dp"/>
 

链式

当约束布局中多个控件在水平或者垂直方向上,他们的约束互相连接着,看上去像是一个双向链表的感觉,如图:
notion image
此时就是一个链式,功能上其实就有点类似线性的布局感觉,但是更强大。通过对链的头设置不同的链式风格来实现不一样的摆放效果,有三种风格
app:layout_constraintVertical_chainStyle="spread|spread_inside|packed"
CHAIN_SPREAD
 
notion image
 
CHAIN_SPREAD_INSIDE
notion image
CHAIN_PACKED
notion image
以及设置头节点来伴随偏移
notion image
控件权重
在线性布局中有个权重,在约束布局中同样有,前提是要形成链式,设置app:layout_constraintVertical_weight 属性
notion image
<androidx.constraintlayout.utils.widget.MockView
        android:id="@+id/mv_open_img"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toTopOf="@+id/mv_logo"
        android:layout_width="match_parent"
        app:layout_constraintVertical_weight="0.8"
        android:layout_height="0dp"/>

    <androidx.constraintlayout.utils.widget.MockView
        android:id="@+id/mv_logo"
        app:layout_constraintVertical_weight="0.2"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/mv_open_img"
        app:layout_constraintBottom_toBottomOf="parent"
        android:layout_width="match_parent"
        android:layout_height="0dp"/>
notion image

辅助工具

 

Optimizer

 

Guideline

在约束布局中可以通过参考线来最为参照,而参考线在实际渲染是不可见的。
android:orientation="vertical" 参考线方向
app:layout_constraintGuide_begin="10dp" 参考线距离开始位置
app:layout_constraintGuide_end="10dp" 参考线距离末尾位置
app:layout_constraintGuide_percent="0.5" 参考线基于百分比位置
notion image

Barrier

屏障和参考下类似也不会显示在实际的页面,但是屏障是基于几个控件形成的一条屏障,会随着控件变化而变化的。
在这样一个应用场景下,右边的文字总是需要出现在左边两个文字右侧,而且不会产生遮挡,但是有可能在这两个TextView在不同的语言下长度可能有所不同,所以单独基于哪个TextView来约束都会有问题,于是通过基于二者的右侧屏障就不会有问题了,屏障会是根据二者来计算的
notion image
notion image
notion image
 
<androidx.constraintlayout.widget.Barrier
    android:id="@+id/barrier1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:barrierMargin="20dp"
    app:barrierDirection="end"
    app:constraint_referenced_ids="tv_1,tv_2" />
notion image

Group

通过Group可以声明将多个控件分组,通过对组的控制来统一控制这些控件可见性
app:constraint_referenced_ids="id,id"  加入组的控件id
 
notion image
 
<androidx.constraintlayout.widget.Group
    android:id="@+id/group"
    android:visibility="gone"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:constraint_referenced_ids="tv_1,tv_2,tv_3" />

Placeholder

占位,通过调用setContentId将布局上的某个控件移动到Placeholder上去
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <androidx.constraintlayout.utils.widget.MockView
        android:id="@+id/mv"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:layout_width="100dp"
        android:layout_height="100dp"/>
    <androidx.constraintlayout.widget.Placeholder
        android:id="@+id/placeholder"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        android:layout_width="200dp"
        android:layout_height="200dp"/>
    <com.google.android.material.button.MaterialButton
        android:id="@+id/btn"
        android:text="Click Me"
        app:layout_constraintStart_toEndOf="@+id/mv"
        app:layout_constraintTop_toTopOf="@+id/mv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</androidx.constraintlayout.widget.ConstraintLayout>
 
通过代码来设置,这样某个控件会自动跑到placeholder上去
binding.btn.setOnClickListener {
    binding.placeholder.setContentId(R.id.mv)
}
这个原理也很简单就是就是把某个View直接变成placeholder的内容view
notion image
notion image

Layer

给多个View画出一个层,找出这几个view矩形,用来展示背景或者前景,如果Layer顺序在前就是背景,在后就是前景,类似FrameLayout排列
<androidx.constraintlayout.helper.widget.Layer
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#4D000000"
    android:padding="10dp"
    app:constraint_referenced_ids="iv,tv,mv" />

<ImageView
    android:id="@+id/iv"
    android:layout_width="200dp"
    android:layout_height="200dp"
    tools:src="@tools:sample/backgrounds/scenic"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

<TextView
    android:id="@+id/tv"
    android:layout_width="100dp"
    android:layout_height="40dp"
    android:gravity="center"
    tools:text="Android"
    android:textColor="@color/teal_200"
    android:textSize="25sp"
    android:textStyle="bold"
    app:layout_constraintEnd_toEndOf="@+id/iv"
    app:layout_constraintStart_toStartOf="@+id/iv"
    app:layout_constraintTop_toBottomOf="@+id/iv" />
<androidx.constraintlayout.utils.widget.MockView
    app:layout_constraintStart_toEndOf="@+id/iv"
    app:layout_constraintBottom_toTopOf="@+id/iv"
    android:id="@+id/mv"
    android:layout_width="100dp"
    android:layout_height="100dp"/>
背景
背景
前景
前景

Flow流式布局

可以组织多个view,像标签流一样摆放
通过app:flow_wrapMode="none|aligned|chain" 来设置基本的排列风格
none默认风格,各个View被摊开在一行
notion image
aligned 对齐风格
notion image
chain 链式风格
notion image
 
在chain和aligned风格下,此时每一行其实都有点类似于之前用到的链式,所以可以设置每一条链上主轴的排列模式,packed、spread、spread_inside
app:flow_horizontalStyle="spread_inside|packed|spread" 水平线上的排列
如果是竖直排列计算的
app:flow_verticalStyle="spread_inside|packed|spread" 垂直线上的排列
packed
packed
spread_inside
spread_inside
 
spread
spread
 
主轴上的定位已经确定好了, 那么就是确定交叉轴上的对齐效果了
通过app:[flow_verticalAlign|low_horizontalAlign]="top|bottom|center|baseline"
top
top
bottom
bottom
center
center
以及单独设置主轴第一行和主轴最后一行的的排列模式
app:flow_firstHorizontalStyle="packed"
app:flow_lastHorizontalStyle="spread"
notion image
当主轴处于packed和spread排列模式下,通过app:flow_horizontalGap="10dp" 在这两个排列状态下增加间隔 注意是增加
默认没有增加间隔
默认没有增加间隔
添加30dp间隔
添加30dp间隔
可以看到第一行的packed和第二行的spread模式下间隔都变大了
 
 
甚至可以定义每一行最大数量,同样是在chain和aligned模式
app:flow_maxElementsWrap="3"
notion image
总结下属性对应
  • 设置整体风格wrapMode
  • 设置轴的排列风格style
  • 设置轴上的元素对齐align
  • 增加轴上元素的间隔gap
  • 主轴最大元素数量maxElementWrap

MockView

展示出像产品原型一样的View,一个非常简单的View
notion image

其他用法

关键帧动画

通过修改同一个布局中的约束,或者读取不同的约束,开启动画约束布局会会自动根据这个结束帧来完成动画,准备默认布局和结束帧
notion image
 
 
notion image
 
notion image
 

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ConstraintlayoutAnimatorBeforeBinding.inflate(layoutInflater)
        setContentView(binding.root)
        constraintReset = ConstraintSet()
        constraintSetStart = ConstraintSet()
        constraintReset.clone(binding.ctl)//保存原来的约束
        binding.btn.setOnClickListener {
            if (isOpen) {
                close()
            } else {
                open()
            }
            isOpen = !isOpen
        }
    }

fun open() {
    TransitionManager.beginDelayedTransition(binding.ctl)//开启过渡效果
    constraintSetStart.load(this, R.layout.constraintlayout_animator)//加载结束帧的约束规则
    constraintSetStart.applyTo(binding.ctl)
} 

private fun close() {
    TransitionManager.beginDelayedTransition(binding.ctl)//开启过渡效果
    constraintReset.applyTo(binding.ctl)
}
ConstraintSet 就是一堆约束的集合,除了直接加载约束布局里的规则,也可以通过手动方式修改约束
TransitionManager.beginDelayedTransition(constraintLayout);
fullConstraintSet.connect(R.id.fragment_container_detail, ConstraintSet.START, R.id.cl_card_container, ConstraintSet.START, 0);
        fullConstraintSet.applyTo(constraintLayout);
public void connect(int startID, int startSide, int endID, int endSide, int margin) 这个函数定义就是链接约束的起点和终点加上margin和布局里设置的是一样的效果
或者这样
fun open2() {
        TransitionManager.beginDelayedTransition(binding.ctl)//开启过渡效果
        constraintSetStart.clone(binding.ctl)
        constraintSetStart.constrainCircle(R.id.btn1, R.id.btn, 75.dp2px(), 0f) //0
        constraintSetStart.constrainCircle(R.id.btn2, R.id.btn, 75.dp2px(), 315f) // 315
        constraintSetStart.constrainCircle(R.id.btn3, R.id.btn, 75.dp2px(), 270f) // 270
        constraintSetStart.applyTo(binding.ctl)
    }

ConstraintProperties API调用

通过修改控件属性
val properties = ConstraintProperties(binding.btn1)
properties.translationZ(32f)
          .margin(ConstraintSet.START, 43)
          .apply()

ImageFilterButton和ImageFilterView

包装了一些简单的图片矩阵操作来更简单的控制图片的对比度、暖度、亮度等
介绍下相关属性
<declare-styleable name="ImageFilterView">
    <attr format="reference" name="blendSrc"/> 
    <attr format="reference" name="altSrc"/> 替代图片,可以交叉淡出
    <attr format="float" name="saturation"/> 饱和度
    <attr format="float" name="brightness"/> 亮度
    <attr format="float" name="warmth"/> 暖度
    <attr format="float" name="contrast"/> 对比度
    <attr format="float" name="crossfade"/> 交叉透明度
    <attr format="dimension" name="round"/> 圆角
    <attr format="boolean" name="overlay"/> 是顶层淡出还是交叉淡出
    <attr format="float" name="roundPercent"/> 圆角
    <attr format="float" name="imagePanX"/> X偏移当图片宽高比大于ImageView容器
    <attr format="float" name="imagePanY"/> Y偏移当图片宽高小于ImageView容器
    <attr format="float" name="imageZoom"/> 设置图片缩放比
    <attr format="float" name="imageRotate"/>旋转
</declare-styleable>
altSrc地位和src相似,而不是类似前景,其中对图片的属性都是直接作用在src和altSrc上的,其中的iamgeRotate也是直接作用于修饰图片,而rotation则是修饰view上的

MotionLayout

 
 
 
 

© Craig Hart 2021 - 2025