Android 隨筆—— 最強大的佈局 ConstraintLayout

QuincySx發表於2019-03-25

我之前寫過一篇 ConstraintLayout 的文章現在已經到了 2018 年,最新正式版本也已經到了 1.1.2 ,又加了不少好用的特性,可以說這個約束佈局已經成為 Android 中最強大的佈局了,絕對不是吹噓。

本篇文章只會講怎麼使用程式碼畫布局,視覺化的方式精準度方面還是有點差強人意,如果你想了解視覺化方式,請看我之前的文章。

讓我們看一看這個 Android 中最強大的佈局吧!

相對定位

一、基本用法

相對定位約束佈局最基本也最常用的使用方式

我們先簡單看一下用法

<TextView
        android:id="@+id/a"
        android:layout_width="60dp"
        android:layout_height="wrap_content"
        android:layout_marginLeft="20dp"
        android:layout_marginRight="20dp"
        android:gravity="center"
        android:layout_marginTop="30dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        android:text="A"/>

<TextView
        android:id="@+id/b"
        app:layout_constraintTop_toTopOf="@+id/a"
        app:layout_constraintLeft_toRightOf="@+id/a"
        android:layout_width="60dp"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="B" />
複製程式碼

我們先看 A 控制元件,A 的位置怎麼來的呢?

A 控制元件定位

  1. app:layout_constraintTop_toTopOf="parent" A 的頂邊與 parent 的頂邊對齊
  2. app:layout_constraintLeft_toLeftOf="parent" A 的左邊與 parent 的左邊對齊

我們看到 A 與左邊上邊都有一個空隙, 這就是普通的 android:layout_marginXXXX 屬性,我也就不細說了

我們再看 B 控制元件,B 的位置怎麼來的呢?

B 控制元件定位

  1. app:layout_constraintTop_toTopOf="@+id/a" B 的頂部與 A 的頂部對齊
  2. app:layout_constraintLeft_toRightOf="@+id/a" B 的左邊與 A 的右邊對齊

B 控制元件我沒有設定 margin 所以他們是貼在一塊的

從上面的例子我們看到了兩個問題

  1. 看到兩個控制元件 app:layout_constraintXXX_toXXXOf="xxx" 屬性的值不一樣,一個寫的是控制元件 ID,一個是 parent ,這是什麼意思呢?一般控制元件去約束需要一個參照物,這個參照物標識可以是控制元件的 ID ,也可以是父佈局(父容器) —— parent

就好比一根繩子一端拴在當前控制元件的某一位置,另一端拴在參照物的某一個位置上,這就建立起了約束。

  1. 如果仔細看上述程式碼,大家肯定還有一個疑問,我明明在 A 控制元件上設定了 marginRight 為什麼 A 和 B 還是貼著的,這就有一個說法,如果一個邊沒有約束那麼他對應邊的 margin 是不生效的

OK 理解起來很簡單不是嗎,約束佈局最基本的語法就是 app:layout_constraint位置_to位置Of="看齊目標" 那麼像這種普通的相對定位的寫法有多少種呢,我來給你們列舉一下,再配張圖示出它們的具體位置,看完下面基本的約束佈局用法你就已經瞭解了。

控制元件位置展示圖

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
複製程式碼

當然有一部分控制元件沒有 Baseline 這個位置,所以這個位置不是對每種控制元件都有效的

二、圓形定位
<View
        android:id="@+id/a"
        android:layout_width="120dp"
        android:layout_height="50dp"
        android:background="@android:color/holo_red_light"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

<View
        android:id="@+id/b"
        android:layout_width="120dp"
        android:layout_height="50dp"
        android:background="@android:color/holo_blue_light"
        app:layout_constraintCircle="@id/a"
        app:layout_constraintCircleAngle="45"
        app:layout_constraintCircleRadius="100dp" />
複製程式碼

偷偷放一張圖片

image.png

  1. app:layout_constraintCircle 需要看齊的參照物,圖中 B 就是把 A 當做參照物進行約束的
  2. app:layout_constraintCircleAngle 要旋轉的角度,最上方 0 度,預設就是 0 度,順時針開始算。
  3. app:layout_constraintCircleRadius 兩個控制元件中心點的距離

約束鏈

能夠在水平或垂直方向控制元件之間相互約束而組成的一條鏈就是約束鏈,約束鏈是由開頭的控制元件進行屬性控制的。沒錯就是跟著大哥走

  1. 普通的約束鏈示例
<View
        android:id="@+id/a"
        android:layout_width="80dp"
        android:layout_height="50dp"
        android:background="@android:color/holo_red_light"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintHorizontal_chainStyle="packed"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="@+id/b"
        app:layout_constraintTop_toTopOf="parent" />

<View
        android:id="@+id/b"
        android:layout_width="80dp"
        android:layout_height="50dp"
        android:background="@android:color/holo_blue_light"
        app:layout_constraintLeft_toRightOf="@+id/a"
        app:layout_constraintRight_toLeftOf="@+id/c"
        app:layout_constraintTop_toTopOf="@+id/a" />

<View
        android:id="@+id/c"
        android:layout_width="80dp"
        android:layout_height="50dp"
        android:background="@android:color/holo_green_light"
        app:layout_constraintLeft_toRightOf="@+id/b"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="@+id/a" />
複製程式碼

看到 A 控制元件就是引領小弟們的鏈頭,所以眾小弟都聽他的。我們看到它的 app:layout_constraintHorizontal_chainStyle="packed" 屬性。 這個屬性有三種值

  • packed:控制元件緊挨在一起。還可以通過bias屬性設定偏移量。
  • spread:均與分佈控制元件。
  • spread_inside:均與分佈控制元件,但是兩邊控制元件貼邊。

我貼幾張圖大概看一下樣子

當然如果是垂直的就是 app:layout_constraintVertical_chainStyle="xxxx" 屬性

packed 樣式
spread 樣式
spread_inside 樣式

  1. 組合 layout_constraintHorizontal_bias 的約束鏈 如果鏈的樣式是 packed 我們還能組合 layout_constraintHorizontal_bias 使用,我就不貼程式碼了,直接上圖瞭解下

    packed 配合 layout_constraintHorizontal_bias

  2. 還有一種特殊的約束鏈,就是按照控制元件權重平分控制元件(明擺著搶 LinearLayout 飯碗)

<View
        android:id="@+id/a"
        android:layout_width="0dp"
        android:layout_height="50dp"
        android:background="@android:color/holo_red_light"
        app:layout_constraintHorizontal_weight="1"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="@+id/b" />

<View
        android:id="@+id/b"
        android:layout_width="0dp"
        android:layout_height="50dp"
        android:background="@android:color/holo_blue_light"
        app:layout_constraintHorizontal_weight="2"
        app:layout_constraintLeft_toRightOf="@+id/a"
        app:layout_constraintRight_toLeftOf="@+id/c" />

<View
        android:id="@+id/c"
        android:layout_width="0dp"
        android:layout_height="50dp"
        android:background="@android:color/holo_green_light"
        app:layout_constraintHorizontal_weight="2"
        app:layout_constraintLeft_toRightOf="@+id/b"
        app:layout_constraintRight_toRightOf="parent" />
複製程式碼

image.png

這種鏈就是不設定鏈的 style 而是用權重的方式進行排布。

約束佈局散亂特性

一、動態推測控制元件的寬或高

我們約束佈局還支援這樣的一種情況,我們控制元件只確定了控制元件的寬、高的其中一個,然後按比例算出另一邊的寬高。(當然這個需求很小眾) app:layout_constraintDimensionRatio="2:1" 這個屬性就是按比例推測寬高的屬性

<View
        android:id="@+id/a"
        android:layout_width="100dp"
        android:layout_height="0dp"
        android:background="@android:color/holo_red_light"
        app:layout_constraintDimensionRatio="2:1"
        app:layout_constraintLeft_toLeftOf="parent" />
複製程式碼

看以上程式碼 2:1 其實就是 width/height = 2/1 ,height = 50dp,所以這個屬性的就是 width/height ,由此我們只要給定寬或高就能推出另一個。(當然如果你寬高都確定了設定這個屬性就無效了)

這個屬性的值還能設定為 (H,2:1),(W,2:1) 這樣的,其實我看到之後是一臉懵逼的,但是還要硬著頭皮研究,H 就是 Height,W 就是 Width,好吧說一下規律。

  1. (H,2:1) 如果確定寬寬:高度 = 100 * 1 / 2 如果確定高度:寬度 = 100 * 1 / 2

  2. (W,2:1) 如果確定寬寬:高度 = 100 * 2 / 1 如果確定高度:寬度 = 100 * 2 / 1

雖然我這樣說了但是還是不推薦用。本來就是小眾功能如果想用還是用普通的寫法就行了,不要帶什麼 H,W 了。裝逼太刺眼!

二、父佈局填充約束

有時候有這麼一個需求想把控制元件按照比例填充父佈局。Android 螢幕適配這麼複雜,好像不容易實現 這時候約束佈局有兩個屬性 layout_constraintWidth_percent layout_constraintHeight_percent 怎麼看怎麼像百分比佈局,這豈不是把一直不溫不火的百分比佈局給革命了

<View
        android:id="@+id/a"
        android:layout_width="0dp"
        android:layout_height="50dp"
        android:background="@android:color/holo_red_light"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintWidth_percent="0.3" />
複製程式碼

填充 30% 示例
我只放這麼一段程式碼大家應該就知道怎麼用了,我也就不多了說了。

預設是居中的如果想調整位置請結合 app:layout_constraintHorizontal_bias="0.3" 食用

三、聊一聊 margin 屬性

有人要說了 margin 屬性還用你說?當然了我並不會說,我要說的是 layout_goneMarginStart 沒見過吧?這個是什麼意思呢,如果要約束控制元件隱藏了,B 控制元件位置還想保持不動怎麼辦呢,那麼這是個什麼情況呢?

  • 舉個例子 B 控制元件左邊相對 A 控制元件進行定位,並設定了 20dp 的 marginLeft。如果把 A 隱藏了會怎麼樣呢?分別看一下圖1、圖2。
    圖1
    圖2

給我們的 B 控制元件加上兩個屬性

app:layout_goneMarginLeft="100dp"
app:layout_goneMarginTop="30dp"
複製程式碼

成品

預想的效果很棒,不是嗎,從此我們看出 goneMarginXXXX 就是在約束的目標控制元件隱藏時才會生效的 margin。

四、WRAP_CONTENT 的小問題

如果你的控制元件寬或高是 wrap_content 並且控制元件長度過長時,他的約束會失效,我們看個例子

<View
        android:id="@+id/a"
        android:layout_width="120dp"
        android:layout_height="50dp"
        android:background="@android:color/holo_red_light"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/b"
        android:layout_width="wrap_content"
        android:layout_height="50dp"
        android:background="@android:color/holo_blue_light"
        android:text="是事實是事實是事實是事實是事實"
        app:layout_constraintLeft_toRightOf="@+id/a"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/a" />
複製程式碼

沒有超過邊界的例子
超過邊界約束被破壞
修復後的效果
解決方式就是使用

app:layout_constrainedWidth="true"
app:layout_constrainedHeight="true"
複製程式碼
五、最小高度寬度,最大高度寬度

設定最小尺寸 layout_constraintWidth_min and layout_constraintHeight_min 設定最大尺寸 layout_constraintWidth_max and layout_constraintHeight_max

沒啥好說的。

六、控制元件位置偏移

app:layout_constraintHorizontal_bias="0.3" app:layout_constraintVertical_bias="0.3" 相信這兩個屬性在上面大家都看過好多遍了吧,如果控制元件沒有佔滿父佈局,它是可以控制當前控制元件在父佈局的空間裡所佔的位置,放張圖理解一下。就跟拔河一樣哈哈

image.png

該屬性的取值範圍是 0 - 1

這是橫向的分析,豎向與這個一致

約束佈局輔助工具

一、 輔助定位線 Guideline 是一個幫助我們來定位控制元件,但是他又不被使用者所感知。

<android.support.constraint.Guideline
        android:id="@+id/line"
        android:layout_width="wrap_content"
        android:orientation="vertical"
        android:layout_height="wrap_content"/>
複製程式碼

上面的最基本的寫法

  1. id 是必須的不然怎麼約束
  2. android:orientation="vertical" 來控制橫向還是豎向,用過線性佈局應該都知道這個屬性
  3. 他核心的三個屬性
  • layout_constraintGuide_begin="100dp" 距離父容器起始位置的距離
  • layout_constraintGuide_end="100dp" 距離父容器結束位置的距離
  • layout_constraintGuide_percent="0.3" 距離父容器寬度或高度的百分比,取值範圍 0 - 1

如果上述三種屬性同時出現,優先順序由高到低 layout_constraintGuide_percent > layout_constraintGuide_begin > layout_constraintGuide_end

二、 控制元件組 Group 可以同時控制多個控制元件的顯示與隱藏

<android.support.constraint.Group
              android:id="@+id/group"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:visibility="visible"
              app:constraint_referenced_ids="a,b" />
複製程式碼
  1. android:visibility="visible" 控制顯示隱藏,也可以在程式碼中根據 id 獲取 Group 來控制顯示隱藏
  2. app:constraint_referenced_ids="a,b" 受這個 Group 管理的控制元件們的 id,, 號隔開

三、 控制元件屏障 Barrier

<android.support.constraint.Barrier
              android:id="@+id/barrier"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              app:barrierDirection="start"
              app:constraint_referenced_ids="button1,button2" />
複製程式碼
  1. app:barrierDirection=“start” 屬性可以控制這個屏障在哪個位置,具體位置可以參考文章開頭那張介紹位置的圖
  2. app:constraint_referenced_ids 屏障裡面的控制元件們的 id,, 號隔開

下面分析一個例子

<TextView
        android:id="@+id/tv_name"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="姓名:sdfsdfsdsdf"
        app:layout_constraintBottom_toBottomOf="@+id/et_name"
        app:layout_constraintTop_toTopOf="@+id/et_name"/>

<TextView
        android:id="@+id/tv_phone"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:text="手機號:"
        app:layout_constraintBottom_toBottomOf="@+id/et_phone"
        app:layout_constraintTop_toTopOf="@+id/et_phone"/>

<EditText
        android:id="@+id/et_name"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:hint="請輸入姓名"
        app:layout_constraintLeft_toLeftOf="@+id/barrier"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>

<EditText
        android:id="@+id/et_phone"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:hint="請輸入手機號"
        app:layout_constraintLeft_toLeftOf="@+id/barrier"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/et_name"/>

<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"/>
複製程式碼

放一張效果圖

image.png

  1. 首先把 et_name et_phone 位置放好,然後再把 tv_name tv_phone 放在屏障裡,把他們當成一個整體
  2. 然後 et_name et_phone 左邊約束我們的屏障
  3. 為什麼 et_name et_phone 對齊的會是屏障的右邊,因為 app:barrierDirection="right" 這個屬性控制的,如果改成 left 就會變成如下的樣子,全都和屏障的左邊去對齊了

image.png

這個屏障有點稍微複雜那麼一丟丟,大家多多實踐一下

小結

OK 我在這裡寫了約束佈局的一些用法,那麼下一篇我將會繼續絮叨絮叨這個約束佈局在我們平常開發常用的一些寫法和技巧!

如果還有些看不懂,請配合官方文件食用,畢竟官方資料才是我們的一手資料

參考資料

官方文件

相關文章