ConstraintLayout相关属性用法理解及入门
前言:ConstraintLayout作为google钦定的代替LinearLayout及RelativeLayout的布局值得我们学习,并且google仍旧想借此布局继续改善之前布局可视化编辑上的鸡肋效果,但由于当下版本新的可视化效果在布局编辑器上与实际运行结果仍旧可能略有不同,我们还是很难在不编辑代码的情况下仅靠拖拽来编辑布局,所以本文不会从拖拽入手,而是从布局各种相关API的代码层面学习该布局的使用
对于ConstraintLayout来讲,为了更好的在实际应用过程中使用它,有许多重要的概念及规则需要了解熟记,通过本文,你不仅能看到ConstraintLayout官方说明的译文,还将通过多个事例来模拟实际使用中容易遇见的问题,希望以抛砖引玉的方式引出实际应用中可能遇到的坑,并加以避免
1.layout_constraintA_toBOf
1.1基础概念

这里的A与B的取值分别为下图的top、left、right等,并有形如top-bottom、start-end的对应关系,并且这种对应关系是必然的,下面我们会说明原因

例如:
app:layout_constraintTop_toBottomOf="@+id/"
所表达的意思为令控件A的top边与控件B的bottom边共享同一块相同的位置(share the same location)
代码运行结果为:

所以我们就能理解不可能出现layout_constraintTop_toLeftOf这样的API,因为一个是竖边,一个是横边,不可能共享同一块位置
其他的API以此类推很好理解,那baseline的运行结果会是如何?为了对比,我们先看如下代码的运行结果:

我们令B在A的右边,然后设置B的宽高与字体大小大于A,运行结果如我们所预想:

然后我们设置:
app:layout_constraintBaseline_toBaselineOf="@+id/tv_a"
运行结果会不会如我们所想呢:

观察结果可知,由于B的高度大于A,所以B上面的一部分移动到了屏幕之外,并且由于B字体大小大于A,所以即便是指定BaseLine在同一位置,B的文字也要高于A
我们再修改代码,为父布局加入paddingTop,看能否使被屏幕遮盖的布局“移下来”:

观察结果:

B移到了父布局的外部,在父布局上加入padding会导致子布局的内容整体下移,这与我们在其他布局中的经验是一致的,既然这样的话,若我们直接在A上设置marginTop能否让A“带着”B一起下来呢,我们继续修改代码:

运行结果:

观察结果我们发现A并没有按照预想挪下来,究其原因是由于我们没有为A布局设置约束(Constraint),所以A不知道该如何偏移,我们为A添加约束,并将A的约束对象设置为父元素ConstraintLayout本身:

观察代码运行结果:

综上,约束布局中,若期望设置某控件的margin来控制显示位置,则必须指定该控件在该方向上的约束:


官方解释为如果设置控件的margin,它会施加在存在的对应约束上(If side margins are set, they will be applied to the corresponding constraints (if they exist))
1.2居中
查找发现API没有为我们提供例如layoutGravity或centerInParent之类的方法让我们把一个控件在约束布局内居中,但是可以通过官方提供的方式来达到目的

指定让控件的宽高与父布局重叠并不会令该控件铺满父布局,除非父布局与控件拥有完全相同的大小,不然只会令控件在指定的方向上居中

当控件布局成这种状态下时,我们可以通过设置偏移(bias)来改变控件的默认(50%)位置:


1.3由居中写法引申而来的问题
有了以上的基础概念,我们来分析几个典型的例子,在进一步理解的同时也能发现其中的一些问题
1.3.1
上面居中代码中,我们将A的左右边与父布局的左右边设定约束,最终的结果为A居中于父布局,若我们将A的右边与B的右边设定约束,效果会如何呢:

运行结果:

可见A被从父布局的左边“推离”了,而在与B大小相等的位置“居中”,此时我们便能隐约开始理解网上一些其他教程在翻译官方说明时对于原文(What happens in this case is that the constraints act like opposite forces pulling the widget apart equally)的译文的含义了。
1.3.2
假设我们有如下代码:

我们将A的左边与父容器的左边设定约束,然后将A的右边与B的左边设定约束,此时A既约束于父容器也约束于B容器,然后将B的右边与A的左边相约束,然后设置A的左右margin为20dip,设置B在4个方向上的margin也为20dp,运行结果为:

我们发现A与B之间的位置关系是正确的,B控件只有左间距,这也是正确的,符合我们上面得出的间距只在约束方向上有效的结论,但是A控件的左右间距都失效了,感觉有点奇怪有问题,但此处其实我们是创建了一种特殊的约束布局结构--链(chain),对于链的定义、介绍以及为何margin感觉失效的问题我们下面会详细说明,此处先有个印象。但如果我们此时去掉B相对于A的约束,仅依靠A相对于B的约束能否保持这个位置不变呢,为了方便观察结果,我们将A与B在布局文件中书写的顺序互换:

运行结果为:

可见,A控件在尝试以一个诡异的位置“居中”,并且B的位置并没有依照所想能在A的右边,究其原理,必然是约束布局内部的计算方法导致其在此类情况下计算出可居中的宽度及位置并尝试居中,具体的算法还有待翻看源码后总结,此处暂时存疑并有待补充,接下来我们修改间距大小:

观察结果:

我们可以确认一点,在这种情况下,对A布局设置的margin是有效的,但前提是我们没有将A与B相互约束。
2.约束对象可见度为GONE的情况:
约束布局中,我们可以通过设置:
layout_goneMarginX
属性,来设置约束的控件对象可见度为GONE的情况下的margin数值

具体用法非常好理解,我们不再赘述。但值得注意的是,约束布局下被设置为GONE的元素依旧是布局的一部分,与其他布局的区别在于其尺寸变为了0,而不是脱离布局
3.约束布局内子控件的宽高设置
官方说明常用方式为:
a.设置具体的宽高数值
b.设置宽高为wrap_content
c.设置宽高为0dp,以代表MATCH_CONSTRAINT
对于c方式,约束布局中不能直接使用match_parent来设置控件的宽高,直接设置后IDE会自动转换成0dp,这里我们有必要举个例子与上面的居中例子做对比来说明0dp如何MATCH_CONSTRAINT

观察结果:

C控件并没有如我们所想match,我们修改C的属性:

运行效果:

我们再思考一个例子,假设我们在此基础上再加入一个控件D,我们希望此控件位于A的正下方,并且宽度与A相同,我们写下了如下的代码:

运行效果:

发现D在指定的两条边中间居中,符合我们上面的测试结果,现在我需要令D的宽与A一致,修改代码:

运行结果:

可以发现,0dp指的是MATCH_CONSTRAINT,而不是match_parent,需重点理解
d.设置ratio属性
通过使用:
app:layout_constraintDimensionRatio="X,A:B"
属性,可以用比例的形式更灵活的指定控件的宽高,意思为系统会以最大的尺寸为依据,加之宽高比,计算得出0dp所对应的数值,其中X指的是要约束的边,取值为W或H 且可以不填,A与B为具体的比例数值,代表宽:高,我们继续修改控件D的属性以具体查看:

我们设置D的宽为0dp,以告知系统宽需要计算得出,高设置为wrap_content,然后设置ratio的值为16:9,运行结果:

控件D以最大尺寸的边 高为基准,加之以宽高比16:9共同计算出宽度,我们继续修改代码:

控件D的宽高均置为0dp,代表均需计算得出,然后指定宽高比为16:9,此时系统会设置最大尺寸以满足所有约束并保证宽高比:

在此基础上,我们为ratio添加要约束的边,

运行效果:

此时我们的意思是宽作为最大尺寸,加之宽高比16:9,计算出的高效果与不添加一致,若我们改为约束宽:

运行效果:

可以看出,当我们选择约束宽时,意思变为宽作为最大尺寸,加之高宽比为16:9,计算出的高会比宽要大
既然如此,那就说明此种情况下宽高的大小是互为影响的,我们继续修改代码:

我们将D依赖约束的控件A的宽增加至80dp,观察效果:

D的宽随着A的增大而增大,这同时也会导致D的高随之增大,这也是实际应用中需要注意的一点
4.链(chain)
链是一种特殊的约束布局结构,有点像数据结构中的双向链表,其定义就是多个控件相互约束而形成的结构,并拥有头(head)的概念:

链拥有4种风格(style):
a.CHAIN_SPREAD:默认,链会正常的分离开(spread out)
b.CHAIN_SPREAD_INSIDE:与默认风格类似,但此种模式下的链头及链尾不会分离开
c.CHAIN_PACKED:此模式下的控件会被打包(packed)在一起,可以理解为挤在一起而不是分离开,此时指定bias属性可以改变默认的偏移量,类似我们在居中小节中介绍的那样
d.Weighted chain:权重链,在默认风格的情况下,如果某一个控件使用0dp设置宽高,即被设置为MATCH_CONSTRAINT,则叫做权重链(*网络上有其他教程将其译作加权链,这是非常明显的直接套用百度翻译的结果,按照官方定义的理解,此模式更像我们在LinearLayout中使用的权重(weight)属性,所以我们依此思路将其叫做权重链更符合原意)。

还记得我们在居中小节测试代码现象时,无意中写出了这种互相约束的链结构,并在设置链头的margin时发现是无效的,对此官方给出了说明:
在连接处设定的margin会被计算在内,而对于spread(默认)模式下的链,设定的margin会被扣除(If margins are specified on connections, they will be taken in account. In the case of spread chains, margins will be deducted from the allocated space)
按照说明,CHAIN_SPREAD模式下对链头设置的margin是无效的,那其他模式应该是有效的,我们验证一下:

上面代码我们通过指定控件A和C的宽为0dp(MATCH_CONSTRAINT)的形式创建了一组权重链,按照官方说明,非默认链的情况下链头的margin应该是有效的,观察代码结果:

结果确是如我们所想,控件A由于只在左右两边设置了约束,所以设置的20dp只生效于左右两边。
最后,对于其他模式我们没有太多需要说明,但对于权重链来讲,当多个控件被指定为0dp即MATCH_CONSTRAINT后,默认它们将用可用空间平分彼此,但你同样可以使用layout_constraintHorizontal_weight或layout_constraintVertical_weight属性来指定它们的权重,就像我们在LinearLayout中做的那样,权重规则也与LinearLayout中的weight属性相同,具体效果我们就不再赘述了。
5.参照线(Guideline)
Guideline非常像我们PS中经常使用的标尺,作用也非常相似,同样可以用来帮助我们定位控件 但更强大,我们举个很简单的例子,如果我现在约束布局中只有一个控件,那它会因只有父布局可以约束而无法布局在屏幕中的位置,此时我们便可以设置Guideline并令控件相对于其做约束来控制控件的位置,Guideline原理上是一个被设置成View.GONE且宽或高为0的控件,所以你不会在页面上真正的看到它,当然Guideline的作用可不仅仅这么简单,利用Guideline可以让我们的布局工作变得更加灵活及高效,我们先尝试一下简单的例子了解它,另外的章节我们会结合实际项目去掌握它。
我们可以通过
android:orientation="X"
属性来在指定方向创建一条Guideline,X可指定为vertical及horizontal,分别对应横向及竖向的Guideline,再结合
app:layout_constraintGuide_X
属性来指定Guideline来的位置,X可指定为begin、end及percent,根据Guideline方向的不同分别代表从上(左)、下(右)及百分比,属性的值代表具体的位置数值:

我们创建了2条Guideline,一条横向一条竖向,并设置位置为100dp,然后令控件A约束于它们,观察效果:

可以看到,控件A显示在了我们期望的位置
结语:终于,我们深入学习完了有关于ConstraintLayout常用的属性,也调试了这些属性在各种情况下的效果,ConstraintLayout的作用并不仅仅是简单融合并代替线性布局与相对布局,更是一种全新的布局思想,这种思想甚至将影响我们在页面动画动效上的设计,在下一篇文章中,我们将结合实例去尝试使用代码来动态构建ConstraintLayout,并接触ConstraintLayout真正有用有趣的用法。