ConstraintLayout 使用手册

背景

之前我们在这边文章中 Android 优化之布局优化 了解到可以通过使用 ConstraintLayout 来构建我们的布局,这也是 Android 官方推荐首要使用的,手动拖拽的方式习惯后也大大提高了我们的开发效率,如果你还没了解过 ConstraintLayout ,那就继续往下看吧。如果你已经熟练使用的话,不妨扫一眼,说不定有意外的收获。

添加约束条件

1.常规约束

创建约束条件时,每个视图都必须有两个约束条件:一个水平约束条件,一个垂直约束条件,如果我们什么约束条件都没有添加,控件就会位于ConstraintLayout 的左上角。添加约束条件非常简单,我们可以选择手动拖拽的方式或者直接手动编码的方式,个人喜欢拖拽的方式,如果有误差再在布局文件中进行微调。下面示例为 TextView 添加了上下左右四个约束条件:


可以看到,我们在拖动的过程中,布局文件也会生成相应的代码。上图演示的是链接到父布局,除此之外,我们也可以链接到其他控件中,这里不再做演示,以下是常用的约束条件:

  • 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

这些约束条件应该都可以顾名思义,一个比较特别的是 layout_constraintBaseline_toBaselineOf,它用于将一个视图的文本基线与另一视图的文本基线对齐,要创建基线约束条件,可以右键点击要约束的文本视图,然后点击 show Baseline,接着点击文本基线并将其拖到另一基线上。

有些同学可能会对 start 和 left、end 和 right 有困惑。其实如果应用只是面向国内市场的话, start 等价于 left,end 等价于 right ,因为中文的书写方向是从左到右的,但是有些语言是从右到左的书写方式,典型的就是阿拉伯语,所以 Android 从 4.2 开始推荐使用 start 、end 来代替 left 、 right,这样在切换到 RTL 语言时,UI 会自动进行镜像翻转,可以保持一致的用户体验。

2.Guideline 约束

我们可以添加垂直或水平的 Guideline 来约束视图,相当于辅助线一样,用户是看不到 Guideline 的。

通过请点击工具栏中的 Guideline


然后点击 Add Vertical Guideline 或 Add Horizontal Guideline,拖动虚线将其重新定位,然后点击引导线边缘的圆圈以切换测量模式,有 固定数值百分比 两种模式。

3.Barrier 约束

与 Guileline 类似,Barrier 是一条隐藏的线,Barrier 的位置是根据其所包含的视图的位置而移动,包含视图的属性是 constraint_referenced_ids,Barrier 可以是垂直或水平的,可以创建到引用视图的顶部、底部、左侧和右侧。以下示例,Barrier 包含了 id 为 tv_1,tv_2 的 TextView,而 id 为 tv_3 的 TextView 在 Barrier 的右侧。

代码:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">


    <TextView
        android:id="@+id/tv_1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text="我是最长长长长的"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tv_2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:text="短小如我"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tv_1" />

    <androidx.constraintlayout.widget.Barrier
        android:id="@+id/barrier"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:barrierDirection="end"
        app:constraint_referenced_ids="tv_1,tv_2"
        tools:layout_editor_absoluteX="113dp" />


    <TextView
        android:id="@+id/tv_3"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginTop="16dp"
        android:layout_marginRight="10dp"
        android:text="我是在 Barrie 的右边"
        app:layout_constraintLeft_toRightOf="@+id/barrier"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

效果:


调整视图尺寸

我们一般是通过 layout_width 和 layout_height 来为视图指定尺寸,可供的选择有 match_parent、wrap_content 和 具体的数值,ContraintLayout 亦如此,不过它多了一个选择叫作 Match Constraints,在代码中的体现是 0dp,除了可以在代码中直接设置外,我们还可以在编辑器右侧的 Attributes 窗口,点击相应的位置更改,以宽度为例,操作如下:

从动图中可以看到,当我们切换到


layout_width 变为 0dp 了,代表当前的模式为 match_contrains,此时视图是撑满的,这和 layout_constraintWidth_default 的值有关:

  • spread : 尽可能扩展视图以满足每侧的约束条件,默认值。
  • wrap : 仅在需要时扩展视图以适应其内容。这个与在 layout_width 设置为 wrap_content 的区别是 wrap 会受到约束条件的限制,即约束条件优先。而设置 layout_content 为 wrap_content 会强行使宽度始终与内容宽度完全匹配,即内容优先。
  • percent : 顾名思义,设置为百分比的形式,在设置了这个值之后,我们就可以通过 layout_constraintWidth_percent 指定百分比的具体数值(范围为 0 到 1),当然如果指定 layout_constraintWidth_default 为 spread ,设置 layout_constraintWidth_percent 属性也会生效。

调整约束偏差

当我们对某个视图两侧添加约束条件(并且同一维度的视图尺寸为 fixed 或者 wrap_cotent)时,该视图在两个约束条件之间居中,默认偏差为 0.5,对应的属性是 layout_constraintVertical_biaslayout_constraintHorizontal_bias,可以进行对其调整满足业务需求:

将尺寸设置为比例

如果视图至少有一个尺寸设置为 match_constraints(0dp),我么就可以把视图设置为比例的形式,对应的属性是 layout_constraintDimensionRatio ,如下,我们设置了宽充满屏幕,比例为 1:1 的 TextView:

当然,我们可以把宽高都设置为 match_constraints(0dp),这种情况下视图会先满足约束条件,然后把视图指定为该比例的最大尺寸。我们也可以在比例前面加一个 W,或者 H,来约束宽和高,如下:

       <TextView
        android:layout_width="160dp"
        android:layout_height="0dp"
        app:layout_constraintDimensionRatio="W,2:1" />
    <!--假设有足够的空间,最终的宽为 160dp,高为 320dp-->
       <TextView
        android:layout_width="160dp"
        android:layout_height="0dp"
        app:layout_constraintDimensionRatio="H,2:1" />
    <!--假设有足够的空间,最终的宽为 160dp,高为 80dp-->

边距(Margin)

ContraintLayout 的边距只有在有约束条件的情况下才会生效,比如下面这段代码中TextView 没有添加任何约束条件,最后它会显示 ConstraintLayout 的左上角,设置的 margin 不会生效。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <TextView
        android:background="#fff000"
        android:id="@+id/textView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="TextView"
        android:layout_margin="16dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>

需要注意的是,ConstraintLayout 的边距设置为负值并不会生效,这点和其他传统布局是有区别的。

此外,ContraintLayoutMargin 还提供了 GONE_MARGIN:

  • layout_goneMarginStart
  • layout_goneMarginEnd
  • layout_goneMarginLeft
  • layout_goneMarginTop
  • layout_goneMarginRight
  • layout_goneMarginBottom

当约束目标被设置为 View.GONE 后,设置的 GONE_MARGIN 就会生效。

圆形(角度)定位(Circular positioning)

我们可以通过一个角度和距离来约束两个视图的位置,引用官方的一张图:


对应的属性是 :

  • layout_constraintCircle ,参照视图的 id
  • layout_constraintCircleRadius ,该视图中心与参照视图中心的距离
  • layout_constraintCircleAngle ,该视图位于参照视图的角度(0° ~ 360° )·
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    
    <View
        android:background="#fff000"
        android:id="@+id/center"
        android:layout_width="50dp"
        android:layout_height="50dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
   
    <View
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:background="@drawable/bg_circle"
        app:layout_constraintCircle="@+id/center"
        app:layout_constraintCircleAngle="60"
        app:layout_constraintCircleRadius="100dp"
        tools:ignore="MissingConstraints" />
    
</androidx.constraintlayout.widget.ConstraintLayout>

上面实现的效果如下:


Chain,链控制

链是控制一组视图的,视图可以是水平链和垂直链的一部分,但是使用链并不会使链中的视图对齐在同一方向上,因此,我们要指定额外的约束条件。以下示例是创建一个水平链并以视图的顶端对齐:


创建链之后,会有一个 "链头"(Chain Head),链头是链中的第一个元素(水平链中最左侧的视图,垂直链中最顶部的视图)。链最重要是它的样式,我们可以通过选择链中的元素,右键点击 Cycle Chain mode 进行样式切换,当然也可以在链头里设置 layout_constraintHorizontal_chainStyle , 链的样式取值有以下几种(不会忽略 margin 的取值):

  • spread,视图是均匀分布的。
  • spread inside,第一个和最后一个视图固定在链两端的约束边界上,其余视图均匀分布。
  • packed,链内视图被打包一起。

当链的样式设置为 spread 或者 spread inside 时,且我们把一个或多个视图设置为 match_constraints(0dp)。默认情况下,设置了 match_constraints 的属性会把剩余空间均匀分配,但是我们可以使用 layout_constraintHorizontal_weight 和 ayout_constraintVertical_weight 属性来分配权重。这和 LinearLayout 的 layout_weight 的原理是一样的。这种方式也叫做 weight chain

另外,当链设置为 packed 的样式之后,我们可以通过链头的视图偏差 layout_constraintHorizontal_bias 属性来调整整条链的偏差。这种方式称作 packed chain with bias

下面这张来自官方的图可以帮助我们理解链的不同样式之间的区别:


Group

Group 可以把多个控件归为一组,方便隐藏或显示一组控件,相比我们在外面包一层 ViewGroup 的方法,性能上有优势。使用方式如下:

    <androidx.constraintlayout.widget.Group
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:constraint_referenced_ids="id1,id2,id3"/>

不过需要注意的是如果控件被包含在 Group 中,单独一个控件设置 View.GONE 并不会使自己隐藏掉(在 Group 为 View.VISIBLE 的情况下),这是因为设置 View.GONE 会导致重绘,调用 Group 的 updatePreLayout 方法,具体逻辑如下:

    public void updatePreLayout(ConstraintLayout container) {
        int visibility = this.getVisibility();
        float elevation = 0.0F;
        if (VERSION.SDK_INT >= 21) {
            elevation = this.getElevation();
        }

        for(int i = 0; i < this.mCount; ++i) {
            int id = this.mIds[i];
            View view = container.getViewById(id);
            if (view != null) {
                view.setVisibility(visibility);
                if (elevation > 0.0F && VERSION.SDK_INT >= 21) {
                    view.setElevation(elevation);
                }
            }
        }
    }

可以看出,updatePreLayout 方法会把 Group 内的视图的可见性设置为和 Group 的一样。

总结

随着官方的不断完善和优化,与刚出来的时候相比,ConstraintLayout 无论是使用上还是性能上都有了很大的提升,如果还没在项目中使用的同学,是时候上车了。

参考

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,036评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,046评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,411评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,622评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,661评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,521评论 1 304
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,288评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,200评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,644评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,837评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,953评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,673评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,281评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,889评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,011评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,119评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,901评论 2 355