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年了,别装睡了,你还能学。

共勉。

/   正文   /

使用方式

导入包:

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等。

基本使用

相对定位

定义一个控件的位置,起码要使其在纵横方向各至少拥有一个相对约束---即相对于其它控件的位置。看下面这个图:

ConstraintLayout 嵌套NestedScrollView constraintlayout布局新特性_宽高

布局编辑器显示控件 C 在 A下面, 但是 C并没有设置垂直方向的约束,运行时会默认在父布局的顶端,与我们所预想的发生偏差。相对约束的基本属性格式是:

layout_constraintDirection1_toDirection2Of

Direction1和Direction2可以是Left、Top、Right、Bottom其中任意的左右或者上下的组合,也可以是Start、End组合(根据从左向右布局,Start == Left,End == Right)。后续出现的Direction均代表这个属性。

ConstraintLayout 嵌套NestedScrollView constraintlayout布局新特性_控件_02

从属性名我们就可直译出其代表的意思,比如:

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表示相对父布局的位置)。

ConstraintLayout 嵌套NestedScrollView constraintlayout布局新特性_控件_03

当然还有一种常用的文字基线对齐,属于垂直方向的约束,与RelativeLayout的alignBaseLine属性相似。

layout_constraintBaseline_toBaselineOf

ConstraintLayout 嵌套NestedScrollView constraintlayout布局新特性_宽高_04

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。

ConstraintLayout 嵌套NestedScrollView constraintlayout布局新特性_宽高_05

由于View A 已经Gone,则其他依赖于View A的,如View B的位置会有相应的变化,防止出现显示异常,View B通过可以设置layout_goneMarginDireaction来设置当View A Gone时候的间距。如:

app:layout_goneMarginLeft="20dp"

圆形定位

相较于相对定位,圆形定位的属性就很简单了,只有如下三个约束。

<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的中心点!

ConstraintLayout 嵌套NestedScrollView constraintlayout布局新特性_宽高_06

值得注意的是,圆形定位优先于相对定位。Android Studio 当前版本(3.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的正中间。

ConstraintLayout 嵌套NestedScrollView constraintlayout布局新特性_android_07

当然有时候需要的不仅是居中而是中间偏左,或者偏上之类的。那么要需要设置:

//居中默认为 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会偏右。

宽高比

作为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 是用于指定约束方向

尺寸约束

定义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的地方。

Chain

链虽然没有一个具体的类,比如Chain.java,但是也算一种特殊的约束,就也归入辅助工具类吧。在2.0版本将见到更强大的Flow辅助类。

链,两个及以上的控件两两相互约束。且头尾两边的控件受约束于同一水平轴的其他非此链成员控件(比如parent),链才能正常生效。约束效果如下图。

ConstraintLayout 嵌套NestedScrollView constraintlayout布局新特性_控件_08

通过链头(最靠左边或上边的控件)设置如下属性来达到不同分布效果。

//layout_constraintHorizontal_chainStyle
layout_constraintVertical_chainStyle = "spread_inside|spread|packed"

ConstraintLayout 嵌套NestedScrollView constraintlayout布局新特性_宽高_09

下述便于解说,就图上的例子,链两边控件(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。另外生成链的时候,记得使用下面这种简便形式!不然一个一个控件去添加约束,累死个人了。

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" //设置方向

ConstraintLayout 嵌套NestedScrollView constraintlayout布局新特性_android_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的右边。

ConstraintLayout 嵌套NestedScrollView constraintlayout布局新特性_宽高_11

ConstraintLayout 嵌套NestedScrollView constraintlayout布局新特性_android_12

这里还有个知识点:Barrier 继承于 ConstraintHelper,而ConstraintHelper继承于View。ConstraintHelper是用于管理一组控件的行为,与ViewGroup不同的是:

  1. 不增加层级
  2. 不同的Helper可以引用同一个控件

在2.0版本支持自定义Helper。

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将具有一票否决权。

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,有如下反应。

  1. A在原位置上会被当做View.Gone. 其它依赖于A的控件会把A当做一个点来处理。
  2. PlaceHolder的其它约束条件不变,宽高变成了A的宽高
  3. 在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。