例说 Constraint Layout:初探

android studio 相关说明 | 2018-07-19 17:45

| 导语 去年用 Android Studio 新建一个 Demo 工程时,发现自动生成的 MainActivity 的 XML 布局文件使用的竟然是 ConstraintLayout(CL,约束布局),猛然惊觉,谷歌这是要大力推行 CL 的节奏啊! ContstraintLayout 是两年多前在 Google I/O 大会上首次亮相的,这款 Android 的新布局方案很是让人惊艳,不过因为其功能、性能,以及(可视化)工具都还在不断优化中;老的布局们有能力满足日常需求,大家用得也甚是顺手,两年来 CL 并没有被大规模地使用。然而谷歌的墙裂推荐,标志着 CL 布局的技术已经发展地成熟了,之前还在持观望态度的我们,是时候来深入了解一下这个 Android 布局的终极武器了。

从本质上说来,新的 CL 同我们经常使用的 LinearLayout (LL)、RelativeLayout (RL)、FrameLayout (FL)一样,是继承了 ViewGroup 的一种 XML 布局类型,我们可以像使用其他布局一样使用它。

Figure 1. ConstraintLayout 继承关系

下图是一些常用 View、ViewGroup 或 Layout 的继承关系:

Figure 2. 常用 View、ViewGroup 或 Layout 的继承关系

1.2 约束布局的特点

然而,CL 又不仅仅是又一款普通的新布局。它有很多让人兴奋的,优秀的、甚至是独有的特性:

1. 更加扁平化的布局,更快的速度约束布局旨在使你的布局更加扁平化,你可以将布局优化至以前难以想象的精简程度:对于无需滚动控件(如:RecyclerView,ListView,GridView,etc.)的界面,即使设计得很大很复杂,也通常一层就够了!我们都知道嵌套层级的增多会大大影响布局加载的速度。RelativeLayout 需要至少调用两次子 View 的 onMeasure() 方法才能完全确定布局中所有 View 的尺寸和位置,使用了 android:layout_weight 属性的 LinearLayout、使用了 android:stretchColumns/android:shrinkColumns 的 TableLayout,也都需要遍历两次子 View 的 onMeasure() 方法。可怕的是,随着布局层级的叠加,耗时呈指数型地增加。假设我们有一个嵌套了 3 层的 RelativeLayout,那么每次其内容有变化需要 requestLayout()时,其最底端的子 View 需要进行 2^3 即 8 次 onMeaure(),一旦 View 的数量很多,RL 就可能因为速度过慢错过 16 ms 一次的帧绘制!如果是播动画时发生这个问题,用户就会看到明显的卡顿了。这也是为什么我们常常说,尽量不要用 RL 作为嵌套层数很多的布局的父容器。而“崇尚”扁平化布局的 ConstraintLayout 中有很多专为减少嵌套层级而设计的属性。扁而浅的布局使其性能突出,同时又很符合我们的思考方式,撰写起来方便而优雅。如图三中的谷歌 Demo 里的约束布局,谁能想到,这个完整又不失设计感的界面只有一层,完全没有嵌套!

Figure 3. 谷歌 Demo 里约束布局

(关于约束布局的性能探讨,将会在本文续篇《例说 Constraint Layout:性能分析》里详细讨论。)

2. 增加了新属性,功能强大,编写便捷从编写布局文件的思考方式来看,ConstraintLayout 同 RelativeLayout 很相似,从根布局容器到子 View,都按照它们之间的相互关系——即 constraints——来排布,但是 CL 比之 RL 属性更多、更完善,也更加灵活,能完成很多 RL 无法完成的任务,可以说是 RelativeLayout 的全面升级版。实际上,所有其他布局管理器能做的,ConstraintLayout 基本上都能做到,它可以同时具备好几种布局管理器的功能。下面我们来看个例子:RelativeLayout 是无法实现一行中多个 View 等间隔排列的布局的,譬如下图这样五个 icon 等间隔排布的顶部栏。

Figure 4. RelativeLayout 无法实现的顶部栏

以前我们要借用到 LinearLayout 和它的 android:layout_weight 属性才能实现这样的布局,代码如下:

但是这样实现是有缺点的,前面说了,LL 一旦使用了 android:layout_weight 属性,它就会要求子 View 进行两次 onMeaure();而且因为第五个 icon 是由两个 View 组成的,上面的布局一共就会有 3 层;同时,为了避免首尾两个 icon 在端部也有留空,使用了 4 个看不见的 View 来定位,导致 View 个数的整体增加。如果使用 ConstraintLayout,我们可以轻松实现此布局,而且能有多种实现方式,并且都是无嵌套的单层结构!(不同实现方式的详细例子参见续作《例说 Constraint Layout(二)——属性详解》。)再譬如:RL 只能把某个 View 作为一个整体,限制它相对父容器或其他兄弟 View 某一边的位置;而 CL 可以规定一个 View 的任意一边或中心、甚至是文字的基线相对于父容器或其他兄弟任意边、中心、文字基线、guideline 的位置。简单粗暴地说,就是你可以在任意位置,相对于任意东西,放置任意 View。这只是 CL 增加的能力中很小的一部分,却增加了不少灵活性和可控性。而更难得的是,这是在性能提升的同时做到的!

3. 屏幕适配和多分辨率设计更简单Android 的屏幕适配一直是一个耗时耗力的工作,CL 的不少属性,如:bias,可以使我们更简单、更好地布局 UI ,并在不同尺寸、不同分辨率的屏幕上都达到一致地、符合设计意图的效果。

4. 更好的应对布局改变CL 相对于传统布局,能更好地适应布局上的变化。当一个 View 的可见性被设置为 GONE时,相对于它的那些 View 仍能保持在正确的位置上,也能根据情况重新布局(详见续作《例说(二)》)。

5. 借助 Android Studio 的可视化工具,能更迅速的完成界面布局一般我们写一个 XML 布局文件时,都习惯于直接打开 XML 文本进行编辑,但是 ConstraintLayout 不一样,可以说它是和 Android Studio(AS) 自带的布局编辑器(Layout Editor)的可视化功能一起,从无到有被实现的。两者是互相量身定做、相辅相成的,所以使用布局编辑器来创建 CL 可以让你更方便快捷地完成布局。

当然,任何布局都有其缺点和使用限制, CL 也是有缺点的:使用它会使你的 APK 增大 150k 左右;其性能负担也会随着界面中的控件数量以及约束条件的增加而增加,等等。但是瑕不掩瑜,总地说来,ConstraintLayout 在各方面都表现出色,它更强、更快、更简单、更灵活、更全面、更(程序员)友好。如果说有哪个最上层的布局是每个人每次写 XML 布局文件时都应该首先考虑使用的,那它一定是 ConstraintLayout。(谷歌把自动生成的布局的根节点从 RelativeLayout 变为 ConstraintLayout 自有其道理。)

下面我们来看一下怎么将 ConstraintLayout 应用到我们的工程中。ConstraintLayout 是一个向前兼容的、独立成包的 Support 库,最早可以兼容至 Android 2.3(API 9,姜饼版本)。目前市场上手机基本都是 2.3 及以上版本,所以 CL 完全可以满足我们的一般需求。

我们只需要在 build.gradle 文件中添加如下依赖,即可在工程中使用 CL 了:

dependencies {        compile 'com.android.support.constraint:constraint-layout:1.0.2'}注意,这会使你的 APK 增大 150k 左右。

前面已经谈到,ConstraintLayout 和 Layout Editor 是互相为对方量身定制的,来看看谷歌的 CL 开发者是怎么说的:

I wanted to do a new layout Editor. Something that would be really powerful and easy to use. In order to do that, I felt I needed a ViewGroup that was more powerful and flexible - something that would work better with an ID. The result is ConstraintLayout and it’s very flexible. It allows you to easily create a flat hierarchy, which is a good thing.—— Nicolas

CL 和 LE 犹如一对共同怀上、共同诞生龙凤胎,使用 Layout Editor 可以最大限度地发挥 CL 的能力和优点。所以比起传统的直接操作 XML 文件的方式,强烈建议大家改用 LE (的 Design 标签)来编辑约束布局。在此我不详说 LE 的各种细节了,只做一个整体的介绍,并提及一些我以为的重点,其它的相信各位攻城狮随便玩几下就上手啦~

使用 Layout Editor 首先需要将 Android Studio 升级到 2.2 或以上版本。安装完成后,打开一个布局 XML 文件,将编辑器窗口底部的标签页从 Text 切换到 Design,既可以从传统的 XML 编辑模式切换为可视化操作模式。总体说来,新的可视化编辑器非常直观,它主要组成部分如下:

Figure 5. 布局编辑器主界面

调色板(Palette)相当于一个控件选择面板,列出了布局中可用的控件和布局。LE 的可视化基本用法很简单,常规部件都可以通过拖拽释放来添加到布局中,也可以使用鼠标拖动来添加对应的约束。比如我们想要向布局中添加一段文字,那么只需要从左侧的 Palette 区域拖一个 TextView 进去就可以了。

组件树(Component Tree)展示了布局的视图层级,点击其中一项,即可在工作区中选中它。在其中也能看到 View 之间的嵌套结构,当然,往往 CL 的所有 View 都在扁平的一层内。

工具栏提供了可以用于配置布局外观和属性的按钮。最左上角的三个就是用于切换下面的设计编辑区展示哪些视图的。

设计编辑区此区域是开发者用于定义 View 之间关系的界面,也是编写布局时停留时间最多的界面。可视化工作区显示了特定屏幕和主题下,当前你所编写的 UI 的样子。它可以展示两个类似于手机屏幕的界面,分别是两种视图预览模式,设计视图和蓝图视图。两者可以辅助着进行布局编辑和预览,非常直观和好用。(你也可以自行选择如何预览布局:既可以让设计视图和蓝图视图并列显示,也可以只显示其中任一个。)设计视图主要用于预览最终的界面效果,采用彩色界面,它默认不显示约束,除非你的鼠标在上面停留。蓝图视图仅显示各部件的轮廓线,主要用于观察界面内各个控件的约束情况。可以将蓝图模式想象成“X 光”模式,就像我们的 X 光片高亮显示了人体密度最高的部分——骨骼——一样,蓝图模式是 XML 中最重要的内容——属性——的视觉表现形式,省略了无关的细节,突出体现了布局的特征。

属性面板(Properties)此面板罗列了选中的 View 的所有具体属性及它们的值,如文本内容、颜色、点击事件等等。我们也可以在此对各属性进行修改和操作。

新版的 AS 自动生成新 XML 布局时,默认使用约束布局,然而我们工程中有无数既有的布局,它们都不是 CL,如果要想人肉将它们都转换成 CL 来提高性能,工作量会非常大。不过不用担心,AS 提供了转换器,可以很方便将其转换为 CL:打开需要转换的布局 XML 文件 -> 在组件树(Component Tree)面板中右键选中想要转换的(根)布局中的任意元素 -> Convert XXXXXLayout to ConstraintLayout -> 在弹出的确认对话框中选择“OK”。

Figure 6. 右键选中想要转换的(根)布局

Figure 7. 自动转换弹出的确认对话框

铛铛~我们的布局就被秒转成了 ConstraintLayout。

之前举的顶部栏的例子,不算是特别复杂的布局,我们来看一下它经过转换后的效果:

Figure 8. 自动转换后的顶部栏

可以看到,第二个按钮的位置莫名地跑到了最右边(被第五个遮挡了),而且这时候如果我们关注一下组件树(Component Tree)的话,可以发现这个转换后的约束布局,第五个按钮还是存在嵌套结构,即使我们在转换时将弹出的对话框(图七)里的第一项 Flattern Layout Hierarchy 勾选了也没有起任何作用。

Figure 9. 自动转换后的顶部栏的层级结构

所以说自动转换的效果目前还不尽如人意,尤其是一些复杂的布局,还是需要大家再手动修正一下。

如果我们使用可视化工具,通过拖曳 & 释放将一个控件添加到 RelativeLayout 中,它相对其它元素的位置关系会自动被推断并应用上,App 运行起来后呈现的效果就我们在编辑器中看到的样子。而如果对 ConstraintLayout 做相同的操作,就很有可能会发现把 App 跑起来后,编辑器的设计或蓝图视图中所见的不一定即是手机上的所得!我们创建一个 CL,并拖放两个 ImageView 进去,在不做任何修改的情况下,它在 AS 的布局编辑器中的呈现如图十:

Figure 10. 一个约束布局在编辑器视图中的样子

然而将程序跑起来后,在手机上我们看到所有添加的控件都堆积到了左上角:

Figure 11. 此约束布局在手机上的样子

造成位置偏移的根本原因是:每一个 View 都至少需要有 2 个约束(Constraints),一个竖直方向的和一个水平方向的,来确定它的位置。将控件拖曳到 CL 中时,默认是不会自动生成约束的。所以 App 运行起来后,没有足够约束的 View,最终会因为失去“支撑”而“掉落”到屏幕的左上角,有点类似我们的 FrameLayout。而在编辑器视图中,View 之所以没有错位,是因为 AS 会在 View 添加后自动增加属性来表示他们在编辑器中的位置。这个布局代码如下:

如果我们把其中自动生成的和编辑器相关的属性——tools:layout_editor_absoluteX="xxx",tools:layout_editor_absoluteY="xxx"——删除,就发现在编辑器中控件的位置同手机上的一般无二了。

Figure 12. 删除编辑器相关属性后,此约束布局在视图中的样子

这些编辑器相关的属性仅仅是为了我们在编辑时预览方便而设置的,编译后并不起作用。因此手机上 View 错位的问题的根本解决方案是,为 View 添加缺失的约束(除非本来就希望 View 放置在 (0, 0) 位置)。

在 3.2.2 中,我们看到必须为控件添加足够多的约束来将其安放在想要的位置上。当然我们可以手动为 View 添加约束,然而约束布局自然是到处都是约束,每一个 View 至少需要 2 个约束,常常我们会使用到 4 个,甚至可能会有 5 个约束的情况,如果这些都需要我们手动添加的话,工作量不小。幸而 AS 有自动化的工具可以帮我们做到这一点:

应用 infer constraints 功能当我们将 View 拖曳到我们想要的位置后,可以点击 infer constraints 来自动生成约束。编辑器会扫描当前整个布局,推断出对所有 View 最有效的约束。它会在保证机动性的情况下,尽力将 View 约束在当前拖曳到的位置。

打开 Autoconnect 功能Autoconnect 功能打开后(默认为关闭状态),当控件被放置到布局内时,编辑器就会自动创建 2 个或更多个必要的约束将控件相对于其父布局摆放。注意,Autoconnect 功能并不会为 View 添加相对于布局内其它 View 的约束。对其它非 CL 的布局,Autoconnect 的行为略有不同,会相应添加适用于此种布局的属性。当然无论是上述那种方式,开发者还是需要对自动推断结果进行适当的调整才能获得满意的效果,尤其要考虑对不同屏幕尺寸及方向的适配。

编辑器的另一个很实用的功能是,它会给出关于布局中存在的错误和警告的提示。譬如 3.2.2 中所指出的缺少约束的问题,就会被提醒。大家在完成一个布局的编辑前,将提示中的问题都解决为佳。

Figure 13. 编辑器工具栏上的错误和警告提示

Figure 14. 关于约束缺失的警告

好了,关于 ConstraintLayout 的基本情况就先讲解到这里,此文仅是个热身,敬请期待后续关于 CL 的属性详解 & 性能分析。

作者简介:opalli(李科慧),天天P图 Android 工程师