ConstraintLayout在專案中實踐與總結

宇是我發表於2017-11-29

ConstraintLayout,讓佈局更優雅。

一、為什麼要用ConstraintLayout

image.jpg

上圖是網易100分的選課首頁,在Banner圖的下部是推薦類目模組,其中數學、語言、小低和小高分別是推薦類目Item。可見每個類目的子類目個數是不確定的,根據個數的不同,子類目的排列方式也不一樣。

現在我們來實現Item的佈局。如果用LinearLayout、RelativeLayout和FrameLayout去實現Item佈局,我目前想到的最低也需要兩層佈局。如下所示:

<Relative>  
    <ImageView />
    <TextView />
    <LinearLayout>
        <TextView />
        <TextView />
        <TextView />
    </LinearLayout>
    <LinearLayout>
        <TextView />
        <TextView />
    </LinearLayout>
</Relative>
複製程式碼

可以發現沒有一種佈局容器是可以單靠自己搞定這個佈局的,需要巢狀不同佈局。這樣佈局層級增加,佈局計算時間也加長了。這些都是傳統佈局存在的問題,概括起來有以下三點:

  • 複雜佈局能力差,需要不同佈局巢狀使用。
  • 佈局巢狀層級高。不同佈局的巢狀使用,導致佈局的巢狀層級偏高。
  • 頁面效能低。較高的巢狀層級,需要更多的計算佈局時間,降低了頁面效能。

正是由於目前佈局容器存在的問題,我們需要尋找一種可以解決這些問題的佈局容器。正好,ConstraintLayout可以。

二、ConstraintLayout是什麼

ConstraintLayout,中文稱約束佈局,在2016年Google I/O大會時提出,2017年2月釋出正式版,目前穩定版本為1.0.2。約束佈局作為Google今後主推的佈局樣式,可以完全替代其他佈局,降低頁面佈局層級,提升頁面渲染效能。

三、怎麼用ConstraintLayout

3.1 環境搭建

ConstraintLayout支援最低Android Studio版本是2.2,但是有些屬性在2.2的佈局編輯器上不支援編輯,如比例和baseline等約束。所以推薦使用2.3的版本,當然3.0的版本那就更好了。要使用ConstraintLayout,需要在專案中進行如下配置:

  • 在專案外層定義google maven倉庫
repositories {
    maven {
        url 'https://maven.google.com'
    }
}
複製程式碼
  • 在要使用ConstraintLayout的module的build.gradle檔案中引入約束佈局庫
dependencies {
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
}

複製程式碼

3.2 佈局引入

按照上述配置好環境後,我們就可以在專案中使用ConstraintLayout了。有兩種方式使用:

  1. layout轉換的方式使用

    • 首先,開啟一個非ConstraintLayout的佈局檔案,切換到Design Tab

    • 在Component Tree視窗,選中要轉換的layout檔案根佈局,點選右鍵,然後選擇Convert layout to ConstraintLayout

  2. 直接新建一個layout檔案使用

通過如下方式引入約束佈局:

<android.support.constraint.ConstraintLayout

/>
複製程式碼

3.3 屬性介紹

ConstraintLayout的佈局屬性,乍一看有很多,其實可以分為8個部分,下面一一介紹。

3.3.1 相對位置
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
複製程式碼

以上這些屬性,用於設定一個控制元件相對於其他控制元件、Guideline或者父容器的位置。以layout_constraintLeft_toLeftOf為例,其中layout_部分是固定格式,主要的資訊包含在下面兩部分:

  • constraintXXX:指定當前控制元件需要設定約束的屬性部分。如constraintLeft表示對當前控制元件的左邊進行約束設定。
  • toXXXOf:其指定的內容是作為當前控制元件設定約束需要依賴的控制元件或父容器(可以理解為設定約束的參照物)。並通過XXX指定被依賴物件用於參考的屬性。如toLeftOf="parent" :表示當前控制元件相對於父容器的左邊進行約束設定。

ConstraintLayout的相對位置佈局比較靈活,相比於RelativeLayout,ConstraintLayout可以通過layout_constraintBaseline_toBaselineOf設定兩個控制元件之間的文字相對於baseline對齊。一個佈局效果的例子,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/activity_relative_position"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="zr.com.constraintdemo.normal.RelativePositionActivity">

    <Button
        android:id="@+id/btn_A"
        android:text="A"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        />

    <Button
        android:text="在A下方,與A左對齊"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toBottomOf="@id/btn_A"
        app:layout_constraintLeft_toLeftOf="@id/btn_A"
        android:layout_marginTop="32dp"
        />

    <Button
        android:text="在A上方,與A居中對齊"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toTopOf="@id/btn_A"
        app:layout_constraintLeft_toLeftOf="@id/btn_A"
        app:layout_constraintRight_toRightOf="@id/btn_A"
        android:layout_marginBottom="32dp"
        />

    <Button
        android:text="baseline對齊"
        android:layout_width="wrap_content"
        android:layout_height="80dp"
        app:layout_constraintBaseline_toBaselineOf="@id/btn_A"
        app:layout_constraintLeft_toLeftOf="parent"
        android:layout_marginLeft="8dp"
        android:gravity="bottom"
        />

    <Button
        android:text="水平居中對齊"
        android:layout_width="wrap_content"
        android:layout_height="80dp"
        android:gravity="bottom"
        app:layout_constraintTop_toTopOf="@id/btn_A"
        app:layout_constraintBottom_toBottomOf="@id/btn_A"
        app:layout_constraintLeft_toRightOf="@id/btn_A"
        android:layout_marginLeft="16dp"
        />

</android.support.constraint.ConstraintLayout>
複製程式碼

相對佈局例子圖

3.3.2 邊距

在ConstraintLayout中,控制元件除了可以設定普通的邊距屬性,還可以設定當控制元件依賴的控制元件GONE之後的邊距屬性。即我們可以理解可以根據被依賴控制元件是否GONE的狀態,設定兩種邊距值。分別通過如下屬性進行設定:

  • 普通邊距屬性
android:layout_marginStart
android:layout_marginEnd
android:layout_marginLeft
android:layout_marginTop
android:layout_marginRight
android:layout_marginBottom
複製程式碼
  • 被依賴控制元件GONE之後的邊距屬性
layout_goneMarginStart
layout_goneMarginEnd
layout_goneMarginLeft
layout_goneMarginTop
layout_goneMarginRight
layout_goneMarginBottom
複製程式碼

這種特性,可以比較方便實現一些特定的需求,且無需程式碼中進行額外設定。如B控制元件依賴A,A距離父容器左邊20dp,B在A右邊,距離A為20dp。需求當A設定為GONE之後,B距離父容器左邊60dp。這在ConstraintLayout中實現起來就很簡單,對B同時設定如下屬性即可:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/activity_margin"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="zr.com.constraintdemo.normal.MarginActivity">
    <Button
        android:id="@+id/btn_a"
        android:text="A"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:layout_marginTop="100dp"
        />

    <Button
        android:text="B"
        android:textAllCaps="false"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintLeft_toRightOf="@id/btn_a"
        app:layout_constraintTop_toTopOf="parent"
        android:layout_marginLeft="20dp"
        app:layout_goneMarginLeft="60dp"
        android:layout_marginTop="100dp"
        />

</android.support.constraint.ConstraintLayout>
複製程式碼
3.3.3 居中
  • 水平居中:相對一個控制元件或者父容器左右對齊
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent
複製程式碼
  • 垂直居中:相對一個控制元件或者父容器上下對齊
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
複製程式碼

例子:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/activity_center_position"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="zr.com.constraintdemo.normal.CenterPositionActivity">

    <Button
        android:text="水平居中"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        />

    <Button
        android:text="垂直居中"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        />

    <Button
        android:text="水平垂直居中"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        />

</android.support.constraint.ConstraintLayout>
複製程式碼

實現效果截圖

3.3.4 偏移

在設定控制元件的居中屬性之後,通過偏移屬性可以設定讓控制元件更偏向於依賴控制元件的某一方,偏移設定為0~1之間的值。相應屬性:

layout_constraintHorizontal_bias // 水平偏移
layout_constraintVertical_bias   // 垂直偏移
複製程式碼

例子:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/activity_bias"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="zr.com.constraintdemo.normal.BiasActivity">

    <Button
        android:text="水平偏移30%"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintHorizontal_bias="0.3"
        />

    <Button
        android:text="垂直偏移30%"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintVertical_bias="0.3"
        />

    <Button
        android:text="水平垂直偏移70%"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintVertical_bias="0.7"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintHorizontal_bias="0.7"
        />

</android.support.constraint.ConstraintLayout>
複製程式碼

偏移截圖

3.3.5 可見性

可見性這個屬性大家應該很熟悉,但是約束佈局的可見性屬性和其它佈局相比,存在以下區別:

  • 當控制元件設為GONE時,被認為尺寸為0。可以理解為佈局上的一個點。

  • 若GONE的控制元件對其它控制元件有約束,則約束保留並生效,但所有的邊距(margin)會清零。

3.3.6 尺寸
幾種設定方式:
  • 設定固定尺寸,如123dp
  • 使用wrap_content,根據內容計算合適大小
  • match_parent,填充滿父佈局,此時設定的約束都不生效了。(早之前的約束佈局版本貌似不允許在其子view中使用match_parent屬性,但是我寫文章的時候發現也是可以用上去的)
  • 設定0dp,相當於MATCH_CONSTRAINT屬性,基於約束最終確定大小
MATH_CONSTRAINT
  • layout_constraintWidth_minlayout_constraintHeight_min:設定最小值

  • layout_constraintWidth_maxlayout_constraintHeight_max:設定最大值

  • layout_constraintWidth_percentlayout_constraintHeight_percent:設定控制元件相對於父容器的百分比大小(1.1.0開始支援)。使用之前需要先設定為百分比模式,然後設定設定寬高值為0~1之間。

    設定為百分比模式的屬性:

    複製程式碼

app:layout_constraintWidth_default="percent" app:layout_constraintHeight_default="percent" ```

  • 強制約束
    當一個控制元件設為wrap_content時,再新增約束尺寸是不起效果的。如需生效,需要設定如下屬性為true:
app:layout_constrainedWidth=”true|false”	 
app:layout_constrainedHeight=”true|false複製程式碼

看個具體例子:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/activity_dimen"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="zr.com.constraintdemo.normal.DimenActivity">

    <Button
        android:id="@+id/btn_1"
        android:text="minWidth設定為200dp"
        android:textAllCaps="false"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        android:minWidth="200dp"
        />

    <Button
        android:id="@+id/btn_2"
        android:text="設定為MATCH_CONSTRAINT"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toBottomOf="@id/btn_1"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        />

    <Button
        android:id="@+id/btn_3"
        android:textAllCaps="false"
        android:text="layout_constrainedWidth開啟"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toBottomOf="@id/btn_2"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constrainedWidth="true"
        app:layout_constraintWidth_min="300dp"
        />
    <Button
        android:id="@+id/btn_4"
        android:textAllCaps="false"
        android:text="layout_constrainedWidth關閉"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toBottomOf="@id/btn_3"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintWidth_min="300dp"
        />
    <Button
        android:id="@+id/btn_5"
        android:textAllCaps="false"
        android:text="寬50%高30%佈局"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintTop_toBottomOf="@id/btn_4"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintWidth_default="percent"
        app:layout_constraintHeight_default="percent"
        app:layout_constraintWidth_percent="0.5"
        app:layout_constraintHeight_percent="0.3"
        />

</android.support.constraint.ConstraintLayout>
複製程式碼

dimen截圖

3.3.7 比例

控制元件可以定義兩個尺寸之間的比例,目前支援寬高比。 前提條件是至少有一個尺寸設定為0dp,然後通過layout_constraintDimentionRatio屬性設定寬高比。設定方式有以下幾種:

  • 直接設定一個float值,表示寬高比
  • 以” width:height”形式設定
  • 通過設定字首W或H,指定一邊相對於另一邊的尺寸,如”H, 16:9”,高比寬為16:9

如果寬高都設定為0dp,也可以用ratio設定。這種情況下控制元件會在滿足比例 約束的條件下,儘可能填滿父佈局。

下面看個例子:

<?xml version="1.0" encoding="utf-8"?>
<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"
    tools:context="zr.com.constraintdemo.normal.RatioActivity">

    <Button
        android:id="@+id/btn_1"
        android:text="寬高比設定為2:1"
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        app:layout_constraintDimensionRatio="1:1"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        />

    <Button
        android:id="@+id/btn_2"
        android:text="寬高都設定為0dp,高寬比是16:9"
        android:textAllCaps="false"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintDimensionRatio="H,16:9"
        app:layout_constraintTop_toBottomOf="@id/btn_1" />

</android.support.constraint.ConstraintLayout>
複製程式碼

實現截圖

3.3.8 鏈

鏈這個概念是約束佈局新提出的,它提供了在一個維度(水平或者垂直),管理一組控制元件的方式。

建立一個鏈

多個view在同一個方向上雙向引用。如下圖所示:水平方向A、B、C,A位於B左邊,B位於A右邊,他們就是一對雙向引用。同理B和C也是。

ConstraintLayout在專案中實踐與總結

雙向引用佈局程式碼如下所示。A通過app:layout_constraintRight_toLeftOf="@+id/btn_2"引用右邊的B,B通過app:layout_constraintLeft_toRightOf="@+id/btn_1"引用A。

<Button
    android:id="@+id/btn_1"
    android:text="A"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_constraintTop_toBottomOf="@id/tv_spread"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toLeftOf="@+id/btn_2"
    app:layout_constraintHorizontal_chainStyle="spread_inside"
    />

<Button
    android:id="@+id/btn_2"
    android:text="B"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_constraintTop_toBottomOf="@id/tv_spread"
    app:layout_constraintLeft_toRightOf="@+id/btn_1"
    app:layout_constraintRight_toLeftOf="@+id/btn_3"
    />
    
    ...
複製程式碼
鏈頭

最左邊或最上面的控制元件,鏈的屬性由鏈頭控制。

設定

通過layout_constraintHorizontal_chainStylelayout_constraintVertical_chainStyle在鏈的第一個元素上設定。預設spread樣式。如上所示,A作為鏈頭,設定了chainStyle:app:layout_constraintHorizontal_chainStyle="spread_inside"

幾種鏈的樣式如下圖所示:

鏈的展示圖

鏈的佈局程式碼比較多,大家可以看demo。主要是通過修改鏈頭的chainStyle樣式改變鏈的型別。

3.4 Guideline

可以理解為佈局輔助線,用於佈局輔助,不在裝置上顯示。

有垂直和水平兩個方向(android:orientation=“vertical/horizontal”)

  • 垂直:寬度為0,高度等於父容器
  • 水平:高度為0,寬度等於父容器

有三种放置Guideline的方式:

  • 給定距離左邊或頂部一個固定距離(layout_constraintGuide_begin
  • 給定距離右邊或底部一個固定距離(layout_constraintGuide_end
  • 給定寬高一個百分比距離(layout_constraintGuide_percent

看例子:

<?xml version="1.0" encoding="utf-8"?>
<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:id="@+id/activity_guideline"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="zr.com.constraintdemo.GuidelineActivity">

    <!-- 垂直Guideline -->
    <android.support.constraint.Guideline
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/guideline"
        app:layout_constraintGuide_percent="0.5"
        android:orientation="vertical"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <Button
        android:text="GuideLine左邊"
        android:textAllCaps="false"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/btn_1"
        app:layout_constraintRight_toLeftOf="@+id/guideline"
        android:layout_marginTop="16dp"
        app:layout_constraintTop_toTopOf="parent"
        />

    <Button
        android:text="GuideLine右邊"
        android:textAllCaps="false"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/btn_2"
        app:layout_constraintLeft_toRightOf="@+id/guideline"
        android:layout_marginTop="16dp"
        app:layout_constraintTop_toTopOf="parent"
        />

    <!-- 水平Guideline -->
    <android.support.constraint.Guideline
        android:id="@+id/h_guideline"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintGuide_begin="200dp"
        />

    <Button
        android:text="Guideline上面"
        android:textAllCaps="false"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toTopOf="@id/h_guideline"
        app:layout_constraintLeft_toLeftOf="parent"
        />

    <Button
        android:text="Guideline下面"
        android:textAllCaps="false"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toBottomOf="@id/h_guideline"
        app:layout_constraintLeft_toLeftOf="parent"
        />

</android.support.constraint.ConstraintLayout>

複製程式碼

ConstraintLayout在專案中實踐與總結

3.5 程式碼中設定約束

通過ConstraintSet,允許在程式碼中進行約束設定,進行佈局變換。(API 19及以上支援trasmition動畫)

建立ConstraintSet物件的幾種方式:

  • 手動
c = new ConstraintSet(); 
c.connect(....);
複製程式碼
  • 通過一個R.layout.xxx物件
c.clone(context, R.layout.layout1);
複製程式碼
  • 通過一個ConstraintLayout物件
c.clone(clayout);
複製程式碼

佈局變化開啟平滑動畫的方式:

TransitionManager.beginDelayedTransition(constraintLayout);
複製程式碼

其中引數constraintLayout表示動畫作用的約束佈局物件。

看個例子:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        // 4.4以上開啟佈局切換動畫
        TransitionManager.beginDelayedTransition(constraintLayout);

// 清空margin
        applyConstraintSet.setMargin(R.id.btn_1, ConstraintSet.START, 0);
        applyConstraintSet.setMargin(R.id.btn_1, ConstraintSet.END, 0);
        applyConstraintSet.setMargin(R.id.btn_2, ConstraintSet.START, 0);
        applyConstraintSet.setMargin(R.id.btn_2, ConstraintSet.END, 0);
        applyConstraintSet.setMargin(R.id.btn_3, ConstraintSet.START, 0);
        applyConstraintSet.setMargin(R.id.btn_3, ConstraintSet.END, 0);

// 全部相對於父容器居中
        applyConstraintSet.centerHorizontally(R.id.btn_1, R.id.activity_constraint_set);
        applyConstraintSet.centerHorizontally(R.id.btn_2, R.id.activity_constraint_set);
        applyConstraintSet.centerHorizontally(R.id.btn_3, R.id.activity_constraint_set);
        applyConstraintSet.applyTo(constraintLayout);
    }
複製程式碼

看下在4.4系統以上動畫的一個效果:

4.4以上和以下切換的gif

更多ConstraintSet例子,推薦看這篇文章

四、開始實踐

說了這麼多,那麼約束佈局用起來到底怎麼樣呢?下面我們來實踐下:

前面類目的Item佈局具體實現

我們先來分析下類目Item,可以將類目Item分為兩個部分:父類目和子類目兩部分。父類目包括圖片icon和文字描述。子類目包含根據個數佈局可變的按鈕。很明顯,父類目通過約束佈局的相對位置約束設定可以實現。子類目中的子控制元件,可以以父佈局中的某個控制元件和子類目中其他子控制元件為參照物(依賴參照物件)實現佈局。總共放置兩排的按鈕,第一排3個,第二排2個,寬度設定為MATH_CONSTRAINT。然後在程式碼中根據子類目的個數,設定相應按鈕的可見性即可實現Item根據子類目個數展示不同佈局的效果。

佈局XML:

<?xml version="1.0" encoding="utf-8"?>
<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:paddingTop="7.5dp"
    android:paddingBottom="7.5dp"
    android:paddingLeft="12.5dp"
    android:paddingRight="12.5dp"
    android:clipToPadding="false"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <ImageView
        android:id="@+id/img_icon"
        android:layout_width="20dp"
        android:layout_height="20dp"
        android:layout_marginLeft="5dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:src="@mipmap/ic_launcher"
        />

    <TextView
        android:id="@+id/tv_parent_category_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="5dp"
        android:text="少兒程式設計"
        android:textSize="15sp"
        android:textColor="#333333"
        app:layout_constraintBottom_toBottomOf="@id/img_icon"
        app:layout_constraintLeft_toRightOf="@id/img_icon"
        app:layout_constraintTop_toTopOf="@id/img_icon" />

    <!-- 子類目佈局開始 -->
    <Button
        android:id="@+id/btn_one"
        android:layout_width="0dp"
        android:layout_height="40dp"
        android:layout_marginTop="10dp"
        android:text="A"
        app:layout_constraintTop_toBottomOf="@id/img_icon"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="@+id/btn_two" />

    <Button
        android:id="@+id/btn_two"
        android:layout_width="0dp"
        android:layout_height="40dp"
        android:text="B"
        app:layout_constraintLeft_toRightOf="@id/btn_one"
        app:layout_constraintRight_toLeftOf="@+id/btn_three"
        app:layout_constraintTop_toTopOf="@id/btn_one" />

    <Button
        android:id="@+id/btn_three"
        android:layout_width="0dp"
        android:layout_height="40dp"
        android:text="C"
        app:layout_constraintLeft_toRightOf="@id/btn_two"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="@id/btn_two" />

    <!-- 第二排 -->
    <Button
        android:id="@+id/btn_four"
        android:layout_width="0dp"
        android:layout_height="40dp"
        android:layout_marginTop="10dp"
        android:text="D"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="@+id/btn_five"
        app:layout_constraintTop_toBottomOf="@id/btn_one" />

    <Button
        android:id="@+id/btn_five"
        android:layout_width="0dp"
        android:layout_height="40dp"
        android:text="E"
        app:layout_constraintLeft_toRightOf="@+id/btn_four"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="@id/btn_four" />


</android.support.constraint.ConstraintLayout>
複製程式碼

實現效果:

實現類目效果截圖

下一道練習

課程列表

要求:圖片寬高比16:9,圖片寬度固定110dp。

分析:寬高比16:9,需要比例佈局;其他都是一些位置關係,用約束佈局相對位置的一些約束可以實現。

具體實現:

佈局XML:

<?xml version="1.0" encoding="utf-8"?>
<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="wrap_content"
    android:paddingLeft="15dp"
    android:paddingTop="12dp">

    <ImageView
        android:id="@+id/iv_course"
        android:layout_width="110dp"
        android:layout_height="0dp"
        android:scaleType="fitXY"
        android:src="@mipmap/test"
        app:layout_constraintDimensionRatio="16:9"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tv_course_name"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginLeft="10dp"
        android:layout_marginRight="15dp"
        android:ellipsize="end"
        android:maxLines="2"
        android:textColor="#333333"
        android:textSize="15sp"
        app:layout_constraintLeft_toRightOf="@id/iv_course"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="@id/iv_course"
        tools:text="六年級單元過關檢測六年級單元過關檢測六年級單元過關檢測" />

    <TextView
        android:id="@+id/tv_signature"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginRight="15dp"
        android:layout_marginTop="5dp"
        android:ellipsize="end"
        android:maxLines="1"
        android:textColor="#666666"
        android:textSize="12sp"
        app:layout_constraintLeft_toLeftOf="@id/tv_course_name"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/tv_course_name"
        tools:text="簽名" />

    <TextView
        android:id="@+id/tv_content"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginRight="15dp"
        android:layout_marginTop="5dp"
        android:ellipsize="end"
        android:maxLines="1"
        android:textColor="#666666"
        android:textSize="12sp"
        app:layout_constraintLeft_toLeftOf="@id/tv_course_name"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/tv_signature"
        tools:text="內容內容內容內容內容內容內容內容內容內容" />

    <TextView
        android:id="@+id/tv_current_price"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="5dp"
        android:maxLines="1"
        android:textColor="#f6454a"
        android:textSize="15sp"
        app:layout_constraintLeft_toLeftOf="@id/tv_course_name"
        app:layout_constraintTop_toBottomOf="@id/tv_content"
        tools:text="¥ 480" />

    <TextView
        android:id="@+id/tv_origin_price"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="5dp"
        android:maxLines="1"
        android:textColor="#999999"
        android:textSize="12sp"
        app:layout_constraintBottom_toBottomOf="@id/tv_current_price"
        app:layout_constraintLeft_toRightOf="@id/tv_current_price"
        tools:text="¥ 1480" />

</android.support.constraint.ConstraintLayout>
複製程式碼

實現截圖:

實現截圖

複雜度升級

要求:圖片寬度佔整個佈局30%,寬高比16:9。

分析:看到30%,首先考慮的是百分比佈局,但是圖片右邊的view較多,每個都是設定一邊百分比,實在是麻煩。因此,可以考慮使用Guideline,設定Guideline垂直,並距離父容器左邊30%的距離,之後佈局通過Guideline設定約束即可。

佈局XML:

<?xml version="1.0" encoding="utf-8"?>
<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="wrap_content"
    android:paddingLeft="15dp"
    android:paddingTop="12dp">

    <android.support.constraint.Guideline
        android:id="@+id/guideline"
        android:orientation="vertical"
        app:layout_constraintGuide_percent="0.3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <ImageView
        android:id="@+id/iv_course"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:scaleType="fitXY"
        android:src="@mipmap/test"
        app:layout_constraintDimensionRatio="16:9"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="@id/guideline"
        app:layout_constraintTop_toTopOf="parent" />
    ...
</android.support.constraint.ConstraintLayout>
複製程式碼

複雜度再升級

要求:在之前基礎上,底部加一根橫線用於分隔,要求線與上面最近的控制元件距離是15dp。

分析:由於文字內容是可變的,當文字內容多的時候,線可能距離文字近;若文字不多,線也可能距離圖片近。這個時候,基於當前最新1.0.2穩定版本的約束佈局已經不能滿足我們實現一層佈局了,還是需要將圖片和文字整體放入一個佈局容器中,然後橫線依賴這個佈局容器設定約束實現,巢狀好像在所難免了。然而,當約束佈局1.1.0穩定版本釋出時,這問題也可以得到解決。我們先來看看在1.1.0上是怎麼實現的:

<?xml version="1.0" encoding="utf-8"?>
<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="wrap_content"
    android:paddingLeft="15dp"
    android:paddingTop="12dp">

    ...

    <android.support.constraint.Barrier
        android:id="@+id/barrier"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:barrierDirection="bottom"
        app:constraint_referenced_ids="iv_course, tv_origin_price"
        />

    <View
        android:layout_width="match_parent"
        android:layout_height="1px"
        android:background="#d8d8d8"
        android:layout_marginLeft="15dp"
        android:layout_marginTop="12dp"
        app:layout_constraintTop_toBottomOf="@id/barrier"
        />

</android.support.constraint.ConstraintLayout>
複製程式碼

原來可以通過Barrier實現,那麼Barrier是什麼?請往下看。

五、效能怎麼樣?

本文主要介紹ConstraintLayout的使用,因此也不大篇幅講述效能相關內容。

  • 直觀可見的一點是,同樣一種複雜佈局,相對於傳統佈局方式,ConstraintLayout的佈局層級減少了。
  • 具體一些效能的對比,如渲染速度和計算次數等,可以看這篇文章《瞭解使用 ConstraintLayout 的效能優勢》。通過結論可知使用了ConstraintLayout,佈局計算次數降低了,渲染速度也相應提升了。

六、佈局編輯器

從Android studio 2.2版本開始,佈局編輯器支援拖拽的方式進行約束佈局。但是在2.2上佈局編輯器還不是很完善,部分約束不能設定,只能通過xml輸入方式實現。因此推薦用版本為2.3或者更高的Android studio。

限於篇幅,這裡就不展開介紹佈局編輯器了。在這裡推薦兩篇文章,分別是ConstraintLayout 終極祕籍(下) Android新特性介紹,ConstraintLayout完全解析。看完這兩篇,大家應該對佈局編輯器就會有比較深入的瞭解了。

七、ConstraintLayout使用小結

在使用約束佈局的過程中,有一些需要強調的點和碰到的一些坑分享給大家。

7.1 margin只能設定正值或者0,負值無效

我們之前實現重疊佈局時,會通過設定負的margin值實現。但是在約束佈局中,負的margin值不會生效,只能設定0或者大於0的值,小於0也當作0處理。

7.2 鏈的書寫方式注意

一般佈局我們都是遵守先定義,後使用原則,但是約束佈局實現鏈時,這個原則就遵守不了了。這個時候如果還是按照常規的@id/btn_2的方式指定依賴控制元件(這個控制元件在當前控制元件之後宣告的),就會報Error:(23, 46) No resource found that matches the given name錯誤。解決方案其實很簡單,只需要修改指定方式如下:@+id/btn_2即可。

7.3 ConstraintSet動畫Api支援等級

在程式碼中設定控制元件約束,可以通過ConstraintSet實現。約束變了之後,佈局肯定會跟著變。TransitionManager.beginDelayedTransition提供了平滑動畫變換佈局的能力,但是隻支援Api 19及以上的版本。

7.4 自定義guideLine

對Guideline設定相對位置屬性是不生效的,因此當我們想要一個相對於某個view的Guideline時,約束佈局是不能滿足我們的要求的。 看Guideline原始碼:

public class Guideline extends View {
    public Guideline(Context context) {
        super(context);
        super.setVisibility(8);
    }
    ...    
}
複製程式碼

發現Guideline是一個不可見的view,那麼我們可以佈局時放置一個不可見的view來作為Guideline的替代品,實現一些特殊佈局要求。如佈局重疊:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/activity_bias"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="zr.com.constraintdemo.normal.BiasActivity">

    <Button
        android:id="@+id/btn_a"
        android:text="A"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:background="@color/colorAccent"
        />

    <View
        android:id="@+id/view"
        android:layout_width="match_parent"
        android:layout_height="1px"
        app:layout_constraintBottom_toBottomOf="@id/btn_a"
        android:layout_marginBottom="40dp"
        />

    <Button
        android:text="B"
        android:background="@color/colorPrimary"
        android:layout_width="wrap_content"
        android:layout_height="200dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/view"
        />
</android.support.constraint.ConstraintLayout>
複製程式碼

ConstraintLayout在專案中實踐與總結

這種方式可以彌補margin不能設定為負值的不足,而且並沒有增加布局層級。

7.5 區分0dp、match_parentMATCH_CONSTRAINT

  • 0dp等價於MATCH_CONSTRAINT,對控制元件設定其它尺寸相關約束會生效。如app:layout_constraintWidth_min等約束。
  • match_parent,填充滿父佈局,之後設定約束屬性無效。

7.6 使用佈局編輯器多出了一些屬性

layout_optimizationLevel
layout_editor_absoluteX
layout_editor_absoluteY
layout_constraintBaseline_creator
layout_constraintTop_creator
layout_constraintRight_creator
layout_constraintLeft_creator
layout_constraintBottom_creator
複製程式碼

這幾個屬性是 UI 編輯器所使用的,用了輔助拖拽佈局的,在實際使用過程中,可以不用關心這些屬性。

八、即將到來的一些有意思的特性

最新的約束佈局beta版本,已經出到了1.1.0-beta3。在將來約束佈局1.1.0版本釋出後,其中會包含一下一些有意思的特性,讓人看了充滿期待。我們先來一睹為快:

  • Barrier
    Barrier是一個虛擬的輔助控制元件,它可以阻止一個或者多個控制元件越過自己,就像一個屏障一樣。當某個控制元件要越過自己的時候,Barrier會自動移動,避免自己被覆蓋。

  • Group
    Group幫助你對一組控制元件進行設定。最常見的情況是控制一組控制元件的visibility。你只需把控制元件的id新增到Group,就能同時對裡面的所有控制元件進行操作。

  • Circular positioning
    可以相對另一個控制元件,以角度和距離定義當前控制元件的位置,即提供了在圓上定義控制元件位置的能力。如圖所示:

ConstraintLayout在專案中實踐與總結

  • Placeholder
    Placeholder顧名思義,就是用來一個佔位的東西,它可以把自己的內容設定為ConstraintLayout內的其它view。因此它用來寫佈局的模版,也可以用來動態修改UI的內容。

  • 百分比佈局
    允許設定控制元件佔據可用空間的百分比,大大增加布局靈活度和適配性。

總而言之,約束佈局的能力正在變得越來越強大。

九、最後

曾幾何時,對於複雜佈局,很多時候不是一種佈局就可以解決。這時需要考慮佈局巢狀,又或者需要在程式碼中動態設定控制元件寬高比,無形中增加了開發的複雜性和佈局的巢狀層級,進而影響了頁面效能。隨著google推出了ContraintLayout,上述的問題大部分都可以得到有效的解決。

總的來說,ConstraintLayout優勢如下:

  • 佈局高效
  • 輕鬆應對複雜佈局
  • 巢狀層級少
  • 適配性好

本人通過在專案中的實踐,真切體會到了ConstraintLayout應對複雜佈局和自適應頁面的強大能力,不但降低了佈局難度,而且提升了開發效率。開發過程中基本沒怎麼踩深坑,因此也很推薦大家在專案中去使用ConstraintLayout佈局。

附上demo的連結github.com/yushiwo/Con…,當然更建議大家自己去寫一遍,可以加深印象。

參考文獻

  1. ConstraintLayout 終極祕籍(上)
  2. ConstraintLayout 終極祕籍(下)
  3. 瞭解使用 ConstraintLayout 的效能優勢
  4. [譯]Constraint Layout 動畫 |動態 Constraint |用 Java 實現的 UI(這到底是什麼)[第三部分]
  5. Constraint Layout 1.1.x帶來了哪些新東西?
  6. 當然還有官方文件啦

相關文章