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

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:

<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"/>这样就完成了一个基本的定位,以此类推
角度定位

前面有个定位在左下角,类似的效果可以通过View中心点来进行角度定位
app:layout_constraintCircle设置目标 app:layout_constraintCircleAngle设置角度app:layout_constraintCircleRadius设置中心到中心距离比如:

<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类似

比如:此时文本2是基于基线对齐文本1的

<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,以此类推

<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等操作就可以实现在父布局种上下左右的定位

边距
- 普通边距
- Gone边距
普通边距
普通边距和Android基本用法一样通过margin_Start等来添加,有一点要注意只有存在约束margin才会生效

Gone边距
约束布局独有的
Gone_Margin当约束指向的目标可见性变为GONE的时候,目标View会变成一个点,而自身的app:layout_goneMarginStart="100dp" 就会发挥作用,android:layout_marginStart="10dp" 则会失效。
<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会自动在两端约束拉扯下保持居中,比如这个下面这些居中,只要确定两端约束指向目标就能保持居中

偏移
除了通过margin来产生偏移,约束布局提供另一种方式
app:layout_constraintHorizontal_bias="0.5" 来设置水平位置偏移app:layout_constraintVertical_bias="0.2" 来设置垂直偏移取值范围就是0-1

<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_parent、wrap_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"/>
这样就可以更具父布局设定尺寸了
比例高宽

通过设置图片高度为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"/>链式
当约束布局中多个控件在水平或者垂直方向上,他们的约束互相连接着,看上去像是一个双向链表的感觉,如图:

此时就是一个链式,功能上其实就有点类似线性的布局感觉,但是更强大。通过对链的头设置不同的链式风格来实现不一样的摆放效果,有三种风格
app:layout_constraintVertical_chainStyle="spread|spread_inside|packed"CHAIN_SPREAD 
CHAIN_SPREAD_INSIDE
CHAIN_PACKED
以及设置头节点来伴随偏移

控件权重
在线性布局中有个权重,在约束布局中同样有,前提是要形成链式,设置
app:layout_constraintVertical_weight 属性
<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"/>
辅助工具
Optimizer
Guideline
在约束布局中可以通过参考线来最为参照,而参考线在实际渲染是不可见的。
android:orientation="vertical" 参考线方向
app:layout_constraintGuide_begin="10dp" 参考线距离开始位置
app:layout_constraintGuide_end="10dp" 参考线距离末尾位置
app:layout_constraintGuide_percent="0.5" 参考线基于百分比位置
Barrier
屏障和参考下类似也不会显示在实际的页面,但是屏障是基于几个控件形成的一条屏障,会随着控件变化而变化的。
在这样一个应用场景下,右边的文字总是需要出现在左边两个文字右侧,而且不会产生遮挡,但是有可能在这两个TextView在不同的语言下长度可能有所不同,所以单独基于哪个TextView来约束都会有问题,于是通过基于二者的右侧屏障就不会有问题了,屏障会是根据二者来计算的



<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" />
Group
通过Group可以声明将多个控件分组,通过对组的控制来统一控制这些控件可见性
app:constraint_referenced_ids="id,id" 加入组的控件id
<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


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被摊开在一行
aligned 对齐风格
chain 链式风格
在chain和aligned风格下,此时每一行其实都有点类似于之前用到的链式,所以可以设置每一条链上主轴的排列模式,packed、spread、spread_inside
app:flow_horizontalStyle="spread_inside|packed|spread" 水平线上的排列如果是竖直排列计算的
app:flow_verticalStyle="spread_inside|packed|spread" 垂直线上的排列


主轴上的定位已经确定好了, 那么就是确定交叉轴上的对齐效果了
通过
app:[flow_verticalAlign|low_horizontalAlign]="top|bottom|center|baseline" 


以及单独设置主轴第一行和主轴最后一行的的排列模式
app:flow_firstHorizontalStyle="packed"app:flow_lastHorizontalStyle="spread"
当主轴处于packed和spread排列模式下,通过
app:flow_horizontalGap="10dp" 在这两个排列状态下增加间隔 注意是增加

可以看到第一行的packed和第二行的spread模式下间隔都变大了
甚至可以定义每一行最大数量,同样是在chain和aligned模式
app:flow_maxElementsWrap="3"
总结下属性对应
- 设置整体风格wrapMode
- 设置轴的排列风格style
- 设置轴上的元素对齐align
- 增加轴上元素的间隔gap
- 主轴最大元素数量maxElementWrap
MockView
展示出像产品原型一样的View,一个非常简单的View

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



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上的