在使用Android設(shè)計支持庫(Android Design Support Library)時,很難避開CoordinatorLayout:設(shè)計庫中有很多視圖都需要CoordinatorLayout的支持草巡。為什么呢寥袭?實際上CoordinatorLayout本身所做的事情并不多他嫡,要是在標(biāo)準(zhǔn)框架視圖中使用它,結(jié)果也就跟普通的FrameLayout差不多。那么奇跡來自何處呢至非?完全是由于CoordinatorLayout.Behaviors的存在。只要將Behavior綁定到CoordinatorLayout的直接子元素上糠聪,就能對觸摸事件(touch events)荒椭、window insets、measurement舰蟆、layout以及嵌套滾動(nested scrolling)等動作進(jìn)行攔截趣惠。Design Library的大多功能都是借助Behavior的大量運用來實現(xiàn)的。
一.創(chuàng)建Behavior
創(chuàng)建behavior非常簡單:使用extend Behavior就可以了身害。
public class FancyBehavior<V extends View> extends CoordinatorLayout.Behavior<V> {
/** * Default constructor for instantiating a FancyBehavior in code. */
public FancyBehavior() { }
/** * Default constructor for inflating a FancyBehavior from layout.
* * @param context The {@link Context}.
* @param attrs The {@link AttributeSet}.
*/
public FancyBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
// Extract any custom attributes out
// preferably prefixed with behavior_ to denote they
// belong to a behavior
}
}
注意: 這里綁定了泛型類型味悄,也就是說,可以將FancyBehavior綁定到任意視圖類上塌鸯。不過侍瑟,如果只想將Behavior綁定到特定種類的視圖上,就可以用這段代碼:
public class FancyFrameLayoutBehavior extends CoordinatorLayout.Behavior<FancyFrameLayout>
這樣一來丙猬,當(dāng)從視圖收到方法調(diào)用時涨颜,就無需再費神將大量參數(shù)轉(zhuǎn)到正確的子類中了费韭,簡單又便捷。
使用Behavior.setTag()/Behavior.getTag() 可以保存臨時數(shù)據(jù)咐低, 使用onSaveInstanceState()/onRestoreInstanceState()還可以保存Behavior相關(guān)的實例狀態(tài)揽思。雖然筆者建議要保證Behavior盡可能輕量級,不過這些方法可以讓Behavior更具狀態(tài)性见擦。
二.關(guān)聯(lián)Behavior
當(dāng)然钉汗,Behavior無法獨立完成工作,必須與實際調(diào)用的CoordinatorLayout子視圖相綁定鲤屡。具體有三種方式:通過代碼綁定损痰、在XML中綁定或者通過注釋實現(xiàn)自動綁定。
1.通過代碼綁定Behavior
如果將Behavior當(dāng)作綁定到CoordinatorLayout中每個視圖的附加數(shù)據(jù)酒来,那么發(fā)現(xiàn)Behavior實際上是存儲在各個視圖的LayoutParams中也就不足為奇了(之前有關(guān)于布局的博文)卢未。也是因此,Behavior需要綁定到CoordinatorLayout的直接子項中堰汉,因為只有那些子項會包含LayoutParams的特定Behavior子類辽社。
FancyBehavior fancyBehavior = new FancyBehavior();
CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) yourView.getLayoutParams();
params.setBehavior(fancyBehavior);
2.在XML中綁定Behavior
當(dāng)然,每次都用代碼綁定的話總是有些麻煩翘鸭,正如大多自定義的LayoutParams一樣滴铅,完成這件工作也有相應(yīng)的layout_ 屬性,這里是layout_behavior屬性:
<FrameLayout
android:layout_height=”wrap_content”
android:layout_width=”match_parent”
app:layout_behavior=”.FancyBehavior” />
與代碼綁定不同就乓,這里調(diào)用的總是FancyBehavior(Context context, AttributeSet attrs) 構(gòu)造函數(shù)汉匙。此外還能聲明任何自定義屬性,并將其從XML AttributeSet中提取出來生蚁,如果想要賦予開發(fā)者通過XML自定義Behavior的功能噩翠,這一點非常重要。
注意: 與父類負(fù)責(zé)解析與詮釋的Layout_屬性的命名規(guī)則相類似邦投,在Behavior中我們使用behavior_作為屬性前綴伤锚。
3.自動綁定Behavior
如果構(gòu)建了需要自定義Behavior的自定義視圖(就像Design Library中很多組件中那樣),也許你會想要默認(rèn)綁定某個behavior志衣,而無需每次手動在代碼中或XML中指定见芹。為了達(dá)到這個目的,只需在自定義視圖頂層添加簡單的注釋:
@CoordinatorLayout.DefaultBehavior(FancyFrameLayoutBehavior.class)
public class FancyFrameLayout extends FrameLayout {}
這樣蠢涝,默認(rèn)的構(gòu)造函數(shù)就會調(diào)用Behavior,與使用代碼綁定非常類似阅懦。注意:目前任何layout_behavior代表的屬性都會重寫DefaultBehavior和二。
三.攔截觸摸事件
一旦將所有behavior設(shè)置完畢,就可以準(zhǔn)備實際開工了耳胎。Behavior能做的事情之一包括攔截觸摸事件惯吕。
不用CoordinatorLayout時惕它,一般會使用各個ViewGroup的子類,Managing Touch Events training一文有提到過這個問題废登。不過有了CoordinatorLayout淹魄,通過Behavior的onInterceptTouchEvent(),將調(diào)用傳遞給它的onInterceptTouchEvent()堡距,讓Behavior獲得攔截觸摸事件的機(jī)會甲锡。通過返回為true,那么Behavior會通過onTouchEvent()接收后續(xù)的所有觸摸事件羽戒,而且無需視圖了解后續(xù)情況缤沦。SwipeDismissBehavior就是通過這樣的方式在視圖中執(zhí)行任務(wù)的。
不過更嚴(yán)重的觸摸攔截就是攔截任何交互易稠,只要在blocksInteractionBelow()中返回true就會出現(xiàn)這樣的情況缸废。當(dāng)然,在互動被攔截時也許你會希望有些視覺信號提示(以免使用者以為應(yīng)用完全不能用了)——這就是為什么blocksInteractionBelow()的默認(rèn)功能實際上依賴于getScrimOpacity()值——返回非零值會為視圖提供一層顏色遮罩(用getScrimColor()來確定顏色驶社,默認(rèn)為黑)企量,并立即禁用所有的觸摸互動。非常方便亡电。
四.攔截window insets
假設(shè)本文讀者已經(jīng)看過Why would I want to fitsSystemWindows一文届巩, 在該文中我們就fitsSystemWindows的實際作用做了深入探討,不過可歸結(jié)為:window insets需要避免在系統(tǒng)窗口(比如狀態(tài)欄和導(dǎo)航欄)之下出現(xiàn)逊抡。這里Behavior也能發(fā)揮作用:如果視圖為fitsSystemWindows=“true”姆泻,則onApplyWindowInsets()會調(diào)用綁定Behavior,且優(yōu)先級高于視圖自身冒嫡。
注意: 大多情況下拇勃,如果Behavior沒有消耗掉整個window insets,則應(yīng)當(dāng)通過ViewCompat.dispatchApplyWindowInsets() 來傳遞這個insets孝凌,以確保視圖的任何子項有機(jī)會看到這個WindowInsets方咆。
五.攔截Measurement和Layout
Measurement和layout是Android繪制視圖的關(guān)鍵組件,因此Behavior只有在onMeasureChild()和onLayoutChild()回調(diào)前攔截父視圖的measurement和layout蟀架,才能達(dá)到預(yù)計的效果瓣赂。
例如:我們采用泛型ViewGroup并為其添加一個maxWidth:
編寫適用所有項目的通用Behavior非常有用,不過切記:盡量考慮在應(yīng)用內(nèi)使用behavior的辦法片拍,這樣會讓應(yīng)用更為簡單煌集。(并非所有Behavior都應(yīng)當(dāng)是泛型的!)
六.理解視圖間的依賴
上述所有功能都僅需要單個視圖便可實現(xiàn)捌省。不過Behavior的強(qiáng)大之處源自構(gòu)建視圖間的依賴苫纤,也就是說:當(dāng)另一個視圖改變時,你的Behavior會獲得回調(diào),根據(jù)外部情況來變更自身功能卷拘。
Behavior在兩種情況下會成為視圖的依賴:一種是將Behavior相應(yīng)的視圖錨定在另一個視圖上時(隱性依賴)喊废,還有一種是在layoutDependsOn()中明確返回true時。
在視圖中使用CoordinatorLayout的layout_anchor屬性栗弟,就能起到錨定的作用污筷。與layout_anchorGravity屬性一同使用,就能將兩個視圖一并有效地固定在某個位置上乍赫。例如:可以將FloatingActionButton錨定到AppBarLayout上瓣蛀,而在AppBarLayout滾動出屏幕時,F(xiàn)loatingActionButton.Behavior就會通過隱性依賴將自身隱藏起來耿焊。
無論哪種情況揪惦,當(dāng)依賴視圖被移除時,Behavior會獲得onDependentViewRemoved()的回調(diào)罗侯;而只要依賴視圖出現(xiàn)變更器腋,Behavior就會獲得onDependentViewChanged()的回調(diào)(即調(diào)整大小或自身位置)。
將視圖固定在一起的能力正是Design Library實現(xiàn)諸多炫酷功能的辦法——比如FloatingActionButton與Snackbar之間的互動钩杰。FAB的Behavior依賴于添加到CoordinatorLayout上的Snackbar實例纫塌,再通過onDependentViewChanged()回調(diào)將FAB向上移動,避免遮住Snackbar讲弄。
注意: 在添加依賴時措左,視圖總是會在依賴視圖布局后進(jìn)行布局,無視子項次序避除。
七.嵌套滾動
說到嵌套滾動怎披,有詳細(xì)介紹它的相關(guān)文章,在本文中筆者只做粗淺概述瓶摆。需要牢記這幾件事:
無需在嵌套滾動視圖中聲明依賴凉逛,因為CoordinatorLayout的每個子項都有可能接收到嵌套滾動事件。
嵌套滾動不僅可以在CoordinatorLayout的直接子項中發(fā)起群井,也能在任何子視圖(比如CoordinatorLayout的子項的子項的子項中)發(fā)起状飞。
雖然我們稱之為嵌套滾動,不過實際上包括滾動(按照滾動做1:1的位移)與滑動(flinging)兩種動作书斜。
因此诬辈,通過onStartNestedScroll()來發(fā)起感興趣的嵌套滾動事件吧。收到滾動軸(例如橫向或縱向——使它容易忽略在特定方向上的滾動)后荐吉,必須在該方向上返回true焙糟,以獲得隨后的滾動事件。
在向onStartNestedScroll()返回true之后样屠,嵌套滾動分兩步運行:
onNestedPreScroll()在滾動視圖獲得滾動事件前運行酬荞,允許相應(yīng)Behavior消耗一部分或所有的滾動事件(最后消耗的int[]是一個“外部”參數(shù)搓劫,在其中指明消耗掉的滾動)。
滾動視圖在滾動后會調(diào)用onNestedScroll()混巧,可以知道滾動了多少view,未消耗掉的(overscroll)數(shù)量又有多少勤揩。
還有類似滑動操作(盡管pre-fling調(diào)用必須要么消耗掉所有的滑動咧党,要么不消耗滑動——沒有部分消耗的選項)。
在嵌套滾動(或滑動)停止后陨亡,就能獲得onStopNestedScroll()的調(diào)用傍衡。這表示滾動結(jié)束:在下一個滾動開始前,等待重新調(diào)用onStartNestedScroll()负蠕。
舉個例子:如果想要在向下滾動時隱藏FloatingActionButton蛙埂,并在向上滾動時顯示它,只用重寫onStartNestedScroll() 和onNestedScroll()遮糖,就像在這個ScrollAwareFABBehavior中看到的那樣绣的。