ConstraintLayout,讓布局更優雅。
一、為什么要用ConstraintLayout
上圖是網易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了。有兩種方式使用:
-
layout轉換的方式使用
首先,打開一個非ConstraintLayout的布局文件,切換到
Design Tab
在Component Tree窗口,選中要轉換的layout文件根布局,點擊右鍵,然后選擇Convert layout to ConstraintLayout
直接新建一個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_min
和layout_constraintHeight_min
:設置最小值layout_constraintWidth_max
和layout_constraintHeight_max
:設置最大值-
layout_constraintWidth_percent
和layout_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>
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也是。
雙向引用布局代碼如下所示。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_chainStyle
和layout_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>
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系統以上動畫的一個效果:
更多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>
這種方式可以彌補margin不能設置為負值的不足,而且并沒有增加布局層級。
7.5 區分0dp、match_parent
和MATCH_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
可以相對另一個控件,以角度和距離定義當前控件的位置,即提供了在圓上定義控件位置的能力。如圖所示:
Placeholder
Placeholder顧名思義,就是用來一個占位的東西,它可以把自己的內容設置為ConstraintLayout內的其它view。因此它用來寫布局的模版,也可以用來動態修改UI的內容。百分比布局
允許設置控件占據可用空間的百分比,大大增加布局靈活度和適配性。
總而言之,約束布局的能力正在變得越來越強大。
九、最后
曾幾何時,對于復雜布局,很多時候不是一種布局就可以解決。這時需要考慮布局嵌套,又或者需要在代碼中動態設置控件寬高比,無形中增加了開發的復雜性和布局的嵌套層級,進而影響了頁面性能。隨著google推出了ContraintLayout,上述的問題大部分都可以得到有效的解決。
總的來說,ConstraintLayout優勢如下:
- 布局高效
- 輕松應對復雜布局
- 嵌套層級少
- 適配性好
本人通過在項目中的實踐,真切體會到了ConstraintLayout應對復雜布局和自適應頁面的強大能力,不但降低了布局難度,而且提升了開發效率。開發過程中基本沒怎么踩深坑,因此也很推薦大家在項目中去使用ConstraintLayout布局。
附上demo的鏈接https://github.com/yushiwo/ConstraintDemo,當然更建議大家自己去寫一遍,可以加深印象。