ConstraintLayout 用法全解析

zhangshua發表於2018-10-15

*本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家釋出

簡書地址 www.jianshu.com/p/502127a49…

本文是基於constraint-layout:1.1.2

一、前言

在以前,android是使用佈局如LinearLayout 、RelativeLayout等來構建頁面,但這些佈局使用起來很麻煩,並且經常需要一層一層巢狀,寫一個簡單的頁面就需要費很大的勁。所以在16年I/O大會上,google釋出了全新的佈局-ConstraintLayout,其他佈局和ConstraintLayout比起來,根本就沒有存在的必要了... ConstraintLayout具有以下優勢:

  1. 較高的效能優勢。 佈局巢狀層次越高,效能開銷越大。而使用ConstraintLayout,經常就一層巢狀就搞定了,所以其效能要好很多。 詳細的效能分析可參見:解析ConstraintLayout的效能優勢

  2. 完美的螢幕適配 ConstraintLayout的大小、距離都可以使用比例來設定,所以其適配性更好。

  3. 書寫簡單

  4. 視覺化編輯。 ConstraintLayout也有十分方便完善的視覺化編輯器,不用寫xml也基本上能實現大部分功能。但個人還是比較喜歡寫xml,所以本篇文章主要介紹如何使用程式碼控制。如果想看如何使用視覺化編輯器,可以參考郭霖大神的這篇文章

引入:api 'com.android.support.constraint:constraint-layout:1.1.2'

二、ConstraintLayout

1. 定位位置

確定位置的屬性提供了下面13個屬性,其實本質上都是一樣的,看名字應該基本上都知道怎麼用了(就是哪一條邊和哪一條邊對齊)

  • 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

來看個例子:

ConstraintLayout  用法全解析

實現上述UI的相關程式碼如下:

<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*屬性的值可以是某個id或者parent(父佈局)
  • B要位於A的右邊,則使用app:layout_constraintLeft_toRightOf="@id/a",C位於A的下邊,則使用app:layout_constraintTop_toBottomOf="@id/a"

對於一個View的邊界界定,官方給了下面這張圖:

ConstraintLayout  用法全解析

2. margin

設定margin還是繼續用以前的屬性layout_margin* 。 不過需要注意,要使margin生效,必須具有對應方向的layout_constraint*,否則margin不生效.

###3. 關於view gone 假如現在有如下佈局:

ConstraintLayout  用法全解析

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

考慮一個問題,如果B動態設為gone了,C會怎麼顯示呢? 真實情況如下:

ConstraintLayout  用法全解析
為什麼會這樣顯示呢?看他的藍圖應該會好理解些:
ConstraintLayout  用法全解析
可以看出,b設為gone之後,他的寬、高、margin都失效了,變為一個點了,但它的constrain還生效,位於指定的位置。c還是可以繼續以他為錨點。 那麼如何解決關於View gone引起的非預期的佈局變化呢?

  1. 如果可以,儘量使用invisible
  2. 儘量其他view的佈局不依賴會gone的view
  3. google也提供了屬性layout_goneMargin*="xdp",意思是比如當constrainleft的錨點gone時,layout_goneMarginLeft將生效。但因為這個只能設定固定的距離,個人感覺靈活性不是很高。

4. 居中及bias

一個view如何設定為居中呢?如果查詢屬性,會發現並沒有如RelativeLayout類似的layout_centerVertical屬性,那如何設定居中呢?constraint的思想很巧妙。 根據第一節的知識,大家知道如果設定app:layout_constraintLeft_toLeftOf="parent",則view會貼著父view的左邊,設定app:layout_constraintRight_toRightOf="parent" 則會貼著右邊,那如果兩個都設定,效果會怎樣呢?

ConstraintLayout  用法全解析
如圖,兩個都設定,view則會居中。 至此可以看出,對constraint的理解其實可以看成是像兩個彈簧一樣,如果只在左邊加一個彈簧,右邊沒有,那左邊的勢必會把view拉到左邊去,如果在右邊也加一根彈簧,兩個彈簧力相互平衡,則view就居中了。 上面是view居中,如果我想讓view向左偏一些,或者位於1/3處該怎麼處理?其實也是一樣的,想象一下,如果左邊的彈簧力大一些,view不是就自然往左偏了嘛。如何使力大一些呢?使用如下屬性

  • layout_constraintHorizontal_bias
  • layout_constraintVertical_bias

bias即偏移量,他們的取值範圍從0~1,0即挨著左邊,1是挨著右邊,所以要使處於1/3處,可以設定如下屬性app:layout_constraintHorizontal_bias="0.33",效果圖如下:

ConstraintLayout  用法全解析
##5.view的尺寸 設定view的大小除了傳統的wrap_content、指定尺寸、match_parent(雖然官方不推薦使用match_parent)外,還可以設定為0dp(官方取名叫MATCH_CONSTRAINT),0dp在constraint可不是指大小是0dp,而是有特殊含義的。他的作用會隨著不同的設定有不同的含義:

  1. layout_constraintWidth_default layout_constraintWidth_default有三個取值,作用如下:
  • spread,預設值,意思是佔用所有的符合約束的空間
    ConstraintLayout  用法全解析
<?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,實際的效果則是寬度和約束一樣,左右兩邊的留白是margin的效果。

  • percent,意思是按照父佈局的百分比設定,需要layout_constraintWidth_percent設定百分比例
    ConstraintLayout  用法全解析
<?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的寬度設為0.4,則其寬度為父佈局的0.4倍。另外,設定了layout_constraintWidth_percent屬性,可以不用指定layout_constraintWidth_default,他會自動設定為percent

  • wrap,意思匹配內容大小但不超過約束限制,注意和直接指定寬度為wrap_content的區別就是不超過約束限制,如下:
    ConstraintLayout  用法全解析
<?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>
複製程式碼

可以看到雖然文字很長,但第一行的綠色button寬度達到約束時,就不在增加,而第二行的button顯示了完整的內容,超過約束的限制。 在1.1上 對於wrap_content會超過約束限制,谷歌又新增瞭如下屬性

  • app:layout_constrainedWidth=”true|false”
  • app:layout_constrainedHeight=”true|false”

設定為true也可以限制內容不超過約束(這樣感覺layout_constraintWidth_default這個屬性已經沒什麼用了)

  1. ratio layout_constraintDimensionRatio,即寬和高成一定的比例,其值可以是"width:height"的形式,也可以是width/height的值。該屬性生效的前提:寬和高其中有一項為0dp,有constraint。下面按照有幾個0dp來分別介紹下:
  • 如果只有一項為0dp,則該項值按照比例計算出來。比如高為20dp,寬為0dp,radio為"2:1",則最終寬為40dp
  • 如果兩項都為0dp,則尺寸會設定為滿足約束的最大值並保持比例。因為這是系統計算的,有的時候不是我們想要的,我們也可以通過在前面加H、W來指定是哪一個邊需要計算。例如"H,2:1",則是指寬度匹配約束,高度是寬度的1/2
  1. max min 有如下屬性可以設定其的最大最小值,含義如字面值一樣:
  • layout_constraintWidth_min
  • layout_constraintWidth_max
  • layout_constraintHeight_max
  • layout_constraintHeight_min
  1. weight 該屬性在下面講解

###6. 鏈 如圖,在一個水平或者豎直方向上,一排view兩兩互相約束,即為鏈

ConstraintLayout  用法全解析
鏈的第一個元素稱為鏈頭,可以通過設定layout_constraintHorizontal_chainStyle來控制鏈的分佈形式

  • spread 預設模式,分佈樣式如上圖

  • spread_inside 如圖,和spread的區別是沒算兩端的約束

    ConstraintLayout  用法全解析

  • packed 所有元素擠在中間,也可以配合使用bias來改變位置偏移

    ConstraintLayout  用法全解析

可以看出,鏈與LinearLayout效果大致一樣。和LinearLayout一樣,鏈也可以使用layout_constraintHorizontal_weight,來分割剩餘空間。但又和 android:layout_weight不太一樣,不一樣的地方如下:

  • layout_weight ,不管當前view的大小設的是多大,都會繼續佔據剩餘空間
  • layout_constraintHorizontal_weight,這個只對0dp並且layout_constraintWidth_default為spread的view生效,使其大小按比例分割剩餘空間,對於已經設定大小的view不生效

如下面的示例:

ConstraintLayout  用法全解析

<?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雖然三個子view的layout_width值是一樣的,weight也都設定了1,但效果完全不一樣

7. 圓形佈局

ConstraintLayout還提供了一種比較炫酷的圓形佈局,這是以往的佈局所做不到的。涉及到的屬性也很簡單,就下面三個:

  • layout_constraintCircle : 圓心,值是某個view的id
  • layout_constraintCircleRadius : 半徑
  • layout_constraintCircleAngle :角度,值是從0-360,0是指整上方

示例如下:

ConstraintLayout  用法全解析

<?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自身屬性之外,谷歌還提供了很多輔助佈局(只是在佈局中起輔助作用,並不會在介面真正顯示),來使ConstraintLayout的功能更加強大。下面,我們就一一來了解下這些佈局

1. GuideLine

即參考線的意思,有水平參考線和豎直參考線兩種。他的作用就像是一個虛擬的參考線,只是用來方便其他View以他為錨點來佈局。 如上一篇所瞭解到的,ConstraintLayout 的定位原則就是一個View參考其他View的相對佈局,如果有的時候當前佈局沒有合適的參考View,而建一個專門用於定位的View又會太重,這種情況正是GuideLine的用武之地。 例如:

ConstraintLayout  用法全解析

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

可以看到我分別新增了一個水平參考線和豎直參考線,之後的Button的佈局就參考與這兩個參考線,而在佈局中並不會顯示。 Guideline的大部分的屬性如layout_width都是不會生效的,而他的位置的確定是由下面三個屬性之一來確定的:

  • layout_constraintGuide_begin:距離父佈局的左邊或者上邊多大距離
  • layout_constraintGuide_end:距離父佈局的右邊或者下邊多大距離
  • layout_constraintGuide_percent:百分比,0~1,距離父佈局的左邊或者上邊佔父佈局的比例

2. Group

Group是一個可以同時控制多個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>
複製程式碼

可以看到,第一個Group通過app:constraint_referenced_ids指定了a、c兩個控制元件,這樣當該Group可見性為invisible時,a、c的可見性都會變為invisible,為gone則都為gone。所以Group很適合處理有網無網之類的場景,不再需要像之前那樣一個一個view控制可見性,通過Group就可以統一處理了。 Group有一些注意事項:

  • xml中,可見性配置的優先順序:Group優先於View,下層Group優先於上層。
  • Group只可以引用當前ConstraintLayout下的View,子Layout 下的View不可以。
  • app:constraint_referenced_ids裡直接寫的是id的字串,初始化後會通過getIdentifier來反射查詢叫該名字的id。所以如果你的專案用了類似AndResGuard的混淆id名字的功能,切記不要混淆app:constraint_referenced_ids裡的id,否則在release版本就會因找不到該id而失效。或者也可以通過程式碼setReferencedIds來設定id。

3. Placeholder

佔位佈局。他自己本身不會繪製任何內容,但他可以通過設定app:content="id",將id View的內容繪製到自己的位置上,而原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>
複製程式碼

效果如圖:

ConstraintLayout  用法全解析

可以看到,原本B是位於A的右邊並且頂部對齊的,但因為A被Placeholder引用,使A 相當於Gone了。而Placeholder的位置則顯示了A的內容,並且大小也和A相符,Placeholder的大小設定並沒有生效。 大概總結可以認為,Placeholder引用A後的效果是,原本位置的A gone,原本位置的Placeholder變為Placeholder的約束屬性+A的內容屬性。另外,Placeholder也支援使用程式碼setContentId動態的修改設定內容。

關於Placeholder的應用場景,網上其他人也都列出了一些例子:比如可以作為位置模板,引入後只需要寫內容view;使用程式碼動態改變內容,結合TransitionManager可以做一些有趣的過度動畫等。

4. Barrier

屏障,一個虛擬View。他主要解決下面遇到的問題:

ConstraintLayout  用法全解析
如上圖佈局,兩個TextView,一個button位於他們的右邊。現在button設定的是在下面TextView的右邊。假設有時候上面的TextView文字變長了,則佈局會變為下面這個樣子:
ConstraintLayout  用法全解析
上面的TextView和Button重疊了。這時該怎麼解決這個問題呢?Button只能設定一個View作為錨點,設定了上面就顧不了下面了。。。 所以就誕生了Barrier,他可以設定N個View作為錨點,使用方式如下:

<android.support.constraint.Barrier
              android:id="@+id/barrier"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              app:barrierDirection="end"//end,left,right,top,bottom
              app:constraint_referenced_ids="text1,text2" />
複製程式碼

則Barrier始終位於text1,text2兩個View最大寬度的右邊,示意圖如下:

ConstraintLayout  用法全解析

這裡基本的用法就講完了。 下面再考慮一個情況,假如有如下的佈局:

ConstraintLayout  用法全解析

<?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的最上值對齊,沒有問題。但如果a Gone了呢?效果如下:

ConstraintLayout  用法全解析
其實也是符合邏輯,a gone後,會變為一個點,所以C頂齊父佈局也沒問題。但有的時候這不符合我們的需求,我們希望Barrier不要關注Gone的View了,所以谷歌提供了屬性barrierAllowsGoneWidgets,設為false後,就不在關注Gone的View了,效果如下:
ConstraintLayout  用法全解析

##四、結束 本篇已基本上介紹完ConstraintLayout所有的屬性了(除了程式碼寫佈局的ConstraintSet類)。在2018.8.9號,谷歌又釋出了2.0.0-alpha2版本,裡面加入了許多好玩的新特性,相信ConstraintLayout之後會越來越強大。

相關文章