这篇文章上次修改于 2054 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

我之前写过一篇 ConstraintLayout 的文章现在已经到了 2018 年,最新正式版本也已经到了 1.1.2 ,又加了不少好用的特性,可以说这个约束布局已经成为 Android 中最强大的布局了,绝对不是吹嘘。

本篇文章只会讲怎么使用代码画布局,可视化的方式精准度方面还是有点差强人意,如果你想了解可视化方式,请看我之前的文章。

让我们看一看这个 Android 中最强大的布局吧!

相对定位

一、基本用法

相对定位约束布局最基本也最常用的使用方式

我们先简单看一下用法

<TextView
        android:id="@+id/a"
        android:layout_width="60dp"
        android:layout_height="wrap_content"
        android:layout_marginLeft="20dp"
        android:layout_marginRight="20dp"
        android:gravity="center"
        android:layout_marginTop="30dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        android:text="A"/>

<TextView
        android:id="@+id/b"
        app:layout_constraintTop_toTopOf="@+id/a"
        app:layout_constraintLeft_toRightOf="@+id/a"
        android:layout_width="60dp"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="B" />

我们先看 A 控件,A 的位置怎么来的呢?
A 控件定位

  1. app:layout_constraintTop_toTopOf="parent" A 的顶边与 parent 的顶边对齐
  2. app:layout_constraintLeft_toLeftOf="parent" A 的左边与 parent 的左边对齐

    我们看到 A 与左边上边都有一个空隙, 这就是普通的 android:layout_marginXXXX 属性,我也就不细说了

我们再看 B 控件,B 的位置怎么来的呢?
B 控件定位

  1. app:layout_constraintTop_toTopOf="@+id/a" B 的顶部与 A 的顶部对齐
  2. app:layout_constraintLeft_toRightOf="@+id/a" B 的左边与 A 的右边对齐

    B 控件我没有设置 margin 所以他们是贴在一块的

从上面的例子我们看到了两个问题

  1. 看到两个控件 app:layout_constraintXXX_toXXXOf="xxx" 属性的值不一样,一个写的是控件 ID,一个是 parent ,这是什么意思呢?一般控件去约束需要一个参照物,这个参照物标识可以是控件的 ID ,也可以是父布局(父容器) —— parent

    就好比一根绳子一端拴在当前控件的某一位置,另一端拴在参照物的某一个位置上,这就建立起了约束。
  2. 如果仔细看上述代码,大家肯定还有一个疑问,我明明在 A 控件上设置了 marginRight 为什么 A 和 B 还是贴着的,这就有一个说法,如果一个边没有约束那么他对应边的 margin 是不生效的

OK 理解起来很简单不是吗,约束布局最基本的语法就是 app:layout_constraint位置_to位置Of="看齐目标" 那么像这种普通的相对定位的写法有多少种呢,我来给你们列举一下,再配张图标出它们的具体位置,看完下面基本的约束布局用法你就已经了解了。

控件位置展示图

layout_constraintLeft_toLeftOf
layout_constraintLeft_toRightOf
layout_constraintRight_toLeftOf
layout_constraintRight_toRightOf
layout_constraintTop_toTopOf
layout_constraintTop_toBottomOf
layout_constraintBottom_toTopOf
layout_constraintBottom_toBottomOf
layout_constraintBaseline_toBaselineOf
layout_constraintStart_toEndOf
layout_constraintStart_toStartOf
layout_constraintEnd_toStartOf
layout_constraintEnd_toEndOf
当然有一部分控件没有 Baseline 这个位置,所以这个位置不是对每种控件都有效的
二、圆形定位
<View
        android:id="@+id/a"
        android:layout_width="120dp"
        android:layout_height="50dp"
        android:background="@android:color/holo_red_light"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

<View
        android:id="@+id/b"
        android:layout_width="120dp"
        android:layout_height="50dp"
        android:background="@android:color/holo_blue_light"
        app:layout_constraintCircle="@id/a"
        app:layout_constraintCircleAngle="45"
        app:layout_constraintCircleRadius="100dp" />

偷偷放一张图片
image.png

  1. app:layout_constraintCircle 需要看齐的参照物,图中 B 就是把 A 当做参照物进行约束的
  2. app:layout_constraintCircleAngle 要旋转的角度,最上方 0 度,默认就是 0 度,顺时针开始算。
  3. app:layout_constraintCircleRadius 两个控件中心点的距离

约束链

能够在水平或垂直方向控件之间相互约束而组成的一条链就是约束链,约束链是由开头的控件进行属性控制的。没错就是跟着大哥走

  1. 普通的约束链示例

    <View
         android:id="@+id/a"
         android:layout_width="80dp"
         android:layout_height="50dp"
         android:background="@android:color/holo_red_light"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintHorizontal_chainStyle="packed"
         app:layout_constraintLeft_toLeftOf="parent"
         app:layout_constraintRight_toLeftOf="@+id/b"
         app:layout_constraintTop_toTopOf="parent" />
    
    <View
         android:id="@+id/b"
         android:layout_width="80dp"
         android:layout_height="50dp"
         android:background="@android:color/holo_blue_light"
         app:layout_constraintLeft_toRightOf="@+id/a"
         app:layout_constraintRight_toLeftOf="@+id/c"
         app:layout_constraintTop_toTopOf="@+id/a" />
    
    <View
         android:id="@+id/c"
         android:layout_width="80dp"
         android:layout_height="50dp"
         android:background="@android:color/holo_green_light"
         app:layout_constraintLeft_toRightOf="@+id/b"
         app:layout_constraintRight_toRightOf="parent"
         app:layout_constraintTop_toTopOf="@+id/a" />

    看到 A 控件就是引领小弟们的链头,所以众小弟都听他的。我们看到它的 app:layout_constraintHorizontal_chainStyle="packed" 属性。
    这个属性有三种值

  2. packed:控件紧挨在一起。还可以通过bias属性设置偏移量。
  3. spread:均与分布控件。
  4. spread_inside:均与分布控件,但是两边控件贴边。

我贴几张图大概看一下样子

当然如果是垂直的就是 app:layout_constraintVertical_chainStyle="xxxx" 属性

packed 样式
spread 样式
spread_inside 样式

  1. 组合 layout_constraintHorizontal_bias 的约束链
    如果链的样式是 packed 我们还能组合 layout_constraintHorizontal_bias 使用,我就不贴代码了,直接上图了解下
    packed 配合 layout_constraintHorizontal_bias
  2. 还有一种特殊的约束链,就是按照控件权重平分控件(明摆着抢 LinearLayout 饭碗)

    <View
         android:id="@+id/a"
         android:layout_width="0dp"
         android:layout_height="50dp"
         android:background="@android:color/holo_red_light"
         app:layout_constraintHorizontal_weight="1"
         app:layout_constraintLeft_toLeftOf="parent"
         app:layout_constraintRight_toLeftOf="@+id/b" />
    
    <View
         android:id="@+id/b"
         android:layout_width="0dp"
         android:layout_height="50dp"
         android:background="@android:color/holo_blue_light"
         app:layout_constraintHorizontal_weight="2"
         app:layout_constraintLeft_toRightOf="@+id/a"
         app:layout_constraintRight_toLeftOf="@+id/c" />
    
    <View
         android:id="@+id/c"
         android:layout_width="0dp"
         android:layout_height="50dp"
         android:background="@android:color/holo_green_light"
         app:layout_constraintHorizontal_weight="2"
         app:layout_constraintLeft_toRightOf="@+id/b"
         app:layout_constraintRight_toRightOf="parent" />

    image.png

这种链就是不设置链的 style 而是用权重的方式进行排布。

约束布局散乱特性

一、动态推测控件的宽或高

我们约束布局还支持这样的一种情况,我们控件只确定了控件的宽、高的其中一个,然后按比例算出另一边的宽高。(当然这个需求很小众)
app:layout_constraintDimensionRatio="2:1" 这个属性就是按比例推测宽高的属性

<View
        android:id="@+id/a"
        android:layout_width="100dp"
        android:layout_height="0dp"
        android:background="@android:color/holo_red_light"
        app:layout_constraintDimensionRatio="2:1"
        app:layout_constraintLeft_toLeftOf="parent" />

看以上代码 2:1 其实就是 width/height = 2/1 ,height = 50dp,所以这个属性的就是 width/height ,由此我们只要给定宽或高就能推出另一个。(当然如果你宽高都确定了设置这个属性就无效了)

这个属性的值还能设置为 (H,2:1),(W,2:1) 这样的,其实我看到之后是一脸懵逼的,但是还要硬着头皮研究,H 就是 Height,W 就是 Width,好吧说一下规律。

  1. (H,2:1)
    如果确定宽宽:高度 = 100 * 1 / 2
    如果确定高度:宽度 = 100 * 1 / 2
  2. (W,2:1)
    如果确定宽宽:高度 = 100 * 2 / 1
    如果确定高度:宽度 = 100 * 2 / 1
虽然我这样说了但是还是不推荐用。本来就是小众功能如果想用还是用普通的写法就行了,不要带什么 H,W 了。装逼太刺眼!
二、父布局填充约束

有时候有这么一个需求想把控件按照比例填充父布局。Android 屏幕适配这么复杂,好像不容易实现
这时候约束布局有两个属性 layout_constraintWidth_percent layout_constraintHeight_percent
怎么看怎么像百分比布局,这岂不是把一直不温不火的百分比布局给革命了

<View
        android:id="@+id/a"
        android:layout_width="0dp"
        android:layout_height="50dp"
        android:background="@android:color/holo_red_light"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintWidth_percent="0.3" />

填充 30% 示例
我只放这么一段代码大家应该就知道怎么用了,我也就不多了说了。

默认是居中的如果想调整位置请结合 app:layout_constraintHorizontal_bias="0.3" 食用
三、聊一聊 margin 属性

有人要说了 margin 属性还用你说?当然了我并不会说,我要说的是 layout_goneMarginStart 没见过吧?这个是什么意思呢,如果要约束控件隐藏了,B 控件位置还想保持不动怎么办呢,那么这是个什么情况呢?

  • 举个例子 B 控件左边相对 A 控件进行定位,并设置了 20dp 的 marginLeft。如果把 A 隐藏了会怎么样呢?分别看一下图1、图2。
    图1
    图2

给我们的 B 控件加上两个属性

app:layout_goneMarginLeft="100dp"
app:layout_goneMarginTop="30dp"

成品

预想的效果很棒,不是吗,从此我们看出 goneMarginXXXX 就是在约束的目标控件隐藏时才会生效的 margin。

四、WRAP_CONTENT 的小问题

如果你的控件宽或高是 wrap_content 并且控件长度过长时,他的约束会失效,我们看个例子

<View
        android:id="@+id/a"
        android:layout_width="120dp"
        android:layout_height="50dp"
        android:background="@android:color/holo_red_light"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/b"
        android:layout_width="wrap_content"
        android:layout_height="50dp"
        android:background="@android:color/holo_blue_light"
        android:text="是事实是事实是事实是事实是事实"
        app:layout_constraintLeft_toRightOf="@+id/a"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/a" />

没有超过边界的例子
超过边界约束被破坏
修复后的效果
解决方式就是使用

app:layout_constrainedWidth="true"
app:layout_constrainedHeight="true"
五、最小高度宽度,最大高度宽度

设置最小尺寸
layout_constraintWidth_min and layout_constraintHeight_min
设置最大尺寸
layout_constraintWidth_max and layout_constraintHeight_max

没啥好说的。

六、控件位置偏移

app:layout_constraintHorizontal_bias="0.3" app:layout_constraintVertical_bias="0.3"
相信这两个属性在上面大家都看过好多遍了吧,如果控件没有占满父布局,它是可以控制当前控件在父布局的空间里所占的位置,放张图理解一下。就跟拔河一样哈哈
image.png

该属性的取值范围是 0 - 1

这是横向的分析,竖向与这个一致

约束布局辅助工具

一、 辅助定位线
Guideline 是一个帮助我们来定位控件,但是他又不被用户所感知。

<android.support.constraint.Guideline
        android:id="@+id/line"
        android:layout_width="wrap_content"
        android:orientation="vertical"
        android:layout_height="wrap_content"/>

上面的最基本的写法

  1. id 是必须的不然怎么约束
  2. android:orientation="vertical" 来控制横向还是竖向,用过线性布局应该都知道这个属性
  3. 他核心的三个属性
  4. layout_constraintGuide_begin="100dp" 距离父容器起始位置的距离
  5. layout_constraintGuide_end="100dp" 距离父容器结束位置的距离
  6. layout_constraintGuide_percent="0.3" 距离父容器宽度或高度的百分比,取值范围 0 - 1
如果上述三种属性同时出现,优先级由高到低 layout_constraintGuide_percent > layout_constraintGuide_begin > layout_constraintGuide_end

二、 控件组
Group 可以同时控制多个控件的显示与隐藏

<android.support.constraint.Group
              android:id="@+id/group"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:visibility="visible"
              app:constraint_referenced_ids="a,b" />
  1. android:visibility="visible" 控制显示隐藏,也可以在代码中根据 id 获取 Group 来控制显示隐藏
  2. app:constraint_referenced_ids="a,b" 受这个 Group 管理的控件们的 id,, 号隔开

三、 控件屏障
Barrier

<android.support.constraint.Barrier
              android:id="@+id/barrier"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              app:barrierDirection="start"
              app:constraint_referenced_ids="button1,button2" />
  1. app:barrierDirection=“start” 属性可以控制这个屏障在哪个位置,具体位置可以参考文章开头那张介绍位置的图
  2. app:constraint_referenced_ids 屏障里面的控件们的 id,, 号隔开

下面分析一个例子

<TextView
        android:id="@+id/tv_name"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="姓名:sdfsdfsdsdf"
        app:layout_constraintBottom_toBottomOf="@+id/et_name"
        app:layout_constraintTop_toTopOf="@+id/et_name"/>

<TextView
        android:id="@+id/tv_phone"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:text="手机号:"
        app:layout_constraintBottom_toBottomOf="@+id/et_phone"
        app:layout_constraintTop_toTopOf="@+id/et_phone"/>

<EditText
        android:id="@+id/et_name"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:hint="请输入姓名"
        app:layout_constraintLeft_toLeftOf="@+id/barrier"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>

<EditText
        android:id="@+id/et_phone"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:hint="请输入手机号"
        app:layout_constraintLeft_toLeftOf="@+id/barrier"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/et_name"/>

<android.support.constraint.Barrier
        android:id="@+id/barrier"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:barrierDirection="right"
        app:constraint_referenced_ids="tv_name,tv_phone"/>

放一张效果图
image.png

  1. 首先把 et_name et_phone 位置放好,然后再把 tv_name tv_phone 放在屏障里,把他们当成一个整体
  2. 然后 et_name et_phone 左边约束我们的屏障
  3. 为什么 et_name et_phone 对齐的会是屏障的右边,因为 app:barrierDirection="right" 这个属性控制的,如果改成 left 就会变成如下的样子,全都和屏障的左边去对齐了

image.png

这个屏障有点稍微复杂那么一丢丢,大家多多实践一下

小结

OK 我在这里写了约束布局的一些用法,那么下一篇我将会继续絮叨絮叨这个约束布局在我们平常开发常用的一些写法和技巧!

如果还有些看不懂,请配合官方文档食用,毕竟官方资料才是我们的一手资料

参考资料

官方文档