在使用Android設計支持庫(Android Design Support Library)時,很難避開CoordinatorLayout:設計庫中有很多視圖都需要CoordinatorLayout的支持。為什么呢?實際上CoordinatorLayout本身所做的事情并不多,要是在標準框架視圖中使用它,結果也就跟普通的FrameLayout差不多。那么奇跡來自何處呢?完全是由于CoordinatorLayout.Behaviors的存在。只要將Behavior綁定到CoordinatorLayout的直接子元素上,就能對觸摸事件(touch events)、window insets、measurement、layout以及嵌套滾動(nested scrolling)等動作進行攔截。Design Library的大多功能都是借助Behavior的大量運用來實現的。
一.創建Behavior
創建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>
這樣一來,當從視圖收到方法調用時,就無需再費神將大量參數轉到正確的子類中了,簡單又便捷。
使用Behavior.setTag()/Behavior.getTag() 可以保存臨時數據, 使用onSaveInstanceState()/onRestoreInstanceState()還可以保存Behavior相關的實例狀態。雖然筆者建議要保證Behavior盡可能輕量級,不過這些方法可以讓Behavior更具狀態性。
二.關聯Behavior
當然,Behavior無法獨立完成工作,必須與實際調用的CoordinatorLayout子視圖相綁定。具體有三種方式:通過代碼綁定、在XML中綁定或者通過注釋實現自動綁定。
1.通過代碼綁定Behavior
如果將Behavior當作綁定到CoordinatorLayout中每個視圖的附加數據,那么發現Behavior實際上是存儲在各個視圖的LayoutParams中也就不足為奇了(之前有關于布局的博文)。也是因此,Behavior需要綁定到CoordinatorLayout的直接子項中,因為只有那些子項會包含LayoutParams的特定Behavior子類。
FancyBehavior fancyBehavior = new FancyBehavior();
CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) yourView.getLayoutParams();
params.setBehavior(fancyBehavior);
2.在XML中綁定Behavior
當然,每次都用代碼綁定的話總是有些麻煩,正如大多自定義的LayoutParams一樣,完成這件工作也有相應的layout_ 屬性,這里是layout_behavior屬性:
<FrameLayout
android:layout_height=”wrap_content”
android:layout_width=”match_parent”
app:layout_behavior=”.FancyBehavior” />
與代碼綁定不同,這里調用的總是FancyBehavior(Context context, AttributeSet attrs) 構造函數。此外還能聲明任何自定義屬性,并將其從XML AttributeSet中提取出來,如果想要賦予開發者通過XML自定義Behavior的功能,這一點非常重要。
注意: 與父類負責解析與詮釋的Layout_屬性的命名規則相類似,在Behavior中我們使用behavior_作為屬性前綴。
3.自動綁定Behavior
如果構建了需要自定義Behavior的自定義視圖(就像Design Library中很多組件中那樣),也許你會想要默認綁定某個behavior,而無需每次手動在代碼中或XML中指定。為了達到這個目的,只需在自定義視圖頂層添加簡單的注釋:
@CoordinatorLayout.DefaultBehavior(FancyFrameLayoutBehavior.class)
public class FancyFrameLayout extends FrameLayout {}
這樣,默認的構造函數就會調用Behavior,與使用代碼綁定非常類似。注意:目前任何layout_behavior代表的屬性都會重寫DefaultBehavior。
三.攔截觸摸事件
一旦將所有behavior設置完畢,就可以準備實際開工了。Behavior能做的事情之一包括攔截觸摸事件。
不用CoordinatorLayout時,一般會使用各個ViewGroup的子類,Managing Touch Events training一文有提到過這個問題。不過有了CoordinatorLayout,通過Behavior的onInterceptTouchEvent(),將調用傳遞給它的onInterceptTouchEvent(),讓Behavior獲得攔截觸摸事件的機會。通過返回為true,那么Behavior會通過onTouchEvent()接收后續的所有觸摸事件,而且無需視圖了解后續情況。SwipeDismissBehavior就是通過這樣的方式在視圖中執行任務的。
不過更嚴重的觸摸攔截就是攔截任何交互,只要在blocksInteractionBelow()中返回true就會出現這樣的情況。當然,在互動被攔截時也許你會希望有些視覺信號提示(以免使用者以為應用完全不能用了)——這就是為什么blocksInteractionBelow()的默認功能實際上依賴于getScrimOpacity()值——返回非零值會為視圖提供一層顏色遮罩(用getScrimColor()來確定顏色,默認為黑),并立即禁用所有的觸摸互動。非常方便。
四.攔截window insets
假設本文讀者已經看過Why would I want to fitsSystemWindows一文, 在該文中我們就fitsSystemWindows的實際作用做了深入探討,不過可歸結為:window insets需要避免在系統窗口(比如狀態欄和導航欄)之下出現。這里Behavior也能發揮作用:如果視圖為fitsSystemWindows=“true”,則onApplyWindowInsets()會調用綁定Behavior,且優先級高于視圖自身。
注意: 大多情況下,如果Behavior沒有消耗掉整個window insets,則應當通過ViewCompat.dispatchApplyWindowInsets() 來傳遞這個insets,以確保視圖的任何子項有機會看到這個WindowInsets。
五.攔截Measurement和Layout
Measurement和layout是Android繪制視圖的關鍵組件,因此Behavior只有在onMeasureChild()和onLayoutChild()回調前攔截父視圖的measurement和layout,才能達到預計的效果。
例如:我們采用泛型ViewGroup并為其添加一個maxWidth:
編寫適用所有項目的通用Behavior非常有用,不過切記:盡量考慮在應用內使用behavior的辦法,這樣會讓應用更為簡單。(并非所有Behavior都應當是泛型的!)
六.理解視圖間的依賴
上述所有功能都僅需要單個視圖便可實現。不過Behavior的強大之處源自構建視圖間的依賴,也就是說:當另一個視圖改變時,你的Behavior會獲得回調,根據外部情況來變更自身功能。
Behavior在兩種情況下會成為視圖的依賴:一種是將Behavior相應的視圖錨定在另一個視圖上時(隱性依賴),還有一種是在layoutDependsOn()中明確返回true時。
在視圖中使用CoordinatorLayout的layout_anchor屬性,就能起到錨定的作用。與layout_anchorGravity屬性一同使用,就能將兩個視圖一并有效地固定在某個位置上。例如:可以將FloatingActionButton錨定到AppBarLayout上,而在AppBarLayout滾動出屏幕時,FloatingActionButton.Behavior就會通過隱性依賴將自身隱藏起來。
無論哪種情況,當依賴視圖被移除時,Behavior會獲得onDependentViewRemoved()的回調;而只要依賴視圖出現變更,Behavior就會獲得onDependentViewChanged()的回調(即調整大小或自身位置)。
將視圖固定在一起的能力正是Design Library實現諸多炫酷功能的辦法——比如FloatingActionButton與Snackbar之間的互動。FAB的Behavior依賴于添加到CoordinatorLayout上的Snackbar實例,再通過onDependentViewChanged()回調將FAB向上移動,避免遮住Snackbar。
注意: 在添加依賴時,視圖總是會在依賴視圖布局后進行布局,無視子項次序。
七.嵌套滾動
說到嵌套滾動,有詳細介紹它的相關文章,在本文中筆者只做粗淺概述。需要牢記這幾件事:
無需在嵌套滾動視圖中聲明依賴,因為CoordinatorLayout的每個子項都有可能接收到嵌套滾動事件。
嵌套滾動不僅可以在CoordinatorLayout的直接子項中發起,也能在任何子視圖(比如CoordinatorLayout的子項的子項的子項中)發起。
雖然我們稱之為嵌套滾動,不過實際上包括滾動(按照滾動做1:1的位移)與滑動(flinging)兩種動作。
因此,通過onStartNestedScroll()來發起感興趣的嵌套滾動事件吧。收到滾動軸(例如橫向或縱向——使它容易忽略在特定方向上的滾動)后,必須在該方向上返回true,以獲得隨后的滾動事件。
在向onStartNestedScroll()返回true之后,嵌套滾動分兩步運行:
onNestedPreScroll()在滾動視圖獲得滾動事件前運行,允許相應Behavior消耗一部分或所有的滾動事件(最后消耗的int[]是一個“外部”參數,在其中指明消耗掉的滾動)。
滾動視圖在滾動后會調用onNestedScroll(),可以知道滾動了多少view,未消耗掉的(overscroll)數量又有多少。
還有類似滑動操作(盡管pre-fling調用必須要么消耗掉所有的滑動,要么不消耗滑動——沒有部分消耗的選項)。
在嵌套滾動(或滑動)停止后,就能獲得onStopNestedScroll()的調用。這表示滾動結束:在下一個滾動開始前,等待重新調用onStartNestedScroll()。
舉個例子:如果想要在向下滾動時隱藏FloatingActionButton,并在向上滾動時顯示它,只用重寫onStartNestedScroll() 和onNestedScroll(),就像在這個ScrollAwareFABBehavior中看到的那樣。