*本篇文章已授權(quán)微信公眾號(hào) guolin_blog (郭霖)獨(dú)家發(fā)布
本文是基于constraint-layout:1.1.2
一、前言
在以前,android是使用布局如LinearLayout 、RelativeLayout等來構(gòu)建頁(yè)面,但這些布局使用起來很麻煩,并且經(jīng)常需要一層一層嵌套,寫一個(gè)簡(jiǎn)單的頁(yè)面就需要費(fèi)很大的勁。所以在16年I/O大會(huì)上,google發(fā)布了全新的布局-ConstraintLayout,其他布局和ConstraintLayout比起來,根本就沒有存在的必要了...
ConstraintLayout具有以下優(yōu)勢(shì):
較高的性能優(yōu)勢(shì)。
布局嵌套層次越高,性能開銷越大。而使用ConstraintLayout,經(jīng)常就一層嵌套就搞定了,所以其性能要好很多。
詳細(xì)的性能分析可參見:解析ConstraintLayout的性能優(yōu)勢(shì)完美的屏幕適配
ConstraintLayout的大小、距離都可以使用比例來設(shè)置,所以其適配性更好。書寫簡(jiǎn)單
可視化編輯。
ConstraintLayout也有十分方便完善的可視化編輯器,不用寫xml也基本上能實(shí)現(xiàn)大部分功能。但個(gè)人還是比較喜歡寫xml,所以本篇文章主要介紹如何使用代碼控制。如果想看如何使用可視化編輯器,可以參考郭霖大神的這篇文章
引入:
api 'com.android.support.constraint:constraint-layout:1.1.2'
二、ConstraintLayout
1. 定位位置
確定位置的屬性提供了下面13個(gè)屬性,其實(shí)本質(zhì)上都是一樣的,看名字應(yīng)該基本上都知道怎么用了(就是哪一條邊和哪一條邊對(duì)齊)
- 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
來看個(gè)例子:
實(shí)現(xiàn)上述UI的相關(guān)代碼如下:
<android.support.constraint.ConstraintLayout
...>
<Button
android:id="@+id/a"
....
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:text="A" />
<Button
android:id="@+id/b"
....
app:layout_constraintLeft_toRightOf="@id/a"
app:layout_constraintTop_toTopOf="@id/a"
android:text="B" />
<Button
android:id="@+id/c"
....
app:layout_constraintLeft_toLeftOf="@id/a"
app:layout_constraintTop_toBottomOf="@id/a"
android:text="C" />
<Button
android:id="@+id/d"
....
app:layout_constraintLeft_toRightOf="@id/a"
app:layout_constraintTop_toTopOf="@id/c"
android:text="D" />
</android.support.constraint.ConstraintLayout>
從中可以看到,
- layout_constraint*屬性的值可以是某個(gè)id或者parent(父布局)
- B要位于A的右邊,則使用
app:layout_constraintLeft_toRightOf="@id/a"
,C位于A的下邊,則使用app:layout_constraintTop_toBottomOf="@id/a"
對(duì)于一個(gè)View的邊界界定,官方給了下面這張圖:
2. margin
設(shè)置margin還是繼續(xù)用以前的屬性layout_margin*
。
不過需要注意,要使margin生效,必須具有對(duì)應(yīng)方向的layout_constraint*
,否則margin不生效.
3. 關(guān)于view gone
假如現(xiàn)在有如下布局:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
...>
<Button
android:id="@+id/a"
...
android:layout_marginLeft="100dp"
android:layout_marginTop="20dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/b"
...
android:layout_marginLeft="20dp"
android:layout_marginTop="20dp"
app:layout_constraintLeft_toRightOf="@id/a"
app:layout_constraintTop_toTopOf="@id/a"
/>
<Button
android:id="@+id/c"
....
android:layout_marginLeft="20dp"
android:layout_marginTop="20dp"
app:layout_constraintLeft_toRightOf="@id/b"
app:layout_constraintTop_toTopOf="@id/b" />
</android.support.constraint.ConstraintLayout>
考慮一個(gè)問題,如果B動(dòng)態(tài)設(shè)為gone了,C會(huì)怎么顯示呢?
真實(shí)情況如下:
為什么會(huì)這樣顯示呢?看他的藍(lán)圖應(yīng)該會(huì)好理解些:
可以看出,b設(shè)為gone之后,他的寬、高、margin都失效了,變?yōu)橐粋€(gè)點(diǎn)了,但它的constrain還生效,位于指定的位置。c還是可以繼續(xù)以他為錨點(diǎn)。
那么如何解決關(guān)于View gone引起的非預(yù)期的布局變化呢?
- 如果可以,盡量使用invisible
- 盡量其他view的布局不依賴會(huì)gone的view
- google也提供了屬性
layout_goneMargin*="xdp"
,意思是比如當(dāng)constrainleft的錨點(diǎn)gone時(shí),layout_goneMarginLeft將生效。但因?yàn)檫@個(gè)只能設(shè)置固定的距離,個(gè)人感覺靈活性不是很高。
4. 居中及bias
一個(gè)view如何設(shè)置為居中呢?如果查找屬性,會(huì)發(fā)現(xiàn)并沒有如RelativeLayout類似的layout_centerVertical屬性,那如何設(shè)置居中呢?constraint的思想很巧妙。
根據(jù)第一節(jié)的知識(shí),大家知道如果設(shè)置app:layout_constraintLeft_toLeftOf="parent"
,則view會(huì)貼著父view的左邊,設(shè)置app:layout_constraintRight_toRightOf="parent"
則會(huì)貼著右邊,那如果兩個(gè)都設(shè)置,效果會(huì)怎樣呢?
如圖,兩個(gè)都設(shè)置,view則會(huì)居中。
至此可以看出,對(duì)constraint的理解其實(shí)可以看成是像兩個(gè)彈簧一樣,如果只在左邊加一個(gè)彈簧,右邊沒有,那左邊的勢(shì)必會(huì)把view拉到左邊去,如果在右邊也加一根彈簧,兩個(gè)彈簧力相互平衡,則view就居中了。
上面是view居中,如果我想讓view向左偏一些,或者位于1/3處該怎么處理?其實(shí)也是一樣的,想象一下,如果左邊的彈簧力大一些,view不是就自然往左偏了嘛。如何使力大一些呢?使用如下屬性
- layout_constraintHorizontal_bias
- layout_constraintVertical_bias
bias即偏移量,他們的取值范圍從0~1,0即挨著左邊,1是挨著右邊,所以要使處于1/3處,可以設(shè)置如下屬性app:layout_constraintHorizontal_bias="0.33"
,效果圖如下:
5.view的尺寸
設(shè)置view的大小除了傳統(tǒng)的wrap_content、指定尺寸、match_parent(雖然官方不推薦使用match_parent)外,還可以設(shè)置為0dp(官方取名叫MATCH_CONSTRAINT),0dp在constraint可不是指大小是0dp,而是有特殊含義的。他的作用會(huì)隨著不同的設(shè)置有不同的含義:
- layout_constraintWidth_default
layout_constraintWidth_default有三個(gè)取值,作用如下:
-
spread
,默認(rèn)值,意思是占用所有的符合約束的空間
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
...>
<Button
android:id="@+id/a"
android:layout_width="0dp"
...
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"/>
</android.support.constraint.ConstraintLayout>
可以看到layout_width為0dp,實(shí)際的效果則是寬度和約束一樣,左右兩邊的留白是margin的效果。
-
percent
,意思是按照父布局的百分比設(shè)置,需要layout_constraintWidth_percent
設(shè)置百分比例
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout >
<android.support.constraint.ConstraintLayout
android:layout_width="300dp"
android:layout_height="400dp"
app:layout_constraintHorizontal_bias="0.3"
>
<Button
android:id="@+id/a"
android:layout_width="0dp"
...
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintWidth_default="percent"
app:layout_constraintWidth_percent="0.4" />
</android.support.constraint.ConstraintLayout>
</android.support.constraint.ConstraintLayout>
A的寬度設(shè)為0.4,則其寬度為父布局的0.4倍。另外,設(shè)置了layout_constraintWidth_percent屬性,可以不用指定layout_constraintWidth_default,他會(huì)自動(dòng)設(shè)置為percent
-
wrap
,意思匹配內(nèi)容大小但不超過約束限制,注意和直接指定寬度為wrap_content的區(qū)別就是不超過約束限制,如下:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
...>
<Button
android:id="@+id/a"
...
app:layout_constraintLeft_toLeftOf="parent" />
<Button
android:id="@+id/c"
...
app:layout_constraintRight_toRightOf="parent" />
<Button
android:id="@+id/b"
android:layout_width="0dp"
...
app:layout_constraintWidth_default="wrap"
app:layout_constraintLeft_toRightOf="@id/a"
app:layout_constraintRight_toLeftOf="@id/c"/>
<Button
android:id="@+id/d"
android:layout_width="wrap_content"
...
app:layout_constraintTop_toBottomOf="@id/b"
app:layout_constraintLeft_toRightOf="@id/a"
app:layout_constraintRight_toLeftOf="@id/c"/>
</android.support.constraint.ConstraintLayout>
可以看到雖然文字很長(zhǎng),但第一行的綠色button寬度達(dá)到約束時(shí),就不在增加,而第二行的button顯示了完整的內(nèi)容,超過約束的限制。
在1.1上 對(duì)于wrap_content會(huì)超過約束限制,谷歌又新增了如下屬性
- app:layout_constrainedWidth=”true|false”
- app:layout_constrainedHeight=”true|false”
設(shè)置為true也可以限制內(nèi)容不超過約束(這樣感覺layout_constraintWidth_default這個(gè)屬性已經(jīng)沒什么用了)
- ratio
layout_constraintDimensionRatio
,即寬和高成一定的比例,其值可以是"width:height"的形式,也可以是width/height的值。該屬性生效的前提:寬和高其中有一項(xiàng)為0dp,有constraint。下面按照有幾個(gè)0dp來分別介紹下:
- 如果只有一項(xiàng)為0dp,則該項(xiàng)值按照比例計(jì)算出來。比如高為20dp,寬為0dp,radio為"2:1",則最終寬為40dp
- 如果兩項(xiàng)都為0dp,則尺寸會(huì)設(shè)置為滿足約束的最大值并保持比例。因?yàn)檫@是系統(tǒng)計(jì)算的,有的時(shí)候不是我們想要的,我們也可以通過在前面加H、W來指定是哪一個(gè)邊需要計(jì)算。例如"H,2:1",則是指寬度匹配約束,高度是寬度的1/2
- max min
有如下屬性可以設(shè)置其的最大最小值,含義如字面值一樣:
- layout_constraintWidth_min
- layout_constraintWidth_max
- layout_constraintHeight_max
- layout_constraintHeight_min
- weight
該屬性在下面講解
6. 鏈
如圖,在一個(gè)水平或者豎直方向上,一排view兩兩互相約束,即為鏈
鏈的第一個(gè)元素稱為鏈頭,可以通過設(shè)置
layout_constraintHorizontal_chainStyle
來控制鏈的分布形式
spread
默認(rèn)模式,分布樣式如上圖-
spread_inside
如圖,和spread的區(qū)別是沒算兩端的約束
-
packed
所有元素?cái)D在中間,也可以配合使用bias來改變位置偏移
可以看出,鏈與LinearLayout效果大致一樣。和LinearLayout一樣,鏈也可以使用layout_constraintHorizontal_weight
,來分割剩余空間。但又和 android:layout_weight不太一樣,不一樣的地方如下:
- layout_weight ,不管當(dāng)前view的大小設(shè)的是多大,都會(huì)繼續(xù)占據(jù)剩余空間
- layout_constraintHorizontal_weight,這個(gè)只對(duì)0dp并且layout_constraintWidth_default為spread的view生效,使其大小按比例分割剩余空間,對(duì)于已經(jīng)設(shè)定大小的view不生效
如下面的示例:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
...>
<LinearLayout
...
android:orientation="horizontal">
<Button
android:layout_width="10dp"
android:layout_height="50dp"
android:layout_weight="1"
... />
<Button
android:layout_width="wrap_content"
android:layout_height="50dp"
android:layout_weight="1"
... />
<Button
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_weight="1"
... />
</LinearLayout>
<android.support.constraint.ConstraintLayout
....>
<Button
android:id="@+id/a"
android:layout_width="10dp"
android:layout_height="50dp"
....
app:layout_constraintHorizontal_weight="1"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@id/b" />
<Button
android:id="@+id/b"
android:layout_width="wrap_content"
android:layout_height="50dp"
....
app:layout_constraintHorizontal_weight="1"
app:layout_constraintLeft_toRightOf="@id/a"
app:layout_constraintRight_toLeftOf="@id/c" />
<Button
android:id="@+id/c"
android:layout_width="0dp"
android:layout_height="50dp"
...
app:layout_constraintHorizontal_weight="1"
app:layout_constraintLeft_toRightOf="@id/b"
app:layout_constraintRight_toRightOf="parent" />
/>
</android.support.constraint.ConstraintLayout>
</LinearLayout>
可以看出,LinearLayout和ConstraintLayout雖然三個(gè)子view的layout_width值是一樣的,weight也都設(shè)置了1,但效果完全不一樣
7. 圓形布局
ConstraintLayout還提供了一種比較炫酷的圓形布局,這是以往的布局所做不到的。涉及到的屬性也很簡(jiǎn)單,就下面三個(gè):
- layout_constraintCircle : 圓心,值是某個(gè)view的id
- layout_constraintCircleRadius : 半徑
- layout_constraintCircleAngle :角度,值是從0-360,0是指整上方
示例如下:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
...>
<Button
android:id="@+id/a"
...
/>
<Button
android:id="@+id/b"
...
app:layout_constraintCircle="@id/a"
app:layout_constraintCircleAngle="300"
app:layout_constraintCircleRadius="100dp" />
<Button
android:id="@+id/c"
...
app:layout_constraintCircle="@id/a"
app:layout_constraintCircleAngle="45"
app:layout_constraintCircleRadius="200dp" />
/>
</android.support.constraint.ConstraintLayout>
三、輔助組件
除了
ConstraintLayout
自身屬性之外,谷歌還提供了很多輔助布局(只是在布局中起輔助作用,并不會(huì)在界面真正顯示),來使ConstraintLayout
的功能更加強(qiáng)大。下面,我們就一一來了解下這些布局
1. GuideLine
即參考線的意思,有水平參考線和豎直參考線兩種。他的作用就像是一個(gè)虛擬的參考線,只是用來方便其他View以他為錨點(diǎn)來布局。
如上一篇所了解到的,ConstraintLayout 的定位原則就是一個(gè)View參考其他View的相對(duì)布局,如果有的時(shí)候當(dāng)前布局沒有合適的參考View,而建一個(gè)專門用于定位的View又會(huì)太重,這種情況正是GuideLine的用武之地。
例如:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
...>
<android.support.constraint.Guideline
android:id="@+id/guideline"
...
android:orientation="vertical"
app:layout_constraintGuide_percent="0.33" />
<android.support.constraint.Guideline
android:id="@+id/guideline2"
...
android:orientation="horizontal"
app:layout_constraintGuide_begin="130dp" />
<Button
...
app:layout_constraintLeft_toLeftOf="@id/guideline"
app:layout_constraintTop_toTopOf="@id/guideline2" />
</android.support.constraint.ConstraintLayout>
可以看到我分別添加了一個(gè)水平參考線和豎直參考線,之后的Button的布局就參考與這兩個(gè)參考線,而在布局中并不會(huì)顯示。
Guideline
的大部分的屬性如layout_width都是不會(huì)生效的,而他的位置的確定是由下面三個(gè)屬性之一來確定的:
- layout_constraintGuide_begin:距離父布局的左邊或者上邊多大距離
- layout_constraintGuide_end:距離父布局的右邊或者下邊多大距離
- layout_constraintGuide_percent:百分比,0~1,距離父布局的左邊或者上邊占父布局的比例
2. Group
Group是一個(gè)可以同時(shí)控制多個(gè)view 可見性的虛擬View。
例如:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
...>
<android.support.constraint.Group
...
android:visibility="invisible"
app:constraint_referenced_ids="a,c" />
<android.support.constraint.Group
...
android:visibility="visible"
app:constraint_referenced_ids="b,d" />
<Button
android:id="@+id/a"
... />
<Button
android:id="@+id/b"
... />
<Button
android:id="@+id/c"
... />
<Button
android:id="@+id/d"
.../>
</android.support.constraint.ConstraintLayout>
可以看到,第一個(gè)Group通過app:constraint_referenced_ids
指定了a、c兩個(gè)控件,這樣當(dāng)該Group可見性為invisible時(shí),a、c的可見性都會(huì)變?yōu)閕nvisible,為gone則都為gone。所以Group很適合處理有網(wǎng)無(wú)網(wǎng)之類的場(chǎng)景,不再需要像之前那樣一個(gè)一個(gè)view控制可見性,通過Group就可以統(tǒng)一處理了。
Group有一些注意事項(xiàng):
- xml中,可見性配置的優(yōu)先級(jí):Group優(yōu)先于View,下層Group優(yōu)先于上層。
- Group只可以引用當(dāng)前ConstraintLayout下的View,子Layout 下的View不可以。
-
app:constraint_referenced_ids
里直接寫的是id的字符串,初始化后會(huì)通過getIdentifier
來反射查找叫該名字的id。所以如果你的項(xiàng)目用了類似AndResGuard的混淆id名字的功能,切記不要混淆app:constraint_referenced_ids
里的id,否則在release版本就會(huì)因找不到該id而失效。或者也可以通過代碼setReferencedIds
來設(shè)置id。
3. Placeholder
占位布局。他自己本身不會(huì)繪制任何內(nèi)容,但他可以通過設(shè)置app:content="id"
,將id View的內(nèi)容繪制到自己的位置上,而原id的 View就像gone了一樣。
如下:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
...>
<Button
android:id="@+id/a"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginLeft="30dp"
...
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/b"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginLeft="20dp"
...
app:layout_constraintLeft_toRightOf="@+id/a"
app:layout_constraintTop_toTopOf="@+id/a" />
<android.support.constraint.Placeholder
android:id="@+id/place"
android:layout_width="200dp"
android:layout_height="200dp"
app:content="@+id/a"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"/>
<Button
...
app:layout_constraintBottom_toBottomOf="@+id/place"
app:layout_constraintLeft_toRightOf="@+id/place" />
</android.support.constraint.ConstraintLayout>
效果如圖:
可以看到,原本B是位于A的右邊并且頂部對(duì)齊的,但因?yàn)锳被Placeholder引用,使A 相當(dāng)于Gone了。而Placeholder的位置則顯示了A的內(nèi)容,并且大小也和A相符,Placeholder的大小設(shè)置并沒有生效。
大概總結(jié)可以認(rèn)為,Placeholder引用A后的效果是,原本位置的A gone,原本位置的Placeholder變?yōu)镻laceholder的約束屬性+A的內(nèi)容屬性。另外,Placeholder也支持使用代碼setContentId
動(dòng)態(tài)的修改設(shè)置內(nèi)容。
關(guān)于Placeholder的應(yīng)用場(chǎng)景,網(wǎng)上其他人也都列出了一些例子:比如可以作為位置模板,引入后只需要寫內(nèi)容view;使用代碼動(dòng)態(tài)改變內(nèi)容,結(jié)合TransitionManager可以做一些有趣的過度動(dòng)畫等。
4. Barrier
屏障,一個(gè)虛擬View。他主要解決下面遇到的問題:
如上圖布局,兩個(gè)TextView,一個(gè)button位于他們的右邊。現(xiàn)在button設(shè)置的是在下面TextView的右邊。假設(shè)有時(shí)候上面的TextView文本變長(zhǎng)了,則布局會(huì)變?yōu)橄旅孢@個(gè)樣子:
上面的TextView和Button重疊了。這時(shí)該怎么解決這個(gè)問題呢?Button只能設(shè)置一個(gè)View作為錨點(diǎn),設(shè)置了上面就顧不了下面了。。。
所以就誕生了Barrier,他可以設(shè)置N個(gè)View作為錨點(diǎn),使用方式如下:
<android.support.constraint.Barrier
android:id="@+id/barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="end"http://end,left,right,top,bottom
app:constraint_referenced_ids="text1,text2" />
則Barrier始終位于text1,text2兩個(gè)View最大寬度的右邊,示意圖如下:
這里基本的用法就講完了。
下面再考慮一個(gè)情況,假如有如下的布局:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
...>
<Button
android:id="@+id/a"
...
android:layout_marginTop="20dp"
/>
<Button
android:id="@+id/b"
...
android:layout_marginTop="40dp"
/>
<android.support.constraint.Barrier
android:id="@+id/barrier"
...
app:barrierDirection="top"
app:constraint_referenced_ids="a,b" />
<Button
android:id="@+id/c"
...
app:layout_constraintTop_toTopOf="@+id/barrier" />
</android.support.constraint.ConstraintLayout>
目前Button C和Button a、b的最上值對(duì)齊,沒有問題。但如果a Gone了呢?效果如下:
其實(shí)也是符合邏輯,a gone后,會(huì)變?yōu)橐粋€(gè)點(diǎn),所以C頂齊父布局也沒問題。但有的時(shí)候這不符合我們的需求,我們希望Barrier不要關(guān)注Gone的View了,所以谷歌提供了屬性
barrierAllowsGoneWidgets
,設(shè)為false后,就不在關(guān)注Gone的View了,效果如下:四、結(jié)束
本篇已基本上介紹完ConstraintLayout所有的屬性了(除了代碼寫布局的ConstraintSet類)。在2018.8.9號(hào),谷歌又發(fā)布了2.0.0-alpha2版本,里面加入了許多好玩的新特性,相信ConstraintLayout之后會(huì)越來越強(qiáng)大。