说点大家的观点,有点啰嗦
1、ConstraintLayout
允许通过无嵌套视图方式创建大型而复杂的布局。类似于RelativeLayout
,所有视图均根据同级视图和父级布局之间的关系进行布局,但是它比RelativeLayout更灵活,更易于使用。
当然,这里有人是有不同意见的,所有控件都是同一个父View,会显得比较散,分模块操作时效率较低。毕竟就目前来说,也就只有Group来控制一组控件的显示与否。
2.0
之后添加的Layer
会改善这种情况。
而且就正常情况下,在ConstaintLayout里面添加一些其它ViewGroup有时也是无可避免的嘛
2、Android Studio 同时还提供特有的布局编辑器,ConstraintLayout的布局内容均可以通过拖拉拽(以及编辑器的右边属性栏)达成。
不过现在阶段,惯性思维下,对于拖拉拽的不习惯以至于大多人还在观望,即便使用上了,也会习惯性的使用编写XML的方式!
当然有时候只需要修改一行或几行属性,手写会来得快。
哦对了,喜欢手写的直接在编辑器的右边属性栏一个个添加约束,也未尝不可。
另外2.0的基于ConstaintLayout的MotionLayout
据说是特强大的动画布局,Android Studio 4.0 版本也提供了拖拉拽来实现,到时候动画可能就看你的想象力了。
3、毕竟Google对于约束布局的支持是很大的,ConstaintLayout之于RelativeLayout,就像RecycleView之于ListView,终究强者是要上位的。大势所趋!大家都在学,你不学,落后就要挨打咯。
都9102年了,别装睡了,你还能学。
共勉。
使用方式
1. 导入包:
dependencies {
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
// androidx:
// implementation "androidx.constraintlayout:constraintlayout:1.1.3"
}
目前最新版本是1.1.3。 2.0版本已经在测试中了,等到2.0,新的特性就更多了!
什么Layer、Flow、MotionLayout等。
基础使用
2. 相对定位
定义一个控件的位置,起码要使其在纵横方向各至少拥有一个相对约束---即相对于其它控件的位置。看下面这个图
布局编辑器显示控件 C 在 A下面, 但是 C并没有设置垂直方向的约束,运行时会默认在父布局的顶端,与我们所预想的发生偏差。
相对约束的基本属性格式是
layout_constraintDirection1_toDirection2Of
Direction1和Direction2可以是Left、Top、Right、Bottom
其中任意的左右
或者上下
的组合,也可以是Start、End
组合(根据从左向右布局,Start == Left,End == Right)。后续出现的Direction均代表这个属性。
从属性名我们就可直译出其代表的意思,比如:
layout_constraintTop_toBottomOf="@id/btn1",约束该控件的上边界在btn1的下边界下面,且若不设置边距(margin),则与btn1下边界在同一水平线上。
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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">
<Button
android:id="@+id/btn1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
android:text="button1"/>
<Button
android:id="@+id/btn2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/btn1"
app:layout_constraintLeft_toRightOf="@id/btn1"
android:text="button2"/>
</android.support.constraint.ConstraintLayout>
那么例子中btn2的相对约束就是在btn1的右下角。(btn1中的parent表示相对父布局的位置)
当然还有一种常用的文字基线对齐,属于垂直方向的约束,与RelativeLayout的alignBaseLine属性相似
layout_constraintBaseline_toBaselineOf
3. Margin
用于设置与其它控件的边距。
与其它Layout类型不同的是,Margin的设置依赖于控件是否有添加相应方向的相对约束。
当设置了某个方向的边界的相对约束之后,该方向设置的margin才能生效!
否则margin无效。
<Button
android:id="@+id/btn1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
android:layout_marginRight="20dp"
android:text="button1"/>
上述android:layout_marginRight="20dp"
无效。当然如果你通过布局编辑器操作的话,本身就无法写入这一条无效语句,如果你用的是xml写入的这一条,然后再去编辑器编辑该控件的时候,会发现这条语句也会被优化而删除!
特殊情况:设置了Margin的控件的visibility属性变为View.Gone
- View.Gone 的控件在约束布局中,依然可以通过findViewById()找到,只是宽高都为0dp,即视为一个点,且其每个方向的margin也都变为0
- 由于View A 已经Gone,则其他依赖于View A的,如View B的位置会有相应的变化,防止出现显示异常,View B通过可以设置
layout_goneMarginDireaction
来设置当View A Gone时候的间距。如
app:layout_goneMarginLeft="20dp"
4.圆形定位
相较于相对定位,圆形定位的属性就很简单了,只有如下三个约束
<Button android:id="@+id/buttonA" ... />
<Button android:id="@+id/buttonB" ...
app:layout_constraintCircle="@+id/buttonA"
app:layout_constraintCircleRadius="100dp"
app:layout_constraintCircleAngle="45" />
解读下就是:以ButtonA的中心点作为原点,从原点处以Y轴正半轴向右偏离45度画一条长度为100dp的线段,线段的另一个顶点为ButtonB的中心点!
值得注意的是,圆形定位优先于相对定位。
Android Studio 当前版本(3.5)并没有直接支持拖拽来写这些角度。。不写相对约束居然还飘红,有点过分
5. 居中与倾向(Biaz)
这个就有点意思
<!--水平方向添加左右两条约束-->
<TextView
android:id="@+id/tv1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@+id/btn1"
app:layout_constraintEnd_toStartOf="@+id/btn2"
/>
当控件在水平(或垂直)方向左右(上下)同时使用了相对约束,那么控件会位于两个约束控件的正中间。
比如说上述代码,tv1的约束条件是,在btn1的右边,btn2的左边,按照规定,三个控件在水平线上应该是紧紧相邻的,但是这里不可能做到,除非tv1的宽度刚好等于btn1与btn2的间距。
所以在这种约束规则下,tv1的表现为位于btn1和btn2的正中间。
当然有时候需要的不仅是居中而是中间偏左,或者偏上之类的。
那么要需要设置:
//居中默认为 0.5,取值0.0-1.0
//小于0.5即偏左(也不一定,就比如上述例子,若btn2与btn1间距小于tv的宽度
//那么小于0.5就偏右了)
app:layout_constraintHorizontal_bias = "0.5"
//垂直方向同理
app:layout_constraintVertical_bias = "0.5"
另外1,在此规则下,若将相对应的宽高设置为0dp,则控件会撑满间距!同时bias设置无效。
另外2,上述这个例子中,根据tv1的约束,若btn2在btn1的左边会发生什么呢?
实际上tv1的中心点依然会在这btn1和btn2的两条约束边界的中间,此时设置bias小于0.5时tv1会偏右。
6.宽高比
作为ConstraintLayout的子控件,其宽高一般是不支持设置为match_parent的,而是使用match_constraint
代替(xml中使用0dp表示match_constraint)。之所以是“一般不支持”,控件有在比较简单约束条件下,match_parent是和0dp等效的,所以还是用0dp就可以了。
使用match_constraint的控件最好同时有设置其左右
/上下
的约束组合!否则,有可能会真的是0dp。
进入正题,当有宽高至少有一边设置为0dp时,我们可以设置该控件的宽高比!
- 当只有一边设置为0dp:
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintDimensionRatio="1:2"
// 默认格式为,宽:高,可通过添加W或H来改变,如 "H,1:2" 为高:宽 = 2:1
此时"1:2" == "W,1:2" == "H,2:1", W/H 用于指定分子与分母
该控件宽高比会变为1:2,由于layout_width="0dp",则宽度随着高度变化而变化。
- 如若宽高都为0dp。
在这种情况下,系统将设置满足所有约束并维持指定长宽比的最大尺寸。
(这句话翻译自文档,要细品。)
假设控件A在水平的左右方向都存在约束,垂直方向只有一条约束。相对于垂直方向,A在水平方向上的宽度比较固定(等于屏幕宽度),所以高度会根据比例跟着变化。
假设控件A在水平方向以及垂直方向都有存在约束,那就可以通添加w或h来指定约束方向。
app:layout_constraintDimensionRatio="w,1:2" //或 "h,1:2"
"w,1:2"表示 宽度根据高度变化而变化,且宽高比依旧是1:2
"h,1:2"表示 高度根据宽度变化而变化,且宽高比依旧是1:2
W/H 是用于指定约束方向
7.尺寸约束
定义layout_width和layout_height的时候,同样是有三种方式:固定值、wrap_content、0dp
- 使用 wrap_content的时候,可以使用如下来限制控件大小
android:minWidth 设置布局的最小宽度
android:minHeight 设置布局的最小高度
android:maxWidth 设置布局的最大宽度
android:maxHeight 设置布局的最大高度
- 使用0dp时则可以使用:
layout_constraintWidth_min、layout_constraintHeight_min:将为此控件设置最小尺寸
layout_constraintWidth_max、layout_constraintHeight_max:将为此控件设置最大尺寸
layout_constraintWidth_percent、layout_constraintHeight_percent:将此控件的尺寸设置为父控件的百分比
辅助工具类
终于到了重中之重了,这些拓展的辅助工具类才是ConstaintLayout真香于RelativeLayout的地方。
8. Chain
链虽然没有一个具体的类,比如Chain.java,但是也算一种特殊的约束,就也归入辅助工具类吧。在2.0版本将见到更强大的Flow辅助类。
链,两个及以上的控件两两相互约束。且头尾两边的控件受约束于同一水平轴的其他非此链成员控件(比如parent),链才能正常生效。约束效果如下图
通过链头(最靠左边或上边的控件)设置如下属性来达到不同分布效果
//layout_constraintHorizontal_chainStyle
layout_constraintVertical_chainStyle = "spread_inside|spread|packed"
下述便于解说,就图上的例子,链两边控件(A和C)的约束控件为父控件parent
- Spread
默认的类型,在充分考虑了margin之后,链上的控件均匀分布(在考虑margin之后的,布局剩余的空间,均匀分配给在各个控件的间隙,包括与parent的间隙。若剩余空间为负值,即控件总长度大于父控件两边界的间距,则间隙为0,此时链居中,两边超出屏幕外的控件自生自灭)
- Spread inside
A和C控件固定在链的两端的约束上,即贴着parent,其余控件均匀分布。
相对于Spread布局,不同的是,布局剩余的空间不考虑两边控件与parent的间隙。当然若是空隙为负,表现则同Spread模式
- Weighted
加权分布,在上述这两种模式中,若有一控件将宽度设置为0dp,那么该控件将充满剩余的空间。而且,类似于LinearLayout,对于剩余的空间可以通过设置每个控件的权重属性:
app:layout_constraintHorizontal_weight = 1
//app:layout_constraintVertical_weight = 1
根据权重为不同的控件分配不同比例的空间。
- Packed
将每个控件紧贴(需要考虑margin)在一起,剩余的空间间隙分配在两边控件与parent之间。而且可以通过调节链头的bias来分配两边间隙。
若间隙小于0,表现如同Spread。
另外生成链的时候,记得使用下面这种简便形式!不然一个一个控件去添加约束,累死个人了。
9. Guideline
指导线,作为其它控件的约束准则。其它控件可以方便的通过GuideLine进行定位。
GuideLine继承于View,但并不会在布局中呈现(View.Gone)
可以通过设置下面三种属性之一来设置GuideLine的位置
app:layout_constraintGuide_begin="100dp" //与parent左边界或上边界(根据GuideLine的方向)的距离
app:layout_constraintGuide_end="100dp" ////与parent右边界或下边界(根据GuideLine的方向)的距离
app:layout_constraintGuide_percent="0.5" // 百分比, 0~1
android:orientation="vertical|horizontal" //设置方向
10. Barrier
栅栏,类似于GuideLine,设置为View.GONE,也是设置辅助线的作用,不过这个辅助线取决于多个控件的同一侧边界。当所依赖的控件大小有所变化的时候,Barrier也有可能跟着变化。
假设Barrier定义如下:
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="right" // 或left、top、bottom
app:constraint_referenced_ids="buttonA,buttonB" //引用多个控价,用逗号隔开
/>
<Button
android:id="@+id/buttonC"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button"
app:layout_constraintStart_toEndOf="@+id/barrier"
app:layout_constraintTop_toTopOf="parent"
/>
即Barrier的位置取决于buttonA和buttonB谁的右边界更靠右,而ButtonC在Barrier的右边。
这里还有个知识点:Barrier 继承于 ConstraintHelper
,而ConstraintHelper继承于View.
ConstraintHelper是用于管理一组控件的行为,与ViewGroup不同的是:1.不增加层级;2. 不同的Helper可以引用同一个控件
在2.0版本支持自定义Helper。
11. Group
这个比较简单了,也是继承于ConstraintHelper, 用于控制一组控件的显示与否。
<androidx.constraintlayout.widget.Group
android:id="@+id/group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="visible"
app:constraint_referenced_ids="button1,button2" />
值得注意的是,由于控件被包含在Group中,通过 View.setVisibility(int)来控制控件的显示与否,是无效的。
另外,当一个控件被添加在不同的Group中,此时这根据布局文件中排最后一个的Group将具有一票否决权。
12. PlaceHolder
占位是指提前设置一个绘制内容为空的控件,根据约束完成定位后,在恰当的时候将这个PlaceHolder的位置提供给其它控件使用!
设置占位并绑定指定的控件的方式有两种:
<android.support.constraint.Placeholder
android:id="@+id/pl"
android:layout_width="50dp"
android:layout_height="50dp"
app:content="@id/btn1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
或者
placeHolder.setContentId(R.id.btn1)
当一个控件A被绑定至PlaceHolder,有如下反应。
- A在原位置上会被当做View.Gone. 其它依赖于A的控件会把A当做一个点来处理。
- PlaceHolder的其它约束条件不变,宽高变成了A的宽高
- 在PlaceHolder的位置显示出A的内容
虽然目前这个功能对我来说很鸡肋,但对这个功能实现感兴趣,所以我觉得这个可以稍微了解更深点的
在源码中我们看到这几处代码:
PlaceHolder.class
//根据绑定的控件,更新PlaceHolder测量后的宽高
public void updatePostMeasure(ConstraintLayout container) {
if (this.mContent != null) {
LayoutParams layoutParams = (LayoutParams)this.getLayoutParams();
LayoutParams layoutParamsContent = (LayoutParams)this.mContent.getLayoutParams();
layoutParamsContent.widget.setVisibility(0);
// 这里
layoutParams.widget.setWidth(layoutParamsContent.widget.getWidth());
layoutParams.widget.setHeight(layoutParamsContent.widget.getHeight());
layoutParamsContent.widget.setVisibility(8); //控件不可见
}
}
//PlaceHolder.class
// 在layout()之前将绑定的控件 layoutParamsContent.isInPlaceholder = true
public void updatePreLayout(ConstraintLayout container) {
if (this.mContentId == -1 && !this.isInEditMode()) {
this.setVisibility(this.mEmptyVisibility);
}
this.mContent = container.findViewById(this.mContentId);
if (this.mContent != null) {
LayoutParams layoutParamsContent = (LayoutParams)this.mContent.getLayoutParams();
layoutParamsContent.isInPlaceholder = true; //这个属性
this.mContent.setVisibility(0);
this.setVisibility(0);
}
}
ConstraintLayout.class
// 更新绑定的控件的位置
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
int widgetsCount = this.getChildCount();
boolean isInEditMode = this.isInEditMode();
int helperCount;
for(helperCount = 0; helperCount < widgetsCount; ++helperCount) {
View child = this.getChildAt(helperCount);
...
if (child instanceof Placeholder) {
Placeholder holder = (Placeholder)child;
View content = holder.getContent();
if (content != null) {
content.setVisibility(0);
content.layout(l, t, r, b); //更改所绑定控件的显示位置
}
}
}
}
...
}
用文字来描述就是,当一个控件A被添加到PlaceHolder后,会被标记为isInPlaceholder=true
,且A被设置为View.Gone, 那么其它依赖于A的控件就会把A当成一个点。
同时在测量完成后(layout之前),将PlaceHolder的宽高修改为A的宽高
接着在onLayout过程时,A设为View.VISIABLE, PlaceHolder的布局位置让给A,即执行A.layout(l,t,r,b)(参数来自PlaceHolder)
此时虽然A可见了,但依赖于A的其它控件的大小及位置已经与A无关了。(这里没深究,大小是因为,测量时因为A为Gone,布局位置应当是与A.layoutParams.isInPlaceholder == true
相关)
Optimizer
优化器,系统会自动尝试减少视图的约束,从而提高布局速度
官方还没啥使用文档。。
给出了这么个使用方式:
添加:app:layout_optimizationLevel 到 ConstraintLayout的标签中
app:layout_optimizationLevel="direct|barrier|chain"
这个属性值有六种,standard为默认形式,即会优化direct和barrier这两种类型的约束
none : no optimizations are applied // 不优化
standard : Default. Optimize direct and barrier constraints only
direct : optimize direct constraints
barrier : optimize barrier constraints
chain : optimize chain constraints (experimental) // 实验性
dimensions : optimize dimensions measures (experimental), reducing the number of measures of match constraints elements // 实验性
关于这个几个属性,在这个问答中有比较详细的解释。大家自己看看吧。
https://stackoverflow.com/questions/49802490/what-is-constraintlayout-optimizer
猜测:上文设置无效margin约束,会自动被优化删除,可能就是这个触发的?
ConstrainsSet 与 ConstaintLayout.LayoutParams
ConstaintLayout.LayoutParams
顾名思义,就是我们布局参数了。
通常我们可以通过修改布局参数值来控制一个控件的呈现形式。
但是ConstaintLayout的特殊性,如果要做一些比较复杂的变更步骤就会变得繁琐,比如说在Chain中加入一个控件,你觉得还行?那将几个控件组成一条链呢?
所以谷歌的建议是,使用ConstrainsSet
来进行动态修改控件的参数。ConstrainsSet.createHorizontalChain(...) 就可以创建一条链,不过这里更多是需要理解方法中的参数
接着了解下使用方式:
- 生成ConstaintSet对象。
无参对象
val c = new ConstraintSet();
从已存在的layout中导出所有子控件的约束形成约束集
c.clone(context, R.layout.layout1);
c.clone(cLayout);
- 修改指定的控件的约束条件
c.setAlpha(int viewId, float alpha)
c.constrainHeight(int viewId, int height)
//设置控件间的相对约束,side的取值为1~7
//即:ConstaintSet.LEFT、ConstaintSet.RIGHT... 等上述相对布局可使用的7个Dreaction
c.connect(int startID, int startSide, int endID, int endSide)
...
//基本可通过xml设置的属性,在ConstraintSet中都能找相对应的方法
- 使步骤2中的修改生效
c.applyTo(cLayout);
// cLayout的所有控件必须都设置有viewId,因为该方法有如下判断:
if (id == -1) {
throw new RuntimeException("All children of ConstraintLayout must have ids to use ConstraintSet");
}
// 不过我注意到2.0版本是这样的,可以通过setForceId(boolean b) 来控制是否都需要设置id
if (this.mForceId && id == -1) {
throw new RuntimeException("All children of ConstraintLayout must have ids to use ConstraintSet");
}
这里有个关键地方需要提下:
- 若在第一步ConstraintSet使用的是无参数的构造方法
val set = ConstraintSet()
set.setMargin(R.id.btn1, ConstraintSet.LEFT, 300)
set.constrainWidth(R.id.btn1, 300)
set.applyTo(cLayout)
那么在setMargin()时会生成一个Constraints
对象用来存R.id.btn1的这条Margin约束。(ConstraintSet使用Map关联viewId和Constraints)
在appleTo(cLayout)的时候上述的Constraints
替换cLayout中的R.id.btn1原本定义在xml的所有约束条件!
另外还有个可能就是,cLayout不存在id为R.id.btn1的子控件。那一般就什么都不会发生。
从源码来看,对于这个cLayout中不存在的id,若我们使用了这样的代码。。
val set = ConstraintSet()
set.createBarrier(R.id.btn1, ...)
set.applyTo(cLayout)
那么btn1会被作为一个新的Barrier控件加入cLayout中(addView()的方式),同理的还有GuideLine。
本文最重要的点
- 实践出真章。多实战,你会发现还挺好用的,然后发现文中一些错误的主观观点...
- 参考自官方文档:
https://developer.android.com/reference/android/support/constraint/ConstraintLayout