ConstraintLayout教程
原文地址: https://www.raywenderlich.com/9475-constraintlayout-tutorial-for-android-complex-layouts
- 轉換其他型別的佈局到ConstraintLayout
- 相對於在螢幕上的其他元素動態定位UI元素
- 讓你的Views實現動畫
在這篇教程中,會構建一個星際旅行的app,火箭將會繞著行星不停的轉動。

星際旅行
在這個app中有很多元素,你能夠學到如何使用複雜的ConstraintLayout來正常的顯示他們。
轉換一個佈局到ConstraintLayout
在 Component Tree 面板中,在最頂層的佈局中右鍵並且選擇 Convert LinearLayout to ConstraintLayout :

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

image
閱讀完所有內容後不改變預設的選中狀態,然後點選ok關閉對話方塊,AndroidStudio接下來就會把你的佈局轉換成ConstraintLayout。
轉換過後,你的佈局就變成下面的這個樣子:

image
如果此時所有的檢視都跑到了左上角,請不要驚慌,
請確保關閉 AutoConnect

image
移除推斷約束
執行轉換的過程中,AndroidStudio會執行好幾個步驟,最後一步就是進行推斷約束,但是結果並不是你想要的,此時,你只需要轉到編輯選單並選擇撤銷推斷約束。

image
或者,執行cmd+Z,現在你的介面看起來如下:

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

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

image
調整Image
通過點選頂部的每個圖示, spaceStationIcon , flightIcon 和 roverIcon 來修復影象的大小,接著在屬性面板,將 layout_width 和 layout_heigth 從 wrap_content 改為30dp。

image
此時你將會在 Comoonent Tree 中看到一堆錯誤,這是因為沒有任何約束資訊告訴它在哪裡定位,現在開始解決這個問題。
新增約束:找出對齊方式
使用自上而下的方法設定約束,從螢幕頂部的元素開始,一直向下設定。
希望頂部三個圖示水平分佈排列,然後標籤置於圖示下方
約束第一個圖示
單擊第一個圖示並且顯示約束錨點,點選上面的錨點並且拖動到頂部的view,該圖示就會自動滑動到頂部。先不要連線左側的約束、
接著,切換到Code介面檢查第一個圖示xml的更新,發現添加了一個新的約束 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" />
你也可以在設計檢視中調整邊距,切換到設計檢視點選 Attributes tab。
接著點選第一個圖示並且檢視屬性,你會看到margins的圖形表示。
你可以通過從下拉選單中選擇邊距或單擊數字並輸入新值來為邊距選擇新值。

image
水平對齊頂部的三個圖示:使用Chains
接著,希望頂部的三個圖示在同一條水平線上並且平均分佈,需要為每一個圖示新增一系列的約束,這裡有個更快速的方法就是使用chains。
Chains
如果有雙向約束,就會出現鏈條,當你使用了選單中的對齊約束時,Android Studio實際上就使用了鏈條,你可以將不同的樣式、權重、邊距應用到鏈條.
接著切換到設計面板,同時選中頂部的三個圖示,右鍵然後選擇 Center -> Horizontally ,此時會自動建立一個鏈條並且生成約束。
在設計面板你就能看到鏈與其他約束的不同,其他的是波浪線表示,而鏈條是一條鏈。

image
探索鏈條
要探索某些鏈的模式,選擇一個元素,單擊圖示底部顯示的 迴圈鏈模式 按鈕。

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

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

image
此時三個圖示的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面板,就重新再做一次。
對齊每個圖示的文字
接著設定圖示下面的文字,將第一個Text的左側的約束連線到第一個圖示的左側,右側的約束連線到右側,上面的約束新增到上面,其他三個Text做同樣的操作。
然後將工具欄中的預設邊距更改為15dp,只需要將頂部錨點拖動到圖示的底部錨點,即可在一個步驟中設定約束和邊距。

image
現在上面兩行的約束錯誤已經消失了,XML中圖示和標籤的程式碼如下:
<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
在最終的佈局中,雙箭頭影象應居中並且與兩個綠色檢視重疊

image
設定水平和垂直的Guidelines
選中雙箭頭圖示並且設定寬高為60dp,然後在該圖示上點選右鍵,選擇 Center -> Horizontally in Parent .

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

image
將會新增一條水平的虛線到佈局中。
在 Conponent Tree 中選中水平guideline,在Attributes 檢查器中,改變ID為guideline1,注意guideline屬性: layout_constraintGuide_begin 和 layout_constraintGuide_percent
對於水平的guideline1,設定 layout_constraintGuide_begin 為200dp

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

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

image
選擇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,你可以設定幀動畫從而是你的views動起來,為此,你需要提供佈局檔案的副本,稱為ConstraintSet,ConstraintSet只需要包含給定ConstraintLayout中元素的約束,邊距以及填充。
如果你使用的是kotlin程式碼,那麼你可以直接將ConstraintSet應用到你的ConstraintLayout。
要構建動畫,你需要指定單個佈局檔案和ConstraintSet作為起始和結束關鍵幀,你也可以應用過渡是動畫更有趣。
設定動畫的起始佈局
在專案中複製佈局檔案並命名為keyframe1.xml,並將此佈局設定為應用程式的起始佈局。
開啟keyframe1.xml,將guideline1的layout_constraintGuide_begin屬性值從200dp改為0dp,這樣會移動guideline,限制在guideline中的元素,將會移除螢幕
接著將guideline2的layout_constraintGuide_percent屬性值從0.05修改成1,這會將指南移動到螢幕最右側,從而受其約束的元素被移動到螢幕外。
接著修改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到另一個的過渡,建立一個佈局動畫,你只需要向Transition Manager提供要設定動畫的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。
接著執行app可以看到大小的改變。

image
使用自定義過渡使動畫更簡單
建立一個自定義動畫來替代預設的動畫,可以自定義動畫的時長。
新增如下方法到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) }
- 動畫執行過程中,Activity無法繪製任何內容,onEnterAnimationComplete()方法表示動畫執行完成,可以呼叫繪製程式碼。
- 會將佈局資訊從最終佈局拉入constraintSet2
- 建立一個自定義過渡,使用AutoTransition,首先淡出要消失的目標,然後移動並調整現有目標的大小,最後淡出出現的目標。
- 動畫執行時長為1000毫秒
- 呼叫Transition Manager的beginDelayedTransition方法,但這次提供的是自定義過渡
- 應用一個新的ConstraintSet到當前消失的ConstraintLayout上。
效果如下:

image
使圓形約束動起來
要在火星周圍製作火箭動畫,必須改變兩個屬性:圓形約束的角度,他將火箭的位置移動到圓周,以及火箭的旋轉來完成動畫,你還可以檢查單向/往返開關值以確定火箭是飛行半圈還是一整圈。
替換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設定為火箭的當前角度,依賴one way/ Round Trip切換,endAngle在startAngle的之上新增180或360
- 使用startAngle和endAngle建立一個ValueAnimator
- 在動畫監聽器中,獲取動畫值並將其設定給rocket的layoutParams中的circleAngle屬性
- 用動畫值旋轉火箭,將使火箭飛的更加自然。
- 單向動畫需要一秒,而往返動畫需要2秒
- 使用LinearInterpolator,可以試試AnticipateOvershootInterpolator看看會發生什麼!

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