ConstraintLayout 的解析,扁平化布局

前言

ConstraintLayout从推出到现在也有好长时间了,依然没有成为主流布局,首选布局RelativeLayout,LinearLayout,FrameLayout,简单的布局一个RelativeLayout就能实现,稍微复杂点的布局就开始嵌套使用了。其实如果大家去扒一扒自己app的布局,找一个复杂点的界面,然后去看下布局结构,嵌套了多少层,或者去开发者选项去开启调制过渡重绘,看看有多少界面是红色的,甚至是深红色。布局嵌套的越多,初始化布局用于计算,定位,绘制所花的时间越多,如果这个界面再有些图片要显示,那这个界面肯定会有卡顿现象。ConstraintLayout 其实就是Google为了解决布局嵌套次数太多推出的一个新的布局,它有一个最大的好处,就是扁平化布局——几乎不需要任何嵌套。

约束

ConstraintLayout翻译过来也叫约束布局,通过各种约束条件,定位view的位置。在约束这一特点上,跟RelativeLayout的布局有些相似。


先看个效果图:


图1

这个布局很简单,按钮2在按钮1右边,如果用RelativeLayout实现的话:

<RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <Button
            android:id="@+id/btn1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true"
            android:layout_alignParentTop="true"
            android:text="按钮1" />

        <Button
            android:id="@+id/btn2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentTop="true"
            android:layout_toEndOf="@id/btn1"
            android:layout_toRightOf="@id/btn1"
            android:text="按钮2" />
    </RelativeLayout>

换成ConstraintLayout实现:

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">


    <Button
        android:id="@+id/btn1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="按钮1"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="@id/btn2"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btn2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="按钮2"
        app:layout_constraintLeft_toRightOf="@id/btn1"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

实现的效果都是一样的。那我们来对比下这两种实现方式有什么不同:
btn1中有三个属性:
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@id/btn2"
app:layout_constraintTop_toTopOf="parent"
从字面上就可以理解:

app:layout_constraintLeft_toLeftOf:左对齐,设置为parent为与父布局左对齐。 对应RelativeLayout类似于layout_alignParentLeft和layout_alignLeft的两个属性;

app:layout_constraintRight_toLeftOf:右对左,简单的就可以理解为btn1的右对齐btn2的左侧。对应RelativeLayout类似于layout_toLeftOf属性;

app:layout_constraintTop_toTopOf:上对齐:设置为parent为与父布局左对齐。对应RelativeLayout类似于layout_alignParentTop和layout_alignTop两个属性;

btn2与btn1不同在于这个:

app:layout_constraintLeft_toRightOf:左对右:简单的理解为btn2左对齐btn1的右侧。对应RelativeLayout类似于layout_toRightOf属性;

其中从这个简单的例子上就可以看出ConstraintLayout和RelativeLayout有些不同:ConstraintLayout可以相互约束,RelativeLayout却不能相互“约束”——btn2可以toRightOf btn1,而btn1却不能toLeftOf btn2。

原因在于RelativeLayout布局解析是从上而下,btn1解析的时候还没解析btn2,而btn1却引用btn2的id,就会报错;解析到btn2的时候btn1已经解析完毕,btn2引用btn1的id时就不会有问题。

注:如果发生编译错误,那就在错误的地方使用@+id替换@id

如果只实现单约束条件:即btn2的左对齐btn1的右,也是可以实现上述需求。那相互约束又有什么优势?在btn1有一个属性app:layout_constraintLeft_toLeftOf="parent",那如果我们对btn2也来一个右对齐父布局,会产生什么效果呢?

<Button
        android:id="@+id/btn2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="按钮2"
        app:layout_constraintLeft_toRightOf="@id/btn1"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

添加这个属性后,布局变成这样:


图2

而这个功能却是LinearLayout使用weight才能实现。其实这些也都还好吧,切换下布局也就能实现,但如果是下面这种效果图呢:


图3

按钮1和按钮2紧挨着,仅仅使用单个RelativeLayout或单个LinearLayout和两个Button布局,是无法实现图3的功能,要么添加一个View辅助布局,要么布局嵌套实现,但无论是哪种实现,都会增加额外的开销。
那如果用ConstraintLayout又该如何实现呢?看代码:
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/btn1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="按钮1"
        app:layout_constraintHorizontal_chainStyle="packed"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="@id/btn2"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btn2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="按钮2"
        app:layout_constraintLeft_toRightOf="@id/btn1"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

对比原来的布局文件,细心的朋友会发现多了一行:

app:layout_constraintHorizontal_chainStyle="packed"

没错,只需要添加这个属性,就可以实现图3的样式。

Chains链

上面提到一个属性就行解决的问题,到底是何方神圣?其实这就是ConstraintLayout特有的Chains链。Chains链大家可以简单的理解为在同一级(垂直或者水平)条件下,相近的View有相互约束的存在,便构成了Chains链结构,位于最左侧或者最上侧的View被称为链头,想要实现特殊样式,只需要更改链头的chainStyle属性即可,且只有链头的chainStyle属性更改会生效,其他View的chainStyle属性设置了也不会起作用。
链头的属性有三种:

  • spread

  • packed

  • spread_inside

默认不设置的话属性是spread。我们拿一张官方图片来看看:


Chains链

这张图片有三种样式比较容易理解,Spread Chain、Spread Inside Chain、Packed Chain,这三种样式只需要更改链头的chainStyle属性值就可以实现,那么其余两种又是怎么实现的呢?

Weighted Chain:
图4

代码实现:

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/btn1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="按钮1"
        app:layout_constraintHorizontal_chainStyle="spread"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="@id/btn2"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btn2"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="按钮2"
        app:layout_constraintLeft_toRightOf="@id/btn1"
        app:layout_constraintRight_toLeftOf="@id/btn3"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btn3"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="按钮3"
        app:layout_constraintLeft_toRightOf="@id/btn2"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

只需要将btn2和btn3的width设置为0dp即可。这地方需要做一点答疑,为什么是0dp而不是match_parent?因为在ConstraintLayout中,若要使用约束条件,match_parent被0dp所替代。意思就是说,如果你将btn2的0dp换成了match_parent,那么整个布局里就只有btn存在,其余布局全部遮挡,就是图5的样子:

图5

再做一步变化:


图6

按钮1:按钮2:按钮3 = 1:3:2的样式分布,看代码:

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/btn1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="按钮1"
        app:layout_constraintHorizontal_chainStyle="spread"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="@id/btn2"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btn2"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="按钮2"
        app:layout_constraintHorizontal_weight="3"
        app:layout_constraintLeft_toRightOf="@id/btn1"
        app:layout_constraintRight_toLeftOf="@id/btn3"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btn3"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="按钮3"
        app:layout_constraintHorizontal_weight="2"
        app:layout_constraintLeft_toRightOf="@id/btn2"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

多了这个属性app:layout_constraintHorizontal_weight,这个属性就比较容易理解了,跟LinearLayout的weight相似,对剩余空间的划分利用,但前提是view的width必须设置成0dp才会有效。

Packed Chain With Bias

看其名知其意,肯定是与bias有关。老规矩,先看图:


图7

看代码:

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/btn1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="按钮1"
        app:layout_constraintHorizontal_chainStyle="packed"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="@id/btn2"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintHorizontal_bias="0.2"/>

    <Button
        android:id="@+id/btn2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="按钮2"
        app:layout_constraintLeft_toRightOf="@id/btn1"
        app:layout_constraintRight_toLeftOf="@id/btn3"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btn3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="按钮3"
        app:layout_constraintLeft_toRightOf="@id/btn2"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

首先app:layout_constraintHorizontal_chainStyle = packed,其次所有的View宽都是自适应,然后链头多一个属性app:layout_constraintHorizontal_bias = 0.2。简单的讲,链头左边的距离占剩余未利用的空间的比例。设置为0.2意思就是链头左边空余空间占据总剩余未利用空间的1/5。

学会上面这些,ConstraintLayout基本就掌握了80%了,来写布局啥的基本上没有什么问题,剩下的一些东西都是为了提高效率。

辅助工具类Group

业务开发中,难免会遇到这种情况,根据条件判断,某些View要展示,某些View要隐藏。基本上这些强关联性的View都会具有相同的visibility属性,那么按照以前的写法,就要每个View都需要设置一遍visibility才行。但是,现在有了Group辅助类,就不需要那么麻烦了。
先看代码:

<android.support.constraint.Group
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:constraint_referenced_ids="btn1,btn2,btn3" />

app:constraint_referenced_ids这个属性的意思是关联具有相同visibility属性View的id,关联上之后,给Group设置android:visibility="gone",三个Button就会全部隐藏,从控制三个View到控制一个View,效率提升很多,代码也变简洁了。

辅助类Guideline

顾名思义,就是一个辅助线,当约束条件变的比较困难的时候,可以使用Guideline来辅助定位,可以使用属性android:orientation来确定是横向还是纵向。
GuideLine有三个属性来控制定位:

  • layout_constraintGuide_begin:距离左侧或者顶部的距离;
  • layout_constraintGuide_end:距离右侧或底部的距离;
  • layout_constraintGuide_percent:百分比控制,指定在父控件中的宽度或高度的百分比,如0.8,表示距离顶部或者左侧的80%的距离。

辅助类Barrier

Barrier 是用多个 View 作为限制源来决定自身位置的一种辅助线。字面上不好理解,先看图:


图8

类似于这种场景其实app中还是比较常见的,左边TextView后边EditText,输入一些信息。通常的做法是左边的ViewGroup限制宽度,右边一个ViewGroup保证所有的EditText起始位置保持一致;亦或是找到TextView中文案最长一个的宽度,将其他TextView的宽度也设置成最长的那个,然后右边EditText,也能做到图8这样的效果。但问题是最长的宽度,你怎么能够确定就是那一个呢?如果哪天某个文案变了,原本宽度最大的变成第二大的,是不是代码也要跟着改一大堆保证右侧EditText起始位置一致呢?那么现在,有了Barrier 一切都变的简单了。上代码:

<android.support.v7.widget.AppCompatTextView
        android:id="@+id/tv_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="姓名:"
        android:textSize="18sp" />

    <android.support.v7.widget.AppCompatTextView
        android:id="@+id/tv_phone"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:text="电话号码:"
        android:textSize="18sp"
        app:layout_constraintTop_toBottomOf="@id/tv_name" />

    <android.support.v7.widget.AppCompatEditText
        android:id="@+id/et_name"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:hint="请输入姓名"
        android:textSize="18sp"
        app:layout_constraintBottom_toBottomOf="@id/tv_name"
        app:layout_constraintLeft_toRightOf="@id/barrier"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <android.support.v7.widget.AppCompatEditText
        android:id="@+id/et_phone"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:hint="请输入电话"
        android:textSize="18sp"
        app:layout_constraintBottom_toBottomOf="@id/tv_phone"
        app:layout_constraintLeft_toRightOf="@id/barrier"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="@id/tv_phone" />

四个View,然后:

<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" />

先看app:barrierDirection这个属性是指定限制的方向,有6个值,分别是 , 有 left,right,top,bottom,start,end。app:constraint_referenced_ids这个属性跟Group的类似,约束View的id。

Barrier约束两个TextView,同时设置属性为right,表示在俩TextView的右侧,而俩EditText以Barrier为约束条件,标明在Barrier的右侧。而Barrier的位置能根据俩TextView文案的长短,自动的调整位置。这样无论我们改多少个字,改哪个TextView,都不需要去担心右侧对齐的问题了。上两张图,便于理解:



虚线就是Barrier自动调整的位置。

百分比视图

ConstraintLayout确实很强大,将百分比的布局功能也加了进来,而且设置起来非常方便。使用场景最多的就是首页Banner,先上图:



Banner宽高比设置为横向的16:9,上代码:

<android.support.v7.widget.AppCompatImageView
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:scaleType="centerCrop"
        android:src="@mipmap/ic_launcher"
        app:layout_constraintDimensionRatio="H,16:9"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

app:layout_constraintDimensionRatio就是这个关键属性,有三种写法:

  • 直接写X:X,表示宽高比;
  • 写了H与不写H效果一致,也是表示宽高比;
  • 写了W,表示高宽比

补充:2019/12/30

1.最近有这么一个需求,要求一个TextView后面跟一个Image,image紧跟着TextView,如图:
TIM图片20191230160641.png

当TextView超出一行时,TextView多余文案用...代替,而image固定在最右边。如图:
TIM图片20191230160659.png

乍一看好像也没啥吧,实际写的时候就会有各种各样的问题,要么是image不能跟随要么就是image被顶没了。如果按照以往的想法,那就是重写一个布局,实时计算空间预留,不过很麻烦,现在用ConstraintLayout就可以完美实现:

<TextView
        android:id="@+id/tv2"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:ellipsize="end"
        android:singleLine="true"
        android:text="Hello Worldxxxxxxx!xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toRightOf="@id/tv1"
        app:layout_constraintRight_toLeftOf="@+id/tv3"
        android:layout_marginRight="10dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintWidth_default="wrap" 
        app:layout_constraintHorizontal_chainStyle="packed"
        app:layout_constraintHorizontal_bias="0"/>

    <ImageView
        android:id="@+id/tv3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toRightOf="@id/tv2"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

关键属性是TextView的android:layout_width="0dp"app:layout_constraintWidth_default="wrap",这两个属性组合起来使用的大致意思就是,既能让TextView的宽跟随文案长度自动适配,又能让文案顶到一行时TextView的宽度固定,不再占用其他布局空间。
以上俩属性是关键属性,如果要做到图上那种效果,还有俩属性需要设置:app:layout_constraintHorizontal_chainStyle="packed"app:layout_constraintHorizontal_bias="0"。这俩属性就不再多做介绍了。

PS:新版本中,app:layout_constraintWidth_default="wrap"这个属性已经被标为了过期的属性,替代属性:app:layout_constrainedWidth="true",要想达到同样的效果,需要设置TextView的宽为wrap:android:layout_width="wrap_content"


结语

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

推荐阅读更多精彩内容