前言
ConstraintLayout从推出到现在也有好长时间了,依然没有成为主流布局,首选布局RelativeLayout,LinearLayout,FrameLayout,简单的布局一个RelativeLayout就能实现,稍微复杂点的布局就开始嵌套使用了。其实如果大家去扒一扒自己app的布局,找一个复杂点的界面,然后去看下布局结构,嵌套了多少层,或者去开发者选项去开启调制过渡重绘,看看有多少界面是红色的,甚至是深红色。布局嵌套的越多,初始化布局用于计算,定位,绘制所花的时间越多,如果这个界面再有些图片要显示,那这个界面肯定会有卡顿现象。ConstraintLayout 其实就是Google为了解决布局嵌套次数太多推出的一个新的布局,它有一个最大的好处,就是扁平化布局——几乎不需要任何嵌套。
约束
ConstraintLayout翻译过来也叫约束布局,通过各种约束条件,定位view的位置。在约束这一特点上,跟RelativeLayout的布局有些相似。
先看个效果图:
这个布局很简单,按钮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" />
添加这个属性后,布局变成这样:
而这个功能却是LinearLayout使用weight才能实现。其实这些也都还好吧,切换下布局也就能实现,但如果是下面这种效果图呢:
按钮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。我们拿一张官方图片来看看:
这张图片有三种样式比较容易理解,Spread Chain、Spread Inside Chain、Packed Chain,这三种样式只需要更改链头的chainStyle属性值就可以实现,那么其余两种又是怎么实现的呢?
Weighted Chain:
代码实现:
<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的样子:
再做一步变化:
按钮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有关。老规矩,先看图:
看代码:
<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 作为限制源来决定自身位置的一种辅助线。字面上不好理解,先看图:
类似于这种场景其实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,如图:乍一看好像也没啥吧,实际写的时候就会有各种各样的问题,要么是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"
。