从多个角度介绍约束布局设计中的控件定位。
01、约束布局基础
从 Android Studio 2.3版本起,约束布局是Android Studio布局文件的默认布局。其他布局方式在实现复杂一些的布局设计时存在多种或多个布局嵌套的情况,设备调用这样的布局文件就需要花费更多的时间。约束布局在灵活性和可视化方面比其他布局方式更胜一筹(与号称宇宙第一IDE的Visual Studio的图形化界面设计相比还有差距)。为减少布局嵌套,使用约束布局的属性更接近于相对布局属性。因此,很多资料在讲解约束布局时采用与相对布局类似的方式,不可避免地就要讲解一堆约束布局的定位属性。约束布局与其他布局的最大区别在于支持图形化拖放操作。可以在布局文件的Design视图中采用鼠标拖放操作结合属性栏窗口设置完成约束布局的界面设计,大幅简化布局代码输入和控件间定位关系的人为判断。
【注】
约束布局可以实现图形化拖放设计,但不是真正的所见即所得。Design视图中的效果与实际运行结果还是有所不同的,经常出现的问题是定位属性设置错误或缺项。
选择布局文件main.xml,在Design视图的工具栏中拖放两个Button按钮到Design界面中,如图1所示。
■ 图1 约束布局Design视图
此时由于未加入约束定位,组件树中的控件都会用红色惊叹号标识。运行程序,两个按钮会显示在屏幕左上角,坐标为(0,0)。
选中按钮控件,显示控件句柄,如图2 所示。4个角上的正方形句柄用于调整控件的尺寸,圆形句柄用于设置控件的定位。
■ 图2 控件句柄
在“按钮1”左侧圆形句柄上按住鼠标左键并拖向屏幕左侧,此时“按钮1”会自动靠到屏幕左侧,意味着“按钮1”与屏幕左侧对齐。选中“按钮1”再次向右拖动到如图3所示的位置,4个方框圈出数字为132,单位为dp,代表“按钮1”相对父容器(此时为屏幕左边缘)的距离为132dp。也可以在属性栏或布局栏(图中右侧中间的Constraint Widget)中修改数字改变距离值。以上操作对应两个属性:
■ 图3 “按钮1”相对父容器水平方向定位
注意,一个前缀是“app:”,另一个前缀是“android:”。前者引用app目录下build.gradle文件中androidx.constraintlayout:constraintlayout定义的属性;后者引用系统定义的属性。
同样,选择“按钮1”上方的圆形句柄定位屏幕上边缘距离。重新运行程序,“按钮1”将出现在设定的位置。“按钮2”还是在屏幕左上角。可以采用同样的方式对“按钮2”进行操作,也可以采用相对“按钮1”的位置进行定位。例如,将“按钮2”定位在“按钮1”下方50dp位置,“按钮2”右侧离屏幕右侧100dp。为实现上述定位要求,先选中“按钮2”上方的圆形句柄并拖动到“按钮1”(此时“按钮1”上会出现上、下两个圆形句柄)下方的圆形句柄,调整距离值为50dp。如果进行此操作,鼠标左键释放时指向“按钮1”区域而非圆形句柄,会弹出如图4的上下文菜单,可选择“按钮2”是对齐“按钮1”顶部还是底部。
■图4 设定“按钮2”相对“按钮1”的垂直方向定位
选择“按钮2”右侧的圆形句柄并拖动到屏幕右侧,调整距离值为100dp,“按钮2”的定位如图5所示。
■ 图5 “按钮2”的定位
约束布局运行结果如图6所示。
■ 图6约束布局运行结果
设计图与运行图相比,两个按钮的显示位置还是有差异,这是因为设计图并不是按实际设备的屏幕分辨率来设定的,距离真正的所见即所得还有一定的差距。当控件定位属性不全时,组件树会有红色圆形警告标识
,Code视图的控件标签也会标红,同时会多出一个前缀为“tools:”的属性。例如,tools:layout_editor_absoluteX="180dp"指明当前控件在Design视图中的X轴坐标是180dp。此属性只在Design视图中起辅助定位时使用,在运行时还是被忽略的,即运行时控件在X轴坐标还是0(对齐屏幕左侧)。
如果对“按钮1”在水平方向分别将左边缘和右边缘依次定位到屏幕两侧,Android Studio会将“按钮1”在水平方向自动置中,定位标尺直线变为折线,其含义是最终定位还需考虑其他定位属性。Design视图显示水平方向双重定位,如图7所示。运行程序时,“按钮1”也会显示在屏幕水平方向正中央。
■ 图7水平方向双重定位
此时“按钮1”隐含以下属性(此时布局文件中不会显示此属性):
只要将“按钮1”水平拖动,如图8所示,将多出属性layout_constraintHorizontal_bias,其代表左右定位尺寸(左右圆形句柄到定位基线的距离,定位基线可能是屏幕边缘,也可能是其他控件边缘)的偏离率,0.5代表按钮左右定位长度相等,小于0.5时按钮偏向左边,大于0.5时按钮偏向右边。可以在属性栏中直接修改偏离率,也可以在Inspector(layout_constraintHorizontal_bias属性,如图8标注Constraint Widget的图形部分)中直接拖动带数字的进度条修改偏离率,还可以通过直接拖动“按钮1”的方式或在Code视图中修改偏离率。
■ 图8 layout_constraintHorizontal_bias属性
如果希望“按钮1”无论怎么左右移动,按钮左侧到屏幕左边缘均至少保留50dp的间隙,可先选中“按钮1”,然后在Inspector左侧文本框中输入50,layout_marginStart属性如图9所示。“按钮1”左侧折线连接一段50dp的直线,相应的layout_marginStart属性为50dp,此时偏离率是不计算这50dp直线长度的,即使app:layout_constraintHorizontal_bias属性值等于0,“按钮1”左侧还是会保留50dp的空白,此时拖动“按钮1”到50dp时就无法再向左边移动。
■图9 layout_marginStart属性
选中控件的圆形句柄,按delete键可以删除选中的定位。如果选中的是控件,按delete键会删除整个控件(含控件定位属性)。
Inspector如图10所示。
■ 图10 Inspector
表示wrap_content,Code视图中的属性为android:layout_width="wrap_content"。
表示固定值,给控件指定了一个固定的长度或者宽度值。
表示任意长度或者宽度值。以控件宽度为例,Code视图中控件的宽度属性变为android:layout_width="0"。配合其他定位属性,控件宽度可能为wrap_content(左右圆形句柄只有一个用于定位)、match_parent(左右圆形句柄都用于定位)或任意长度(左右圆形句柄都用于定位,且同时定义了layout_marginStart或layout_marginEnd)。
在“按钮1”上右击,弹出控件快捷菜单,如图11所示。
■ 图11 控件快捷菜单
选择Show Baseline菜单,会在“按钮1”上显示Baseline(即基准线),如图12所示。单击“按钮1”的基准线并拖放到“按钮2”的基准线位置就可实现基准线对齐。基准线对齐主要用在多个高度不同的控件间实现文字对齐(如果按钮中文字显示为多行或两个按钮的字体大小不同,基准线对齐是将第一行文字底部对齐)。
■ 图12 Baseline
选择Clear Constraints of Selection菜单,将删除选中控件的所有约束布局属性。
选择Convert view菜单,弹出转换控件窗口,如图13所示。选择想变更的控件类型(Android中称之为View),可以在保留定位数据的情况下变更控件类型。
■ 图13 转换控件窗口
其他菜单项是常用选项,如复制、粘贴等,还有几个菜单项在设计图上方的工具栏中有相同功能按钮。相关功能可参看后续内容。
Transforms如图14所示。在Transforms中可设置View的X、Y、Z轴的旋转和坐标参照点的偏移。
■ 图14 Transforms
图14中对Z轴旋转了45°,对应属性android:rotation="45"。如果定义在按钮控件内,则对应按钮控件旋转45°。如果定义在ConstraintLayout标签内,则ConstraintLayout标签内的所有控件都会旋转45°。
第7行定义约束布局内所有控件都旋转-45°。此时button1和button2都旋转-45°。
第25行定义button2旋转90°,扣除约束布局旋转的-45°,最终效果是button2旋转45°。
android:rotation属性运行结果如图15所示。其前缀是android,属于android命名空间,所以也可以用在其他布局中,如线性布局。android:rotation是以控件中心且垂直于屏幕为轴心的Z轴旋转。android:rotationX和android:rotationY分别对应控件X方向中心轴和Y方向中心轴旋转。
■ 图15 android:rotation属性运行结果
02、Barrier
在实际布局中可能会遇到Barrier定位,如图16所示。希望“按钮3”布置在“按钮1”和“按钮2”最右侧40dp的位置,即如果“按钮2”比“按钮1”更靠右,则“按钮3”左侧距离“按钮2”右侧40dp;如果“按钮1”比“按钮2”更靠右,则“按钮3”左侧距离“按钮1”右侧40dp。
■ 图16 Barrier定位
为此约束布局引入了Barrier的概念,增加了Barrier标签,其中定义以下两个属性:
以上两行是在“按钮1”与“按钮2”右侧建立Barrier,Barrier类似一堵墙,两个按钮谁更靠右,这堵墙就以谁为边界。而“按钮3”左侧定位以这个墙为基准。使用图形化界面建立Barrier方法如下:建立“按钮1”与“按钮2”并完成相应定位。选中“按钮1”,单击
按钮,弹出如图17所示的界面,选择Add Vertical Barrier菜单添加一个垂直方向Barrier。
■ 图17 添加垂直方向Barrier
默认添加的Barrier与“按钮1”左侧对齐。修改Barrier属性,如图18所示,在barrier属性栏中修改barrierDirection属性为right(或者是end)、constraint_referenced_ids属性为“button1,button2”(默认只有button1)。如果事先已经明确“按钮1”和“按钮2”共同建立Barrier,也可同时选中“按钮1”和“按钮2”,然后再添加垂直方向的Barrier,constraint_referenced_ids属性自动填写为“button1,button2”。
■ 图18 修改barrier属性
选中“按钮3”,添加左侧定位到任意控件右边缘,使“按钮3”的Declared Attributes属性栏中多出一项layout_constraintStart_toEndOf属性(也可以在All Attributes中查找对应属性),将其改为要对齐的Barrier,将layout_marginStart改为40dp。Android Studio自动生成布局文件,代码如下:
第35行定义button3左侧对齐到barrier1右侧。
第38~44行定义barrier1,其中第43行定义barrier1阻挡的控件是button1和button2,第42行指明barrier1阻挡方向是右侧。
03、Guideline
之前的控件定位都是基于屏幕(更准确的称呼为控件父容器的约束布局)或者是可见控件,约束布局中引入了一种在运行时看不见的Guideline——定位基准线作为定位补充。添加Guideline如图19所示,分别添加垂直和水平方向的Guideline。
拖动Guideline至所需位置,指定左定位150dp的垂直方向Guideline,如图20所示。
■ 图19 添加Guideline
■ 图20 指定左定位150dp的垂直方向Guideline
单击左侧向上箭头将切换为向下箭头,此时Guideline按Bottom位置定位,再次单击将切换成百分比符号,代表Guideline使用位置百分比设置自身定位(Guideline属性app:layout_constraintGuide_percent="0.33"是将Guideline设置在屏幕长度或宽度的1/3位置),指定百分比的水平方向Guideline,如图21所示。
■ 图21 指定百分比的水平方向Guideline
【注】
目前约束布局版本水平方向的Guideline可通过鼠标单击实现
三种定位方式循环切换,垂直方向的Guideline的切换还有问题。约束布局的功能在不停地升级,或许下一版本会将这个问题解决。
添加控件并将其定位指向Guideline,使用Guideline定位如图22所示。实际运行时Guideline是不可见的。
■ 图22 使用Guideline定位
布局代码如下:
第8~13行定义了一个距离屏幕左边界150dp的垂直方向Guideline。
第15~20行定义了一个距离屏幕上端1/3位置的水平方向Guideline。
第22~29行定义了一个按钮,其顶端和左侧分别定位到两个Guideline。由于Guideline是一条线,因此第28行layout_constraintStart_toStartOf换成layout_constraintStart_toEndOf的效果是一样的。
04、Group
使用约束布局的一个目的是减少布局的嵌套。如果想把多个控件设置为隐藏,就需分别设置各控件的可视化属性。使用Group相当于将各控件进行分组,设置Group属性等效于将Group中各控件设置相同属性。在Design视图中添加Group时需先选择要包含的控件,然后如图23所示选择Add Group菜单项添加的Group对象,新添加Group对象取名为group1。
■ 图22 添加Group
group1在程序运行时是不可见的,要将相应的控件放入group1,最便捷的方式是在组件树中将相应的按钮控件拖放到group1,如图23所示。
■ 图24 将按钮添加到group1
此时group1中包含了button1和button2。Group的关键代码如下:
其中,第5行定义group1中包含button1和button2。运行程序,可以看到屏幕上显示button1和button2。改变group1的visibility属性为invisible,再次运行程序,屏幕上不显示button1和button2。需要注意的是,group1的属性栏中有两个visibility属性,如图25所示。
■ 图25 visibility属性
上方的visibility属性在Code视图中显示为android:visibility,其设定的值影响相关控件在Android设备上是否显示。下方的visibility属性前有一个
符号,在Code视图中显示为tools:visibility,其设定只影响在Android Studio的Design视图中是否显示,并不影响在Android设备上的运行显示。
05、Circle
Circle方式定位目前还无法使用图形界面操作,可以在Code视图中输入代码。Circle定位示意图如图26所示。以参考定位控件A中心为圆点,以控件B中心到控件A中心为半径,与垂直向上方向的夹角来定位控件B的位置。
■ 图26 Circle定位示意图
第23行定义button2按照Circle方式来定位,定位基准为button1。
第24行定义夹角为45°。
第25行定义button1与button2中心点的距离为150dp。
目前,Circle方定位方式还不完善,除了不支持图形化拖曳设计以外,在Code视图下Button标签也会显示为红色,组件树中button2也会用红色惊叹号标识,提示相关约束属性不完备。
06、Chain
Chain用于指定链式约束。Chain示意图如图27所示。图27中,控件A与控件B相互约束形成一个简单的链式约束。
■ 图27 Chain示意图
链式约束中的第一个控件称为Chain Head。在整个约束链中只要在Chain Head中定义链式约束类型即可。Chain Head示意图如图28所示。
■ 图28 Chain Head示意图
在Head控件中添加layout_constraintHorizontal_chainStyle属性,ChainStyle属性及示意图如图29所示。5种链式约束类型的主要区别是控件间的间隔或控件自身宽度比例。如果Head控件中未设置layout_constraintHorizontal_chainStyle属性,则默认为Spread Chain类型。
■ 图29 ChainStyle属性及示意图
以下是根据官方代码编写的案例,分别实现图350的5种链式约束类型。
5种链式约束类型运行结果如图30所示。
■ 图30 5种链式约束类型运行结果
在Code视图中按钮的文本是小写,但在Design视图和运行结果中都是大写,这是因为从Android 5.0起按钮的属性默认为android:textAllCaps="true"。只要在按钮属性中增加android:textAllCaps="false"即可恢复原有的大小写显示。相应的Java命令为button.setAllCaps(false)。