前言:ConstraintLayout作為google欽定的代替LinearLayout及RelativeLayout的布局值得我們學習,并且google仍舊想借此布局繼續改善之前布局可視化編輯上的雞肋效果,但由于當下版本新的可視化效果在布局編輯器上與實際運行結果仍舊可能略有不同,我們還是很難在不編輯代碼的情況下僅靠拖拽來編輯布局,所以本文不會從拖拽入手,而是從布局各種相關API的代碼層面學習該布局的使用
對于ConstraintLayout來講,為了更好的在實際應用過程中使用它,有許多重要的概念及規則需要了解熟記,通過本文,你不僅能看到ConstraintLayout官方說明的譯文,還將通過多個事例來模擬實際使用中容易遇見的問題,希望以拋磚引玉的方式引出實際應用中可能遇到的坑,并加以避免
1.layout_constraintA_toBOf
1.1基礎概念
這里的A與B的取值分別為下圖的top、left、right等,并有形如top-bottom、start-end的對應關系,并且這種對應關系是必然的,下面我們會說明原因
例如:
app:layout_constraintTop_toBottomOf="@+id/"
所表達的意思為令控件A的top邊與控件B的bottom邊共享同一塊相同的位置(share the same location)
代碼運行結果為:
所以我們就能理解不可能出現layout_constraintTop_toLeftOf這樣的API,因為一個是豎邊,一個是橫邊,不可能共享同一塊位置
其他的API以此類推很好理解,那baseline的運行結果會是如何?為了對比,我們先看如下代碼的運行結果:
我們令B在A的右邊,然后設置B的寬高與字體大小大于A,運行結果如我們所預想:
然后我們設置:
app:layout_constraintBaseline_toBaselineOf="@+id/tv_a"
運行結果會不會如我們所想呢:
觀察結果可知,由于B的高度大于A,所以B上面的一部分移動到了屏幕之外,并且由于B字體大小大于A,所以即便是指定BaseLine在同一位置,B的文字也要高于A
我們再修改代碼,為父布局加入paddingTop,看能否使被屏幕遮蓋的布局“移下來”:
觀察結果:
B移到了父布局的外部,在父布局上加入padding會導致子布局的內容整體下移,這與我們在其他布局中的經驗是一致的,既然這樣的話,若我們直接在A上設置marginTop能否讓A“帶著”B一起下來呢,我們繼續修改代碼:
運行結果:
觀察結果我們發現A并沒有按照預想挪下來,究其原因是由于我們沒有為A布局設置約束(Constraint),所以A不知道該如何偏移,我們為A添加約束,并將A的約束對象設置為父元素ConstraintLayout本身:
觀察代碼運行結果:
綜上,約束布局中,若期望設置某控件的margin來控制顯示位置,則必須指定該控件在該方向上的約束:
官方解釋為如果設置控件的margin,它會施加在存在的對應約束上(If side margins are set, they will be applied to the corresponding constraints (if they exist))
1.2居中
查找發現API沒有為我們提供例如layoutGravity或centerInParent之類的方法讓我們把一個控件在約束布局內居中,但是可以通過官方提供的方式來達到目的
指定讓控件的寬高與父布局重疊并不會令該控件鋪滿父布局,除非父布局與控件擁有完全相同的大小,不然只會令控件在指定的方向上居中
當控件布局成這種狀態下時,我們可以通過設置偏移(bias)來改變控件的默認(50%)位置:
1.3由居中寫法引申而來的問題
有了以上的基礎概念,我們來分析幾個典型的例子,在進一步理解的同時也能發現其中的一些問題
1.3.1
上面居中代碼中,我們將A的左右邊與父布局的左右邊設定約束,最終的結果為A居中于父布局,若我們將A的右邊與B的右邊設定約束,效果會如何呢:
運行結果:
可見A被從父布局的左邊“推離”了,而在與B大小相等的位置“居中”,此時我們便能隱約開始理解網上一些其他教程在翻譯官方說明時對于原文(What happens in this case is that the constraints act like opposite forces pulling the widget apart equally)的譯文的含義了。
1.3.2
假設我們有如下代碼:
我們將A的左邊與父容器的左邊設定約束,然后將A的右邊與B的左邊設定約束,此時A既約束于父容器也約束于B容器,然后將B的右邊與A的左邊相約束,然后設置A的左右margin為20dip,設置B在4個方向上的margin也為20dp,運行結果為:
我們發現A與B之間的位置關系是正確的,B控件只有左間距,這也是正確的,符合我們上面得出的間距只在約束方向上有效的結論,但是A控件的左右間距都失效了,感覺有點奇怪有問題,但此處其實我們是創建了一種特殊的約束布局結構--鏈(chain),對于鏈的定義、介紹以及為何margin感覺失效的問題我們下面會詳細說明,此處先有個印象。但如果我們此時去掉B相對于A的約束,僅依靠A相對于B的約束能否保持這個位置不變呢,為了方便觀察結果,我們將A與B在布局文件中書寫的順序互換:
運行結果為:
可見,A控件在嘗試以一個詭異的位置“居中”,并且B的位置并沒有依照所想能在A的右邊,究其原理,必然是約束布局內部的計算方法導致其在此類情況下計算出可居中的寬度及位置并嘗試居中,具體的算法還有待翻看源碼后總結,此處暫時存疑并有待補充,接下來我們修改間距大小:
觀察結果:
我們可以確認一點,在這種情況下,對A布局設置的margin是有效的,但前提是我們沒有將A與B相互約束。
2.約束對象可見度為GONE的情況:
約束布局中,我們可以通過設置:
layout_goneMarginX
屬性,來設置約束的控件對象可見度為GONE的情況下的margin數值
具體用法非常好理解,我們不再贅述。但值得注意的是,約束布局下被設置為GONE的元素依舊是布局的一部分,與其他布局的區別在于其尺寸變為了0,而不是脫離布局
3.約束布局內子控件的寬高設置
官方說明常用方式為:
a.設置具體的寬高數值
b.設置寬高為wrap_content
c.設置寬高為0dp,以代表MATCH_CONSTRAINT
對于c方式,約束布局中不能直接使用match_parent來設置控件的寬高,直接設置后IDE會自動轉換成0dp,這里我們有必要舉個例子與上面的居中例子做對比來說明0dp如何MATCH_CONSTRAINT
觀察結果:
C控件并沒有如我們所想match,我們修改C的屬性:
運行效果:
我們再思考一個例子,假設我們在此基礎上再加入一個控件D,我們希望此控件位于A的正下方,并且寬度與A相同,我們寫下了如下的代碼:
運行效果:
發現D在指定的兩條邊中間居中,符合我們上面的測試結果,現在我需要令D的寬與A一致,修改代碼:
運行結果:
可以發現,0dp指的是MATCH_CONSTRAINT,而不是match_parent,需重點理解
d.設置ratio屬性
通過使用:
app:layout_constraintDimensionRatio="X,A:B"
屬性,可以用比例的形式更靈活的指定控件的寬高,意思為系統會以最大的尺寸為依據,加之寬高比,計算得出0dp所對應的數值,其中X指的是要約束的邊,取值為W或H 且可以不填,A與B為具體的比例數值,代表寬:高,我們繼續修改控件D的屬性以具體查看:
我們設置D的寬為0dp,以告知系統寬需要計算得出,高設置為wrap_content,然后設置ratio的值為16:9,運行結果:
控件D以最大尺寸的邊 高為基準,加之以寬高比16:9共同計算出寬度,我們繼續修改代碼:
控件D的寬高均置為0dp,代表均需計算得出,然后指定寬高比為16:9,此時系統會設置最大尺寸以滿足所有約束并保證寬高比:
在此基礎上,我們為ratio添加要約束的邊,
運行效果:
此時我們的意思是寬作為最大尺寸,加之寬高比16:9,計算出的高效果與不添加一致,若我們改為約束寬:
運行效果:
可以看出,當我們選擇約束寬時,意思變為寬作為最大尺寸,加之高寬比為16:9,計算出的高會比寬要大
既然如此,那就說明此種情況下寬高的大小是互為影響的,我們繼續修改代碼:
我們將D依賴約束的控件A的寬增加至80dp,觀察效果:
D的寬隨著A的增大而增大,這同時也會導致D的高隨之增大,這也是實際應用中需要注意的一點
4.鏈(chain)
鏈是一種特殊的約束布局結構,有點像數據結構中的雙向鏈表,其定義就是多個控件相互約束而形成的結構,并擁有頭(head)的概念:
鏈擁有4種風格(style):
a.CHAIN_SPREAD:默認,鏈會正常的分離開(spread out)
b.CHAIN_SPREAD_INSIDE:與默認風格類似,但此種模式下的鏈頭及鏈尾不會分離開
c.CHAIN_PACKED:此模式下的控件會被打包(packed)在一起,可以理解為擠在一起而不是分離開,此時指定bias屬性可以改變默認的偏移量,類似我們在居中小節中介紹的那樣
d.Weighted chain:權重鏈,在默認風格的情況下,如果某一個控件使用0dp設置寬高,即被設置為MATCH_CONSTRAINT,則叫做權重鏈(*網絡上有其他教程將其譯作加權鏈,這是非常明顯的直接套用百度翻譯的結果,按照官方定義的理解,此模式更像我們在LinearLayout中使用的權重(weight)屬性,所以我們依此思路將其叫做權重鏈更符合原意)。
還記得我們在居中小節測試代碼現象時,無意中寫出了這種互相約束的鏈結構,并在設置鏈頭的margin時發現是無效的,對此官方給出了說明:
在連接處設定的margin會被計算在內,而對于spread(默認)模式下的鏈,設定的margin會被扣除(If margins are specified on connections, they will be taken in account. In the case of spread chains, margins will be deducted from the allocated space)
按照說明,CHAIN_SPREAD模式下對鏈頭設置的margin是無效的,那其他模式應該是有效的,我們驗證一下:
上面代碼我們通過指定控件A和C的寬為0dp(MATCH_CONSTRAINT)的形式創建了一組權重鏈,按照官方說明,非默認鏈的情況下鏈頭的margin應該是有效的,觀察代碼結果:
結果確是如我們所想,控件A由于只在左右兩邊設置了約束,所以設置的20dp只生效于左右兩邊。
最后,對于其他模式我們沒有太多需要說明,但對于權重鏈來講,當多個控件被指定為0dp即MATCH_CONSTRAINT后,默認它們將用可用空間平分彼此,但你同樣可以使用layout_constraintHorizontal_weight或layout_constraintVertical_weight屬性來指定它們的權重,就像我們在LinearLayout中做的那樣,權重規則也與LinearLayout中的weight屬性相同,具體效果我們就不再贅述了。
5.參照線(Guideline)
Guideline非常像我們PS中經常使用的標尺,作用也非常相似,同樣可以用來幫助我們定位控件 但更強大,我們舉個很簡單的例子,如果我現在約束布局中只有一個控件,那它會因只有父布局可以約束而無法布局在屏幕中的位置,此時我們便可以設置Guideline并令控件相對于其做約束來控制控件的位置,Guideline原理上是一個被設置成View.GONE且寬或高為0的控件,所以你不會在頁面上真正的看到它,當然Guideline的作用可不僅僅這么簡單,利用Guideline可以讓我們的布局工作變得更加靈活及高效,我們先嘗試一下簡單的例子了解它,另外的章節我們會結合實際項目去掌握它。
我們可以通過
android:orientation="X"
屬性來在指定方向創建一條Guideline,X可指定為vertical及horizontal,分別對應橫向及豎向的Guideline,再結合
app:layout_constraintGuide_X
屬性來指定Guideline來的位置,X可指定為begin、end及percent,根據Guideline方向的不同分別代表從上(左)、下(右)及百分比,屬性的值代表具體的位置數值:
我們創建了2條Guideline,一條橫向一條豎向,并設置位置為100dp,然后令控件A約束于它們,觀察效果:
可以看到,控件A顯示在了我們期望的位置
結語:終于,我們深入學習完了有關于ConstraintLayout常用的屬性,也調試了這些屬性在各種情況下的效果,ConstraintLayout的作用并不僅僅是簡單融合并代替線性布局與相對布局,更是一種全新的布局思想,這種思想甚至將影響我們在頁面動畫動效上的設計,在下一篇文章中,我們將結合實例去嘗試使用代碼來動態構建ConstraintLayout,并接觸ConstraintLayout真正有用有趣的用法。