ConstraintLayout在項目中實踐與總結

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也是。

image

雙向引用布局代碼如下所示。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>

image

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>
image

這種方式可以彌補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
    可以相對另一個控件,以角度和距離定義當前控件的位置,即提供了在圓上定義控件位置的能力。如圖所示:

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

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

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

九、最后

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

總的來說,ConstraintLayout優勢如下:

  • 布局高效
  • 輕松應對復雜布局
  • 嵌套層級少
  • 適配性好

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

附上demo的鏈接https://github.com/yushiwo/ConstraintDemo,當然更建議大家自己去寫一遍,可以加深印象。

參考文獻

  1. ConstraintLayout 終極秘籍(上)
  2. ConstraintLayout 終極秘籍(下)
  3. 了解使用 ConstraintLayout 的性能優勢
  4. [譯]Constraint Layout 動畫 |動態 Constraint |用 Java 實現的 UI(這到底是什么)[第三部分]
  5. Constraint Layout 1.1.x帶來了哪些新東西?
  6. 當然還有官方文檔啦
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,401評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,011評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,263評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,543評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,323評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,874評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,968評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,095評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,605評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,551評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,720評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,242評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,961評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,358評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,612評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,330評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,690評論 2 370

推薦閱讀更多精彩內容