文章

Android ConstraintLayout 约束布局使用全解析

这篇文章没有硬核,主要做一下记录。

基础

ConstraintLayout 旨在代替 RelativeLayout,但实际上不能简单地理解为加强版的相对布局,因为它加强的太多了...

ConstraintLayout 主要优点如下:

  • 提供强大的布局属性,几乎可以实现任何布局。
  • 扁平化布局。大幅提高测量性能。
  • 因为扁平化布局,所以便于实现 View 之间的过渡动画。

当然,也不是没有缺点:

  • 性能比 LinearLayout / FrameLayout 差,简单场合不建议使用。

基本约束

通常情况下,任何一个 View 至少有水平/垂直方向各一个约束。和 RelativeLayout 使用起来比较类似。基本约束的属性名规则如下:

app:layout_constraint[xxx]_to[xxx]Of="@+id/xxx"

比如 app:layout_constraintTop_toBottomOf="@+id/guideline" 表示把当前 View 的顶部与 guideline 的底部对齐。与相对布局不同,如果要与父 View 对齐,约束布局依然使用这类属性集,只是把 value 从参照 View 的 id 改成固定值 parent 就好了,API 设计更加统一。

可选的参照边有:

  • Top / Bottom
  • Start / End
  • Left / Right
  • Baseline

Baseline 是约束布局新增的参照,对齐不同文字大小的 TextView 时非常有用。

图中左边是 Baseline 对齐,代码如下(省略无关部分):

<TextView
          android:text="%"
          app:layout_constraintBaseline_toBaselineOf="@+id/textView1"
          app:layout_constraintStart_toEndOf="@+id/textView1" />

而右边是普通的底边对齐,显然不是我们希望的效果。

居中

约束布局通过两端同时约束来实现居中,非常符合直觉。想象一下拔河时左右同时拉,那么中间的标记就会居中。

<ImageView
           android:id="@+id/avatar"
           android:layout_marginStart="10dp"
           app:layout_constraintStart_toEndOf="@id/imageView"
           app:layout_constraintTop_toTopOf="@id/imageView"
           app:layout_constraintVertical_bias="0.5"
           app:layout_constraintBottom_toBottomOf="@id/imageView"/>

app:layout_constraintVertical_bias 属性可以控制垂直居中的位置百分比,可选值的范围是 [0,1],0.5 自然就是真正居中。同理 app:layout_constraintHorizontal_bias 控制水平居中的位置。

约束布局不仅可以实现在内部居中,还可以让自己被一条线居中分割。比如现代 App 常见的界面设计:

如果用传统相对布局,只能让头像与标题图下对齐,再往上偏移半个头像高度的距离。万一头像高度是动态的,就更麻烦了。在约束布局中非常好实现:利用与普通对齐一样的思路,只不过上下都参照同一个边

<ImageView
        app:layout_constraintBottom_toBottomOf="@+id/banner"
        app:layout_constraintTop_toBottomOf="@+id/banner" />

当 View 的顶部与底部都被约束到同一个边(标题图的底边)时,就可以实现图中的效果。

极坐标

上面说一般情况下一个 View 至少有水平/垂直方向各一个约束,那时在常规的直角坐标系中。约束布局还支持按照极坐标来描述位置,此时需要的是三个属性:

  • app:layout_constraintCircle: 参照的 View
  • app:layout_constraintCircleRadius: 与参照点的距离
  • app:layout_constraintCircleAngle: 与参照点的角度。垂直向上是 0 度,顺时针为正方向。

在这个场景下,View 的锚点都是中点,而不是左上角,很实用。

<androidx.appcompat.widget.AppCompatImageView
        android:id="@+id/earth"
        app:layout_constraintCircleAngle="75"
        app:layout_constraintCircleRadius="150dp"
        app:layout_constraintCircle="@id/sun" />

不仅布局起来方便,要做类似太阳系动画的话也是非常简单,只要改变角度属性就行了。

当一行或一列 View 互相约束时会自动形成链,在 IDE 中显示为锁链的线条。给链的第一个 View添加 app:layout_constraintHorizontal_chainStyle 属性(垂直链为 app: layout_constraintVertical_chainStyle)可以改变链的对齐方式,可选值有:

  • spread: 默认样式,等分空间。
  • spread_inside: 等分空间,两侧不留空隙。
  • packed: 挤在一起。

若把链方向上的 View 大小设置为 0 就会填充剩余空间,此时使用 app:layout_constraintHorizontal_weight 可以调节水平权重(垂直方向同理),形成类似 LinearLayout 的效果。比如把三个 ImageView 的宽度都设置为 0dp,权重分别为 2/1/1,效果如下:

ConstraintLayout 中宽/高设置为 0 被称为 match_constraint

奇怪的需求

有时候会有那种听起来奇怪,实际某些场景下有很自然,实现起来很麻烦的需求。

条件边距

试想一个场景:两个 View 横向排列,中间没有间隙。但如果第一个 View 隐藏了,希望第二个 View 继续显示,但是与最左侧边界有空隙。

是不是比较恶心?

约束布局提供了 app:layout_goneMarginXxxx 系列属性,用于对应方向所参考的 View 处于 GONE 状态时使用不同的边距。

限制宽度

有时把两个 View 居中对齐了,但是其中一个比较长:

现在希望这个很长的 View 被限制到和图片一样宽。如果恰好图片也是动态宽度呢?对于约束布局来说只要添加一个属性就行了:

app:layout_constrainedWidth="true"

效果如下:

在限制宽度后,如果那个很长的 View 又变得很小,就会居中显示。此时配上 bias 相关属性又可以调整位置了。一个典型的用例是聊天气泡:

限制宽高比

app:layout_constraintDimensionRatio="2:1" 属性可以限制宽高比。这种写法要求宽与高其中一个是 0dp,表示这条边根据比例计算。

那如果一条边是充满约束(0dp)怎么办?好像语义冲突了。这时宽高比的要这么写 H,2:1 表示高度按比例计算。想以高度为准宽度按比例计算的话就这么写 W,2:1

(感觉这个 API 设计的好放飞自我)

按百分比计算大小

app:layout_constraintWidth_percent 属性可以按照父 View(也就是约束布局)宽度的百分比确定子 View 的宽度,要求子 View 宽度设置为 0dp。高度同理。值域为 [0,1]。

辅助控件

辅助控件使用 id 关联其他 View。如果使用了额外的混淆工具混淆 id 需要特殊处理。

Guideline

顾名思义,就是一个参考线,其他 View 可以和它对齐。

比如一个表单,希望左侧的标签全部右对齐,右侧的输入框左对齐时,就可以在中间放一个参考线:

参考线自身有两种定位方式

  1. 百分比

    <androidx.constraintlayout.widget.Guideline
            android:id="@+id/guideline"
            android:orientation="vertical"
            app:layout_constraintGuide_percent="0.3" />
    
  2. 固定值

    <androidx.constraintlayout.widget.Guideline
            android:id="@+id/guideline"
            android:orientation="vertical"
            app:layout_constraintGuide_begin="120dp" />
    <!-- 也可以使用 app:layout_constraintGuide_end  -->
    

Barrier

Barrier 也是一种参考线,只是它的位置是自动计算的,通常位于最大的一个 View 后面。

比如这样一个需求:两行内容,每行后边有一个图标,我们希望两个图标左对齐。这就要求算出两行内容最长的那一行的右边位置,作为图标的位置。如图:

中间的那个参考线就是 Barrier:

<androidx.constraintlayout.widget.Barrier
        android:id="@+id/barrier"
        app:barrierDirection="end"
        app:constraint_referenced_ids="view1,view2" />

它关联一组 View,有四个方向可供选择,在指定的方向上计算最大位置。

Group

因为约束布局是扁平的,如果想对其中的一些 View 分个组便于一起隐藏/显示的话,就可以使用 Group

<androidx.constraintlayout.widget.Group
        android:id="@+id/group"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:constraint_referenced_ids="view,view1,view7,view8" />

它的 app:constraint_referenced_ids 属性指定了与哪些 View 关联起来,操作这个 Group 就相当于操作这些 View 了。记得 Group 只支持操作 visibilityelevation 属性。

Layer

LayerGroup 用法一样,它额外支持旋转/缩放/平移等操作。这些操作把这些 View 视为一个整体计算锚点

Layer 从 2.0 开始才支持 Group 支持的操作。

Flow

上面 GroupLayer 的源码并不复杂,稍微看一下就能理解,它不过是保存了 View id 列表,然后循环遍历操作。换句话说,ConstraintHelper 子类的 View 可以拿到所关联的 View 的实例,并修改它们。这样想象空间就大了。

其中一个应用就是把关联的 View 单独布局,比如 Flow 就可以把这些 View 线性排列,排不下时自动换行。

Flow 是一个虚拟布局,它的本质是操作关联 View 的属性,最终布局还是由 ConstraintLayout 完成的,没有增加布局层级。

Placeholder

同样顾名思义,就是一个占位符。运行时调用 setContentId() 方法可以把任意 View 放到 Placeholder 的位置,并应用 Placeholder 的约束属性,原始 View 视为 GONE。