CoordinatorLayout的Behavior初學

在使用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:

CODE1
CODE2
CODE3

編寫適用所有項目的通用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中看到的那樣。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容