背景
之前我們?cè)谶@邊文章中 Android 優(yōu)化之布局優(yōu)化 了解到可以通過(guò)使用 ConstraintLayout 來(lái)構(gòu)建我們的布局,這也是 Android 官方推薦首要使用的,手動(dòng)拖拽的方式習(xí)慣后也大大提高了我們的開(kāi)發(fā)效率,如果你還沒(méi)了解過(guò) ConstraintLayout ,那就繼續(xù)往下看吧。如果你已經(jīng)熟練使用的話(huà),不妨掃一眼,說(shuō)不定有意外的收獲。
添加約束條件
1.常規(guī)約束
創(chuàng)建約束條件時(shí),每個(gè)視圖都必須有兩個(gè)約束條件:一個(gè)水平約束條件,一個(gè)垂直約束條件,如果我們什么約束條件都沒(méi)有添加,控件就會(huì)位于ConstraintLayout 的左上角。添加約束條件非常簡(jiǎn)單,我們可以選擇手動(dòng)拖拽的方式或者直接手動(dòng)編碼的方式,個(gè)人喜歡拖拽的方式,如果有誤差再在布局文件中進(jìn)行微調(diào)。下面示例為 TextView 添加了上下左右四個(gè)約束條件:

可以看到,我們?cè)谕蟿?dòng)的過(guò)程中,布局文件也會(huì)生成相應(yīng)的代碼。上圖演示的是鏈接到父布局,除此之外,我們也可以鏈接到其他控件中,這里不再做演示,以下是常用的約束條件:
- 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
這些約束條件應(yīng)該都可以顧名思義,一個(gè)比較特別的是 layout_constraintBaseline_toBaselineOf,它用于將一個(gè)視圖的文本基線(xiàn)與另一視圖的文本基線(xiàn)對(duì)齊,要?jiǎng)?chuàng)建基線(xiàn)約束條件,可以右鍵點(diǎn)擊要約束的文本視圖,然后點(diǎn)擊 show Baseline,接著點(diǎn)擊文本基線(xiàn)并將其拖到另一基線(xiàn)上。
有些同學(xué)可能會(huì)對(duì) start 和 left、end 和 right 有困惑。其實(shí)如果應(yīng)用只是面向國(guó)內(nèi)市場(chǎng)的話(huà), start 等價(jià)于 left,end 等價(jià)于 right ,因?yàn)橹形牡臅?shū)寫(xiě)方向是從左到右的,但是有些語(yǔ)言是從右到左的書(shū)寫(xiě)方式,典型的就是阿拉伯語(yǔ),所以 Android 從 4.2 開(kāi)始推薦使用 start 、end 來(lái)代替 left 、 right,這樣在切換到 RTL 語(yǔ)言時(shí),UI 會(huì)自動(dòng)進(jìn)行鏡像翻轉(zhuǎn),可以保持一致的用戶(hù)體驗(yàn)。
2.Guideline 約束
我們可以添加垂直或水平的 Guideline 來(lái)約束視圖,相當(dāng)于輔助線(xiàn)一樣,用戶(hù)是看不到 Guideline 的。
通過(guò)請(qǐng)點(diǎn)擊工具欄中的 Guideline

然后點(diǎn)擊 Add Vertical Guideline 或 Add Horizontal Guideline,拖動(dòng)虛線(xiàn)將其重新定位,然后點(diǎn)擊引導(dǎo)線(xiàn)邊緣的圓圈以切換測(cè)量模式,有 固定數(shù)值 和 百分比 兩種模式。
3.Barrier 約束
與 Guileline 類(lèi)似,Barrier 是一條隱藏的線(xiàn),Barrier 的位置是根據(jù)其所包含的視圖的位置而移動(dòng),包含視圖的屬性是 constraint_referenced_ids,Barrier 可以是垂直或水平的,可以創(chuàng)建到引用視圖的頂部、底部、左側(cè)和右側(cè)。以下示例,Barrier 包含了 id 為 tv_1,tv_2 的 TextView,而 id 為 tv_3 的 TextView 在 Barrier 的右側(cè)。
代碼:
<?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="我是最長(zhǎng)長(zhǎng)長(zhǎng)長(zhǎng)的"
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>
效果:

調(diào)整視圖尺寸
我們一般是通過(guò) layout_width 和 layout_height 來(lái)為視圖指定尺寸,可供的選擇有 match_parent、wrap_content 和 具體的數(shù)值,ContraintLayout 亦如此,不過(guò)它多了一個(gè)選擇叫作 Match Constraints,在代碼中的體現(xiàn)是 0dp,除了可以在代碼中直接設(shè)置外,我們還可以在編輯器右側(cè)的 Attributes 窗口,點(diǎn)擊相應(yīng)的位置更改,以寬度為例,操作如下:

從動(dòng)圖中可以看到,當(dāng)我們切換到

layout_width 變?yōu)?0dp 了,代表當(dāng)前的模式為 match_contrains,此時(shí)視圖是撐滿(mǎn)的,這和 layout_constraintWidth_default 的值有關(guān):
- spread : 盡可能擴(kuò)展視圖以滿(mǎn)足每側(cè)的約束條件,默認(rèn)值。
- wrap : 僅在需要時(shí)擴(kuò)展視圖以適應(yīng)其內(nèi)容。這個(gè)與在 layout_width 設(shè)置為 wrap_content 的區(qū)別是 wrap 會(huì)受到約束條件的限制,即約束條件優(yōu)先。而設(shè)置 layout_content 為 wrap_content 會(huì)強(qiáng)行使寬度始終與內(nèi)容寬度完全匹配,即內(nèi)容優(yōu)先。
- percent : 顧名思義,設(shè)置為百分比的形式,在設(shè)置了這個(gè)值之后,我們就可以通過(guò) layout_constraintWidth_percent 指定百分比的具體數(shù)值(范圍為 0 到 1),當(dāng)然如果指定 layout_constraintWidth_default 為 spread ,設(shè)置 layout_constraintWidth_percent 屬性也會(huì)生效。
調(diào)整約束偏差
當(dāng)我們對(duì)某個(gè)視圖兩側(cè)添加約束條件(并且同一維度的視圖尺寸為 fixed 或者 wrap_cotent)時(shí),該視圖在兩個(gè)約束條件之間居中,默認(rèn)偏差為 0.5,對(duì)應(yīng)的屬性是 layout_constraintVertical_bias 或 layout_constraintHorizontal_bias,可以進(jìn)行對(duì)其調(diào)整滿(mǎn)足業(yè)務(wù)需求:

將尺寸設(shè)置為比例
如果視圖至少有一個(gè)尺寸設(shè)置為 match_constraints(0dp),我么就可以把視圖設(shè)置為比例的形式,對(duì)應(yīng)的屬性是 layout_constraintDimensionRatio ,如下,我們?cè)O(shè)置了寬充滿(mǎn)屏幕,比例為 1:1 的 TextView:

當(dāng)然,我們可以把寬高都設(shè)置為 match_constraints(0dp),這種情況下視圖會(huì)先滿(mǎn)足約束條件,然后把視圖指定為該比例的最大尺寸。我們也可以在比例前面加一個(gè) W,或者 H,來(lái)約束寬和高,如下:
<TextView
android:layout_width="160dp"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="W,2:1" />
<!--假設(shè)有足夠的空間,最終的寬為 160dp,高為 320dp-->
<TextView
android:layout_width="160dp"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="H,2:1" />
<!--假設(shè)有足夠的空間,最終的寬為 160dp,高為 80dp-->
邊距(Margin)
ContraintLayout 的邊距只有在有約束條件的情況下才會(huì)生效,比如下面這段代碼中TextView 沒(méi)有添加任何約束條件,最后它會(huì)顯示 ConstraintLayout 的左上角,設(shè)置的 margin 不會(huì)生效。
<?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 的邊距設(shè)置為負(fù)值并不會(huì)生效,這點(diǎn)和其他傳統(tǒng)布局是有區(qū)別的。
此外,ContraintLayoutMargin 還提供了 GONE_MARGIN:
- layout_goneMarginStart
- layout_goneMarginEnd
- layout_goneMarginLeft
- layout_goneMarginTop
- layout_goneMarginRight
- layout_goneMarginBottom
當(dāng)約束目標(biāo)被設(shè)置為 View.GONE 后,設(shè)置的 GONE_MARGIN 就會(huì)生效。
圓形(角度)定位(Circular positioning)
我們可以通過(guò)一個(gè)角度和距離來(lái)約束兩個(gè)視圖的位置,引用官方的一張圖:

對(duì)應(yīng)的屬性是 :
- 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>
上面實(shí)現(xiàn)的效果如下:

Chain,鏈控制
鏈?zhǔn)强刂埔唤M視圖的,視圖可以是水平鏈和垂直鏈的一部分,但是使用鏈并不會(huì)使鏈中的視圖對(duì)齊在同一方向上,因此,我們要指定額外的約束條件。以下示例是創(chuàng)建一個(gè)水平鏈并以視圖的頂端對(duì)齊:

創(chuàng)建鏈之后,會(huì)有一個(gè) "鏈頭"(Chain Head),鏈頭是鏈中的第一個(gè)元素(水平鏈中最左側(cè)的視圖,垂直鏈中最頂部的視圖)。鏈最重要是它的樣式,我們可以通過(guò)選擇鏈中的元素,右鍵點(diǎn)擊 Cycle Chain mode 進(jìn)行樣式切換,當(dāng)然也可以在鏈頭里設(shè)置 layout_constraintHorizontal_chainStyle , 鏈的樣式取值有以下幾種(不會(huì)忽略 margin 的取值):
- spread,視圖是均勻分布的。
- spread inside,第一個(gè)和最后一個(gè)視圖固定在鏈兩端的約束邊界上,其余視圖均勻分布。
- packed,鏈內(nèi)視圖被打包一起。
當(dāng)鏈的樣式設(shè)置為 spread 或者 spread inside 時(shí),且我們把一個(gè)或多個(gè)視圖設(shè)置為 match_constraints(0dp)。默認(rèn)情況下,設(shè)置了 match_constraints 的屬性會(huì)把剩余空間均勻分配,但是我們可以使用 layout_constraintHorizontal_weight 和 ayout_constraintVertical_weight 屬性來(lái)分配權(quán)重。這和 LinearLayout 的 layout_weight 的原理是一樣的。這種方式也叫做 weight chain。
另外,當(dāng)鏈設(shè)置為 packed 的樣式之后,我們可以通過(guò)鏈頭的視圖偏差 layout_constraintHorizontal_bias 屬性來(lái)調(diào)整整條鏈的偏差。這種方式稱(chēng)作 packed chain with bias。
下面這張來(lái)自官方的圖可以幫助我們理解鏈的不同樣式之間的區(qū)別:

Group
Group 可以把多個(gè)控件歸為一組,方便隱藏或顯示一組控件,相比我們?cè)谕饷姘粚?ViewGroup 的方法,性能上有優(yōu)勢(shì)。使用方式如下:
<androidx.constraintlayout.widget.Group
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="id1,id2,id3"/>
不過(guò)需要注意的是如果控件被包含在 Group 中,單獨(dú)一個(gè)控件設(shè)置 View.GONE 并不會(huì)使自己隱藏掉(在 Group 為 View.VISIBLE 的情況下),這是因?yàn)樵O(shè)置 View.GONE 會(huì)導(dǎo)致重繪,調(diào)用 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 方法會(huì)把 Group 內(nèi)的視圖的可見(jiàn)性設(shè)置為和 Group 的一樣。
總結(jié)
隨著官方的不斷完善和優(yōu)化,與剛出來(lái)的時(shí)候相比,ConstraintLayout 無(wú)論是使用上還是性能上都有了很大的提升,如果還沒(méi)在項(xiàng)目中使用的同學(xué),是時(shí)候上車(chē)了。