原文地址:https://www.raywenderlich.com/9475-constraintlayout-tutorial-for-android-complex-layouts
- 轉(zhuǎn)換其他類型的布局到ConstraintLayout
- 相對于在屏幕上的其他元素動態(tài)定位UI元素
- 讓你的Views實現(xiàn)動畫
在這篇教程中,會構(gòu)建一個星際旅行的app,火箭將會繞著行星不停的轉(zhuǎn)動。

在這個app中有很多元素,你能夠?qū)W到如何使用復(fù)雜的ConstraintLayout來正常的顯示他們。
轉(zhuǎn)換一個布局到ConstraintLayout
在Component Tree面板中,在最頂層的布局中右鍵并且選擇Convert LinearLayout to ConstraintLayout:

接著會看到一個彈出框并且有一些選項:

閱讀完所有內(nèi)容后不改變默認(rèn)的選中狀態(tài),然后點擊ok關(guān)閉對話框,AndroidStudio接下來就會把你的布局轉(zhuǎn)換成ConstraintLayout。
轉(zhuǎn)換過后,你的布局就變成下面的這個樣子:

如果此時所有的視圖都跑到了左上角,請不要驚慌,
請確保關(guān)閉AutoConnect
image
移除推斷約束
執(zhí)行轉(zhuǎn)換的過程中,AndroidStudio會執(zhí)行好幾個步驟,最后一步就是進(jìn)行推斷約束,但是結(jié)果并不是你想要的,此時,你只需要轉(zhuǎn)到編輯菜單并選擇撤銷推斷約束。

或者,執(zhí)行cmd+Z,現(xiàn)在你的界面看起來如下:

你可以稍微拖動視圖,讓他看起來更像原始的樣子:

如果在拖動過程中Android Studio自動添加了任何的約束,可以點擊Clear All Constraints按鈕去清除他們。

調(diào)整Image
通過點擊頂部的每個圖標(biāo),spaceStationIcon,flightIcon和roverIcon來修復(fù)圖像的大小,接著在屬性面板,將layout_width和layout_heigth從wrap_content改為30dp。

此時你將會在Comoonent Tree中看到一堆錯誤,這是因為沒有任何約束信息告訴它在哪里定位,現(xiàn)在開始解決這個問題。
添加約束:找出對齊方式
使用自上而下的方法設(shè)置約束,從屏幕頂部的元素開始,一直向下設(shè)置。
希望頂部三個圖標(biāo)水平分布排列,然后標(biāo)簽置于圖標(biāo)下方
約束第一個圖標(biāo)
單擊第一個圖標(biāo)并且顯示約束錨點,點擊上面的錨點并且拖動到頂部的view,該圖標(biāo)就會自動滑動到頂部。先不要連接左側(cè)的約束、
接著,切換到Code界面檢查第一個圖標(biāo)xml的更新,發(fā)現(xiàn)添加了一個新的約束app:layout_constraintTop_toTopOf="parent",XML看起來如下:
<ImageView
android:id="@+id/spaceStationIcon"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginTop="15dp"
android:src="@drawable/space_station_icon"
app:layout_constraintTop_toTopOf="parent" />
你也可以在設(shè)計視圖中調(diào)整邊距,切換到設(shè)計視圖點擊Attributestab。
接著點擊第一個圖標(biāo)并且查看屬性,你會看到margins的圖形表示。
你可以通過從下拉菜單中選擇邊距或單擊數(shù)字并輸入新值來為邊距選擇新值。

水平對齊頂部的三個圖標(biāo):使用Chains
接著,希望頂部的三個圖標(biāo)在同一條水平線上并且平均分布,需要為每一個圖標(biāo)添加一系列的約束,這里有個更快速的方法就是使用chains。
Chains
如果有雙向約束,就會出現(xiàn)鏈條,當(dāng)你使用了菜單中的對齊約束時,Android Studio實際上就使用了鏈條,你可以將不同的樣式、權(quán)重、邊距應(yīng)用到鏈條.
接著切換到設(shè)計面板,同時選中頂部的三個圖標(biāo),右鍵然后選擇Center -> Horizontally,此時會自動創(chuàng)建一個鏈條并且生成約束。
在設(shè)計面板你就能看到鏈與其他約束的不同,其他的是波浪線表示,而鏈條是一條鏈。

探索鏈條
要探索某些鏈的模式,選擇一個元素,單擊圖標(biāo)底部顯示的循環(huán)鏈模式按鈕。

模式有:
- Packed:元素會被壓縮到一起
- Spread:如上所示,元素被分布到可用空間上
- Spread inside: 與spread類似,但鏈的端點不會分散。

確保以spread為鏈的模式,修改方法有兩種:
- 視圖將以實力屏幕截圖中的圖標(biāo)間隔顯示
- 在其中一個圖標(biāo)的xml中將會出現(xiàn)app:layout_constraintHorizontal_chainStyle="spread",更新該屬性,可以將鏈的模式改變成其他的。
對齊Views
接著,再次選中三個圖標(biāo),從tool bar中,選擇Align -> Vertical Centers.Android Studio會添加約束用來使每一個view的底部和頂部與相鄰的view對齊。
布局看起來如下:

此時三個圖標(biāo)的xml如下所示:
<ImageView
android:id="@+id/spaceStationIcon"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginTop="15dp"
android:src="@drawable/space_station_icon"
app:layout_constraintEnd_toStartOf="@+id/flightsIcon"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/flightsIcon"
android:layout_width="30dp"
android:layout_height="30dp"
android:src="@drawable/rocket_icon"
app:layout_constraintBottom_toBottomOf="@+id/spaceStationIcon"
app:layout_constraintEnd_toStartOf="@+id/roverIcon"
app:layout_constraintStart_toEndOf="@+id/spaceStationIcon"
app:layout_constraintTop_toTopOf="@+id/spaceStationIcon" />
<ImageView
android:id="@+id/roverIcon"
android:layout_width="30dp"
android:layout_height="30dp"
android:src="@drawable/rover_icon"
app:layout_constraintBottom_toBottomOf="@+id/flightsIcon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/flightsIcon"
app:layout_constraintTop_toTopOf="@+id/flightsIcon" />
如果你的界面跟圖片不匹配,檢查Text和Design面板,就重新再做一次。
對齊每個圖標(biāo)的文本
接著設(shè)置圖標(biāo)下面的文字,將第一個Text的左側(cè)的約束連接到第一個圖標(biāo)的左側(cè),右側(cè)的約束連接到右側(cè),上面的約束添加到上面,其他三個Text做同樣的操作。
然后將工具欄中的默認(rèn)邊距更改為15dp,只需要將頂部錨點拖動到圖標(biāo)的底部錨點,即可在一個步驟中設(shè)置約束和邊距。

現(xiàn)在上面兩行的約束錯誤已經(jīng)消失了,XML中圖標(biāo)和標(biāo)簽的代碼如下:
<TextView
android:id="@+id/roverLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:text="@string/rovers"
app:layout_constraintEnd_toEndOf="@+id/roverIcon"
app:layout_constraintStart_toStartOf="@+id/roverIcon"
app:layout_constraintTop_toBottomOf="@+id/roverIcon" />
<TextView
android:id="@+id/flightsLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:text="@string/flights"
app:layout_constraintEnd_toEndOf="@+id/flightsIcon"
app:layout_constraintStart_toStartOf="@+id/flightsIcon"
app:layout_constraintTop_toBottomOf="@+id/flightsIcon" />
<TextView
android:id="@+id/spaceStationLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:text="@string/space_stations"
app:layout_constraintEnd_toEndOf="@+id/spaceStationIcon"
app:layout_constraintStart_toStartOf="@+id/spaceStationIcon"
app:layout_constraintTop_toBottomOf="@+id/spaceStationIcon" />
Using Guidelines
在最終的布局中,雙箭頭圖像應(yīng)居中并且與兩個綠色視圖重疊

設(shè)置水平和垂直的Guidelines
選中雙箭頭圖標(biāo)并且設(shè)置寬高為60dp,然后在該圖標(biāo)上點擊右鍵,選擇Center -> Horizontally in Parent.

為每一個綠色的TextView設(shè)置寬為124dp,高為98dp。
確保雙箭頭圖片在兩個綠色的TextView之上,將左側(cè)TextView的右側(cè)約束到雙箭頭圖片的右側(cè),并將右邊距設(shè)置為40dp。
同樣的將右側(cè)綠色的TextView的左側(cè)約束到雙箭頭圖標(biāo)的左側(cè),并將左側(cè)邊距設(shè)置為40dp。
最后,將兩個TextView的上下分別約束到雙箭頭圖標(biāo)的上下。
[圖片上傳失敗...(image-5a6638-1550557283059)]
最后,點擊Guidelines,選擇Add Horizontal Guideline

將會添加一條水平的虛線到布局中。
在Conponent Tree中選中水平guideline,在Attributes 檢查器中,改變ID為guideline1,注意guideline屬性:layout_constraintGuide_begin 和 layout_constraintGuide_percent
對于水平的guideline1,設(shè)置layout_constraintGuide_begin為200dp

最后,添加一個豎直的guideline,設(shè)置id為guideline2并且設(shè)置layout_constraintGuide_percent為0.05,這將guideline2定位到距離屏幕左側(cè)為屏幕寬度5%的位置。
定位Guidelines
定位guideline使用下面三個屬性:
- layout_constraintGuide_begin: 從左側(cè)或其父級的頂部定位具有指定dp的guideline
- layout_constraintGuide_end: 從右側(cè)或其父級底部定位指定dp的guideline
- layout_constraintGuide_percent: 使用百分比來定位guideline
添加約束到Guidelines
現(xiàn)在guidelines已經(jīng)設(shè)置了,可以給他們添加一些約束。
首先,對于雙箭頭圖標(biāo):
- 將底部約束到水平guideline
- 設(shè)置底部邊距為40dp
對于開關(guān):
- 設(shè)置寬度為160dp
- 設(shè)置左側(cè)約束到垂直guideline
- 設(shè)置top約束到父布局
- 設(shè)置top margin為200dp
對于開關(guān)下面的標(biāo)簽:
- 設(shè)置左邊的約束到垂直guideline
- 設(shè)置頂部約束到開關(guān)的底部
對于galaxy icon
- 設(shè)置寬高為90dp
- 設(shè)置top約束到水平guideline
- 約束bottom到父布局的底部,這樣就會在水平guideline和底部之間居中
- 在父視圖中將其水平居中
對于rocket icon:
- 設(shè)置寬和高為30dp
- 約束rocket icon的上下和右到galaxy圖標(biāo)的上下和左邊
最后,對于DEPART按鈕:
- 將寬度從wrap_content修改為match_parent
- 將底部約束到parent的底部
此時,你已經(jīng)設(shè)置完了所有的約束,在Component Tree中也沒有任何錯誤了,布局此時看起來如下所示:

圓形位置約束
除了上面的,還可以使用距離和角度來約束UI元素。允許你將他們防止到一個圓上,其中一個元素位于圓的中心,另一個元素位于圓周上。

選擇rocket icon,并在代碼視圖中更新其代碼,代碼如下:
<ImageView
android:id="@+id/rocketIcon"
android:layout_width="30dp"
android:layout_height="30dp"
android:src="@drawable/rocket_icon"
app:layout_constraintCircle="@id/galaxyIcon"
app:layout_constraintCircleAngle="270"
app:layout_constraintCircleRadius="100dp" />
第一個約束屬性layout_constraintCircle指示將位于圓心的UI元素的ID,另外兩個屬性表示角度和半徑。
讓UI元素在屏幕上動起來
約束集
使用ConstraintLayout,你可以設(shè)置幀動畫從而是你的views動起來,為此,你需要提供布局文件的副本,稱為ConstraintSet,ConstraintSet只需要包含給定ConstraintLayout中元素的約束,邊距以及填充。
如果你使用的是kotlin代碼,那么你可以直接將ConstraintSet應(yīng)用到你的ConstraintLayout。
要構(gòu)建動畫,你需要指定單個布局文件和ConstraintSet作為起始和結(jié)束關(guān)鍵幀,你也可以應(yīng)用過渡是動畫更有趣。
設(shè)置動畫的起始布局
在項目中復(fù)制布局文件并命名為keyframe1.xml,并將此布局設(shè)置為應(yīng)用程序的起始布局。
打開keyframe1.xml,將guideline1的layout_constraintGuide_begin屬性值從200dp改為0dp,這樣會移動guideline,限制在guideline中的元素,將會移除屏幕
接著將guideline2的layout_constraintGuide_percent屬性值從0.05修改成1,這會將指南移動到屏幕最右側(cè),從而受其約束的元素被移動到屏幕外。
接著修改MainActivity中的setContentView中的R.layout.activity_main為R.layout.keyframe1
動畫視圖
將MainActivity中的:
import kotlinx.android.synthetic.main.activity_main.*
改為:
import kotlinx.android.synthetic.main.keyframe1.*
可以讓你直接飲用UI中的id,而不用使用findViewById(),
接著,添加如下的代碼:
private val constraintSet1 = ConstraintSet()
private val constraintSet2 = ConstraintSet()
private var isOffscreen = true
Transition Manager
可以使用Transition Manager類來處理從一個keyframe到另一個的過渡,創(chuàng)建一個布局動畫,你只需要向Transition Manager提供要設(shè)置動畫的ConstraintSet,他將會處理其余的部分。
將如下的代碼添加到onCreate方法中:
constraintSet1.clone(constraintLayout) //1
constraintSet2.clone(this, R.layout.activity_main) //2
departButton.setOnClickListener { //3
//apply the transition
TransitionManager.beginDelayedTransition(constraintLayout) //4
val constraint = if (!isOffscreen) constraintSet1 else constraintSet2
isOffscreen = !isOffscreen
constraint.applyTo(constraintLayout) //5
}
動畫視圖的界限
不僅可以通過影響其約束來更改屏幕上元素的位置,還可以改變其大小。
打開keyframe1.xml選擇galaxy icon,id為galaxyIcon,將高度從90dp改為10dp。
接著運(yùn)行app可以看到大小的改變。

使用自定義過渡使動畫更簡單
創(chuàng)建一個自定義動畫來替代默認(rèn)的動畫,可以自定義動畫的時長。
添加如下方法到MainActivity中。
override fun onEnterAnimationComplete() {
super.onEnterAnimationComplete()
constraintSet2.clone(this, R.layout.activity_main)
val transition = AutoTransition()
transition.duration = 1000
TransitionManager.beginDelayedTransition(constraintLayout,transition)
constraintSet2.applyTo(constraintLayout)
}
- 動畫執(zhí)行過程中,Activity無法繪制任何內(nèi)容,onEnterAnimationComplete()方法表示動畫執(zhí)行完成,可以調(diào)用繪制代碼。
- 會將布局信息從最終布局拉入constraintSet2
- 創(chuàng)建一個自定義過渡,使用AutoTransition,首先淡出要消失的目標(biāo),然后移動并調(diào)整現(xiàn)有目標(biāo)的大小,最后淡出出現(xiàn)的目標(biāo)。
- 動畫執(zhí)行時長為1000毫秒
- 調(diào)用Transition Manager的beginDelayedTransition方法,但這次提供的是自定義過渡
- 應(yīng)用一個新的ConstraintSet到當(dāng)前消失的ConstraintLayout上。
效果如下:

使圓形約束動起來
要在火星周圍制作火箭動畫,必須改變兩個屬性:圓形約束的角度,他將火箭的位置移動到圓周,以及火箭的旋轉(zhuǎn)來完成動畫,你還可以檢查單向/往返開關(guān)值以確定火箭是飛行半圈還是一整圈。
替換DEPART button的點擊事件的代碼為如下的代碼:
departButton.setOnClickListener {
// TransitionManager.beginDelayedTransition(constraintLayout)
// val constraint = if (!isOffscreen) constraintSet1 else constraintSet2
// isOffscreen = !isOffscreen
// constraint.applyTo(constraintLayout)
val layoutParams = rocketIcon.layoutParams as ConstraintLayout.LayoutParams
val startAngle = layoutParams.circleAngle
val endAngle = startAngle + (if (switch1.isChecked) 360 else 180)
val anim = ValueAnimator.ofFloat(startAngle,endAngle)
anim.addUpdateListener { valueAnimator ->
val animatedValue = valueAnimator.animatedValue as Float
val layoutParams = rocketIcon.layoutParams as ConstraintLayout.LayoutParams
layoutParams.circleAngle = animatedValue
rocketIcon.layoutParams = layoutParams
rocketIcon.rotation = (animatedValue % 360 - 270)
}
anim.duration = if(switch1.isChecked) 2000 else 1000
anim.interpolator = LinearInterpolator()
anim.start()
}
- 在動畫開始之前,將火箭的startAngle設(shè)置為火箭的當(dāng)前角度,依賴one way/ Round Trip切換,endAngle在startAngle的之上添加180或360
- 使用startAngle和endAngle創(chuàng)建一個ValueAnimator
- 在動畫監(jiān)聽器中,獲取動畫值并將其設(shè)置給rocket的layoutParams中的circleAngle屬性
- 用動畫值旋轉(zhuǎn)火箭,將使火箭飛的更加自然。
- 單向動畫需要一秒,而往返動畫需要2秒
- 使用LinearInterpolator,可以試試AnticipateOvershootInterpolator看看會發(fā)生什么!

最后
如果你比較喜歡view動畫,那么可以使用MotionEvent嘗試更多的動畫,https://youtu.be/S3FeIRKu_Z8?t=1275
這篇文章內(nèi)容真的挺豐富的,寫的很不錯,所以就簡單翻譯了一下,如果有朋友覺得哪些翻譯的不夠好的,歡迎給我評論,我會及時糾正。
