看过Android新特性介绍,ConstraintLayout完全解析的朋友们,这是上一篇的补充。
小伙伴们好久不见,我又回来啦。 说实话这篇文章写的算是比较晚了,距离ConstraintLayout出现至今已经有一年了。 且自AS2.3起创建新的Activity,默认的layout根布局就是ConstraintLayout。 所以再不学习就真的晚了。 我也是正式开始学习的道路,先说一下我的学习过程: * 先阅读了ConstraintLayout官方文档和Guideline官方文档 * 实践每个属性并记下笔记(翻译) * 学习了郭神关于(拖拖拽拽)的博客,发现博客中对Chain的概念没有提及 * 查询关于Chain以及一些疑点的资料 * 整理成文 * 当然中间也遇到了许许多多的问题
本文的顺序,大体按照ConstraintLayout官方文档的顺序依次讲解(翻译)属性和用法,并对疑难点进行额外说明。 关于可视化操作,可参考我写的动态图解&实例 ConstraintLayout Chain和Android新特性介绍,ConstraintLayout完全解析
引入也有坑,无力吐槽。 先放上 截止至20170524,最新版本1.0.1:
坑是啥?因为我使用的是最新的release版AndroidStudio2.3.2,新建Activity后,自动帮我引入的是1.0.8-alpha版本, 开始我就这么愉快的学习了,可是当我学习到Chain相关姿势时,特码的,他居然报错。说找不到属性:
Error:(10) No resource identifier found for attribute ‘layout_constraintHorizontal_chainStyle’ in package ‘com.mcxtzhang.constraintlayoutdemo’
ok,那我百度,显然搜不到的,ok,那我再google,特么的居然也搜不到。 震惊,于是机智的我去看源码,发现我使用的1.0.8-alpha版本的源码里根本没有Chain相关属性的支持,所以我就觉得一定是引入的版本有问题,于是我用google搜索”ConstraintLayout last version”,发现诶~官方有说最新版链接如下: http://tools.android.com/recent/constraintlayout102isnowavailable 按照这个链接提示,最新版是1.0.2,嗯哼,当我换成1.0.2后,发现无法download…. 不知道是网络问题还是什么问题,提示我无法下载,具体的错误记不清了。反正就是无法获取到这个版本。 特么的机智的我又直接去AndroidStudio的Library Dependency里去搜索,发现居然搜不到“ConstraintLayout “的库。再次懵逼。 后来我进行最后的一次尝试,因为我看google官方上1.0.2版本的上一个版本是1.0.1.于是我修改版本号,sync gradle,居然成功了。 总结踩坑历程: * 1 最新Release版AndroidStudio模板自带的是1.0.8alpha版ConstraintLayout * 2 使用Chain相关属性报错 * 3 发现该版本源码没有Chain相关属性 * 4 官网说的最新版1.0.2 我无法下载 * 5 AndroidStudio自带的Library Dependency搜不到ConstraintLayout * 6 修改版本号为1.0.1 下载
对此,我只能说“惊不惊喜! 意不意外!”
先概况一下,它是一个为了解决布局嵌套和模仿前端flexible布局的一个新布局。
从字面上理解,ConstraintLayout是约束布局。 在我理解,这是一个RelativeLayout的升级版。 而当初推出RelativeLayout的目的是为了在减少多层嵌套布局, 推出ConstraintLayout也是同样的目的,尽可能的使布局 宽而短,而不是 窄而长。 而ConstraintLayout更加强大,很多需要多层嵌套的布局,使用ConstraintLayout只需要一层即可解决。 它的Chain几种style方式,和前端的flexbox布局风格一致,官方文档中也说了它是flexible方式布局控件的东西。
A ConstraintLayout is a ViewGroup which allows you to position and size widgets in a flexible way.
而且搭配可视化的操作,使得布局也变得更轻松。 Google官方推荐所有操作都在”Design”区域搞定,即通过可视化拖拖拽拽生成布局大致的样子,然后针对具体属性、约束 精细修改。 甚至可以这么说,你完全不需要知道ConstraintLayout的具体属性值分别是什么,只通过拖拽和鼠标点击就可以实现一些布局。
我觉得首先是要知其然知其所以然,那些拖拽点击生成的代码属性到底是什么意思?通过本文可以了解。 另外 虽然大部分操作可以在“Design”区域完成,但是保不齐的需要你切换至“Text”区域,写上一两行属性代码,了解 这些属性 总是有益无害的。 而且,有一些属性是无法简单通过拖拽点击完成的,例如Margins when connected to a GONE widget。
刚才提到RelativeLayout,其实RelativeLayout也是通过约束来布局子View的呀, 以前RelativeLayout的约束有两种: * 1 子控件和子控件之间的约束(如Android:layout_below="@id/title") * 2 子控件和父控件的约束(如 android:layout_alignParentTop="true")
现在ConstraintLayout也是类似的,只不过除了以上两种约束,还多了一种 * 3 子控件和Guideline的约束
其实关于和Guideline的约束,也可以理解成约束1,因为Guideline其实就是一个在屏幕上不显示的View罢了。稍后讲到Guideline会带大家看看它巨简单的源码。
下面开始正文,开始属性的讲解
这一节的属性和相对布局的很像, 值得注意的是参数取值是 ID(@id/button1)代表约束1、3, 或者 字符串"parent" 代表约束2: * 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_constraintXXX_toYYYOf, 这里我的理解,constraintXXX里的XXX代表是这个子控件自身的哪条边(Left、Right、Top、Bottom、Baseline), 而toYYYOf里的YYY代表的是和约束控件的哪条边 发生约束 (取值同样是 Left、Right、Top、Bottom、Baseline)。 当XXX和YYY相反时,表示控件自身的XXX在约束控件的YYY的一侧, 例如app:layout_constraintLeft_toRightOf="@id/button1" ,表示的是控件自身的左侧在button1的右侧。
当XXX和YYY相同时,表示控件自身的XXX和约束控件的YYY的一侧 对齐, 例如:app:layout_constraintBottom_toBottomOf="parent",表示控件自身底端和父控件底端对齐。
代码为:
<Button android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Demo"/> <Button android:id="@+id/button2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="button2" app:layout_constraintLeft_toRightOf="@id/button1"/> <Button android:id="@+id/button3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="20dp" android:text="button3 跳转match页" app:layout_constraintBottom_toBottomOf="parent"/> 123456789101112131415161718 123456789101112131415161718图示:
margin和以往的使用一致,注意margin**不能为负值**即可。 在上图中也顺便展示了margin的使用。
Margins when connected to a GONE widget
举例,当A控件 约束 在B控件的左边,B控件GONE了,此时A会额外拥有一个margin的能力,来“补充”B消失的导致的“位移”。 这就是本节的属性。 这一节的属性开始我并没有理解,后来是通过写了一些Demo实验才明白。奈何官方文档惜字如金,只有一句话,并没有Demo展示:
When a position constraint target’s visibility is View.GONE, you can also indicates a different margin value to be used using the following attributes:
先看属性: * layout_goneMarginStart * layout_goneMarginEnd * layout_goneMarginLeft * layout_goneMarginTop * layout_goneMarginRight * layout_goneMarginBottom
在看Demo:
<Button android:id="@+id/button4" android:layout_width="100dp" android:layout_height="wrap_content" android:text="button4" app:layout_constraintRight_toRightOf="parent" /> <!-- android:layout_marginRight="10dp" 配合 app:layout_goneMarginRight="110dp"一起使用, 在约束的布局gone时,起用goneMargin, 但是一定要预先设置对应方向上的margin --> <Button android:id="@+id/button5" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginRight="10dp" android:text="button5" app:layout_constraintRight_toLeftOf="@id/button4" app:layout_goneMarginRight="110dp"/> 1234567891011121314151617181920 1234567891011121314151617181920此时图示:
当给button4 隐藏GONE掉以后: 图示:
会发现Button5纹丝不动,并没有收到Button4消失的影响。 这里我们再仔细看看button4的android:layout_width="100dp", 而button5的android:layout_marginRight="10dp",app:layout_goneMarginRight="110dp" 110 = 100 +10 , 这是一道小学计算题。
什么意思? 几个注意事项: * app:layout_goneMarginRight要配合android:layout_marginRight一起使用。 * 如果只设置了app:layout_goneMarginRight没有设置android:layout_marginRight,则无效。(alpha版本的bug,1.0.1版本已经修复) * 在约束的布局gone时,控件自身的marginXXX会被goneMarginXXX替换掉,以本文Demo为例,原本button4宽度是100,button5的marginRight是10, 加起来是110,如果想让button4隐藏之后,button5仍然纹丝不动,则需要设置goneMarginRight为10+100 = 110.
约束布局一个有用的地方是它如何处理“不可能”的约束。 比如你定义如下:
<android.support.constraint.ConstraintLayout ... android:layout_width="match_parent" android:layout_height="match_parent"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="button" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent"/> </android.support.constraint.ConstraintLayout> 12345678910111213 12345678910111213按照我们第一小节讲的属性值,这个定义的意思是,Button的左边和父控件的左边对齐,Button的右边和父控件的右边对齐。 可是控件是wrap_content的,它如果不铺满父控件要如何能满足这两个约束呢? 实际效果如下:
控件会居中显示,因为这两个约束作用 类似于 水平方向上,有相反的力 去拉控件,最终控件会居中显示。
搭配bias,能使约束偏向某一边,默认是0.5,有以下属性: * layout_constraintHorizontal_bias (0最左边 1最右边) * layout_constraintVertical_bias (0最上边 1 最底边)
比如上个Demo,我加入app:layout_constraintHorizontal_bias="0.9" ,则会在水平方向上向右偏移至90%。
<android.support.constraint.ConstraintLayout ... android:layout_width="match_parent" android:layout_height="match_parent"> <Button ... app:layout_constraintHorizontal_bias="0.9" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent"/> </android.support.constraint.ConstraintLayout> 12345678910 12345678910
这一节是对前一节goneMargin的补充。 重点是Gone隐藏掉的控件,会被解析成一个点,并忽略margin。
ConstraintLayout能为View.Gone的View特殊处理。 通常,GONE的控件不会被显示,并且不是布局本身的一部分(即如果标记为GONE,则其实际尺寸并不会更改)。 但是在布局计算方面,GONE的View仍然是其中的一个重要区别: 对于布局传递,它们的维度将被视为零(基本上它们将被解析为一个点) 如果他们对其他小部件有约束力,那么他们仍然会受到尊重,但任何margin都将等于零
注意A的margin也被忽略了。
拿上个Demo改一下,为A 加上一个android:layout_marginRight="10dp", 为了使A 隐藏后,B仍能纹丝不动,则B的app:layout_goneMarginRight="120dp"。 B goneMarginRight120 = A宽度100 + A marginRight10 +B marginRight10
<Button android:id="@+id/button4" android:layout_width="100dp" android:layout_height="wrap_content" android:layout_marginRight="10dp" android:text="button4" app:layout_constraintRight_toRightOf="parent" /> <Button android:id="@+id/button5" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginRight="10dp" android:text="button5" app:layout_constraintRight_toLeftOf="@id/button4" app:layout_goneMarginRight="120dp"/> 12345678910111213141516 12345678910111213141516可以为ConstraintLayout 自身定义最小的尺寸,他会在 ConstraintLayout为WRAP_CONTENT时起作用。 ● android:minWidth ● android:minHeight
控件的宽高有三种方式为其设置: * 确定尺寸 * WRAP_CONTENT * 0dp,就等于MATCH_CONSTRAINT
有些人可能有疑问,为什么不用MATCH_PARENT了。 官方文档如是说:
MATCH_PARENT is not supported for widgets contained in a ConstraintLayout, though similar behavior can be defined by using MATCH_CONSTRAINT with the corresponding left/right or top/bottom constraints being set to “parent”.
意思是MATCH_PARENT不再被支持了,通过MATCH_CONSTRAINT替代。 我们写个Demo看一下三种方式设置的效果吧:
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout ... android:layout_width="match_parent" android:layout_height="match_parent"> <Button android:id="@+id/button" android:layout_width="200dp" android:layout_height="100dp" android:text="Button" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toTopOf="parent"/> <Button android:id="@+id/button10" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button" app:layout_constraintLeft_toLeftOf="@+id/button" app:layout_constraintRight_toRightOf="@+id/button" app:layout_constraintTop_toBottomOf="@+id/button"/> <Button android:id="@+id/button11" android:layout_width="0dp" android:layout_height="wrap_content" android:text="Button" app:layout_constraintLeft_toLeftOf="@+id/button10" app:layout_constraintRight_toRightOf="@+id/button10" app:layout_constraintTop_toBottomOf="@+id/button10"/> </android.support.constraint.ConstraintLayout> 12345678910111213141516171819202122232425262728293031323334 12345678910111213141516171819202122232425262728293031323334效果如图:
有些人是不是要说,你特么逗我,不是说好的0dp等于MATCH_CONSTRAINT,应该是撑满屏幕的呀, OK ,把刀放下。让我们仔细看这个MATCH_CONSTRAINT属性。它match的是约束。 而这里第三个按钮的约束是第二个按钮,所以它的宽度设置为MATCH_CONSTRAINT 时,是和它的约束按钮,即第二个按钮一样宽。 注意,此时,竖直方向上没有约束,所以不能使用MATCH_CONSTRAINT属性.
我们仅仅将第三个按钮的属性修改为
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" 12 12则它宽度会撑满屏幕:
我们再修改Demo,分别为后两个按钮加上margin:
<android.support.constraint.ConstraintLayout ... android:layout_width="match_parent" android:layout_height="match_parent"> <Button android:id="@+id/button" android:layout_width="200dp" android:layout_height="100dp" android:text="Button" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toTopOf="parent"/> <Button android:id="@+id/button10" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button" app:layout_constraintLeft_toLeftOf="@+id/button" app:layout_constraintRight_toRightOf="@+id/button" app:layout_constraintTop_toBottomOf="@+id/button"/> <Button android:id="@+id/button12" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginLeft="10dp" app:layout_constraintLeft_toLeftOf="@id/button10" app:layout_constraintRight_toRightOf="@id/button10" app:layout_constraintTop_toBottomOf="@id/button10"/> <Button android:id="@+id/button11" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginRight="10dp" android:text="Button" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@+id/button12"/> </android.support.constraint.ConstraintLayout> 1234567891011121314151617181920212223242526272829303132333435363738394041424344 1234567891011121314151617181920212223242526272829303132333435363738394041424344效果如图:
最后,记住一句话约束要和 0dp 的 方向一致。否则无效。
可以以比例去定义View的宽高。 为了做到这一点,需要将至少一个约束维度设置为0dp(即MATCH_CONSTRAINT) 并将属性layout_constraintDimentionRatio设置为给定的比例。
例如:
<Button android:layout_width="200dp" android:layout_height="0dp" android:text="Ratio" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintDimensionRatio="2:1" app:layout_constraintTop_toTopOf="parent"/> 12345678 12345678如图:
比例值有两种取值: * 浮点值,表示宽度和高度之间的比率 (2,0.5) * “width:height”形式的比例 (5:1,1:5)
如果两个维度均设置为MATCH_CONSTRAINT(0dp),也可以使用比例。 在这种情况下,系统会使用满足所有约束条件和比率的最大尺寸。 如果需要根据一个维度的尺寸去约束另一个维度的尺寸。 则可以在比率值的前面添加 W 或者 H 来分别约束宽度或者高度。
例如,如果一个尺寸被两个目标约束(比如宽度为0,在父容器中居中),可以使用 W 或H 来指定哪个维度被约束。
<Button android:layout_width="0dp" android:layout_height="0dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintDimensionRatio="H,2:1" app:layout_constraintTop_toTopOf="parent"/> 123456 123456这里用“H”表示以高度为约束,高度的最大尺寸就是父控件的高度,“2:1”表示高:宽 = 2 : 1. 则宽度为高度的一半:
链条在同一个轴上(水平或者垂直)提供一个类似群组的统一表现。另一个轴可以单独控制。
如果一组小部件通过双向连接(见图,显示最小的链,带有两个小部件),则将其视为链条。
链条由在链的第一个元素(链的“头”)上设置的属性控制: 头是水平链最左边的View,或垂直链最顶端的View。
如果在连接上指定了边距,则将被考虑在内。 例如
<Button android:id="@+id/buttonA" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="50dp" android:text="Button" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toLeftOf="@+id/buttonB"/> <Button android:id="@+id/buttonB" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button" app:layout_constraintLeft_toRightOf="@+id/buttonA" app:layout_constraintRight_toRightOf="parent"/> 12345678910111213141516 12345678910111213141516通过app:layout_constraintRight_toLeftOf="@+id/buttonB"和app:layout_constraintLeft_toRightOf="@+id/buttonA"就建立了链条,(我中有你,你中有我)。 然后它们两个成了一个整体,所以链条左边设置app:layout_constraintLeft_toLeftOf="parent" 使得和父控件左对齐, 右边设置app:layout_constraintRight_toRightOf="parent"使得和父控件右对齐, 这样整个链条就居中了,最后对左控件设置了margin,相当于整个链条左边有了margin 效果:
当在链的第一个元素上设置属性 layout_constraintHorizontal_chainStyle或layout_constraintVertical_chainStyle 时,链的行为将根据指定的样式(默认为CHAIN_SPREAD)而更改。 看图这里就很像js里的flexible有木有。因为ConstraintLayout就是模仿flexible做的。
取值如下: * spread - 元素将被展开(默认样式) * 加权链 - 在spread模式下,如果某些小部件设置为MATCH_CONSTRAINT,则它们将拆分可用空间 * spread_inside - 类似,但链的端点将不会扩展 * packed - 链的元素将被打包在一起。 孩子的水平或垂直偏差属性将影响包装元素的定位
拿加权链举个例子:
<Button android:id="@+id/buttonA" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="50dp" android:text="Button" app:layout_constraintHorizontal_chainStyle="spread" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toLeftOf="@+id/buttonB"/> <Button android:id="@+id/buttonB" android:layout_width="0dp" android:layout_height="wrap_content" android:text="Button" app:layout_constraintLeft_toRightOf="@+id/buttonA" app:layout_constraintRight_toRightOf="parent"/> 1234567891011121314151617 1234567891011121314151617
和LinearLayout的weight类似。
链的默认行为是在可用空间中平均分配元素。 如果一个或多个元素使用MATCH_CONSTRAINT,它们将使用剩余的空白空间(在它们之间相等)。 属性layout_constraintHorizontal_weight和layout_constraintVertical_weight将决定这些都设置了MATCH_CONSTRAINT的View如何分配空间。 例如,在包含使用MATCH_CONSTRAINT的两个元素的链上,第一个元素使用权重为2,第二个元素的权重为1,第一个元素占用的空间将是第二个元素的两倍
最后关于链条,再给大家看一个关于margin的demo:
<Button android:id="@+id/buttonA" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginLeft="50dp" android:text="Button" app:layout_constraintHorizontal_chainStyle="spread" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toLeftOf="@+id/buttonB"/> <Button android:id="@+id/buttonB" android:layout_width="0dp" android:layout_height="wrap_content" android:text="Button" app:layout_constraintLeft_toRightOf="@+id/buttonA" app:layout_constraintRight_toRightOf="parent"/> 1234567891011121314151617 1234567891011121314151617一图胜千言,可以看到虽然他们的weight相等,但是margin是被计算在约束里的,所以左边的按钮宽度比右边的小。
Guideline只能用于ConstraintLayout中,是一个工具类,不会被显示,仅仅用于辅助布局。 它可以是horizontal或者 vertical的。(例如:android:orientation="vertical") * vertical的Guideline宽度为零,高度为ConstraintLayout的高度 * horizontal的Guideline高度为零,宽度为ConstraintLayout的高度
定位Guideline有三种方式: * 指定距离左侧或顶部的固定距离(layout_constraintGuide_begin) * 指定距离右侧或底部的固定距离(layout_constraintGuide_end) * 指定在父控件中的宽度或高度的百分比(layout_constraintGuide_percent)
一个栗子一看便知:
<android.support.constraint.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"> <android.support.constraint.Guideline android:id="@+id/guideline" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" app:layout_constraintGuide_begin="100dp"/> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="16dp" android:text="Button" app:layout_constraintLeft_toLeftOf="@+id/guideline" app:layout_constraintTop_toTopOf="parent"/> </android.support.constraint.ConstraintLayout> 123456789101112131415161718192021222324 123456789101112131415161718192021222324预览:
源码就这么点,这货的源码和ViewStub有点像啊,可以看出 * 它默认是GONE的。8 就是View.GONE的值。 * 它的public void setVisibility(int visibility)方法被空实现了,所以用户也没办法改变它的可见度。 * 推导出它一定是GONE的。在屏幕上不可见 * this.setMeasuredDimension(0, 0); 和public void draw(Canvas canvas)的空实现,表明这是一个超轻量的View,不可见,没有宽高,也不绘制任何东西。仅仅作为我们的锚点使用。
很久不写博客了,一是工作太忙了,二也是随意的写怕误人子弟。 这篇文章我写了整整一天,每个例子我都边写边跑了一遍, 也看了几篇别人的文章,有些人简单的翻译了官方文档,但是对文档中一些没有举例, 不那么好理解的地方也没有说明,于是便有了此文。 关于可视化操作,建议直接看我写的动态图解&实例 ConstraintLayout Chain和Android新特性介绍,ConstraintLayout完全解析。 我觉得ConstraintLayout ,有这几篇就够了。
文中代码地址在我的Demo合集中: https://github.com/mcxtzhang/Demos/tree/master/constraintlayoutdemo/src/main