攔截一切的CoordinatorLayout Behavior
來自: http://www.jcodecraeer.com//a/anzhuokaifa/androidkaifa/2016/0224/3991.html
原文:Intercepting everything with CoordinatorLayout Behaviors。
如果沒有深入CoordinatorLayout?,你注定無法在探索Android Design Support Library的路上走多遠 - Design Library中的許多view都需要一個CoordinatorLayout。但是為什么呢?CoordinatorLayout本身并沒有做太多事情:和標準的framework視圖一起使用時,它就跟一個普通的FrameLayout差不多。那么它的神奇之處來自于哪里呢?答案就是CoordinatorLayout.Behavior。通過為CoordinatorLayout的直接子view設置一個Behavior,就可以攔截touch events, window insets, measurement, layout, 和 nested scrolling等動作。Design Library大量利用了Behaviors來實現你所看到的功能。
創建一個Behavior
創建一個behavior很簡單:繼承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 } }
注意這個類設置的是普通View,這意味著你可以把FancyBehavior設置給任何View類。但是,如果你只允許讓Behavior設置給一個特定類型的View,則需要這樣寫:
public class FancyFrameLayoutBehavior extends CoordinatorLayout.Behavior<FancyFrameLayout>
這可以省去把回調方法中收到的view參數轉換成正確類型的步驟-效率第一嘛。
可以使用Behavior.setTag()/Behavior.getTag() 來保存臨時數據,還可以使用onSaveInstanceState()/onRestoreInstanceState()來保存跟Behavior相關的實例的狀態。我建議讓Behaviors盡可能的輕,但是這些方法讓狀態化Behaviors成為可能。
設置Behavior
當然了,Behaviors并不會對自身做任何事情-它們需要被設置在一個CoordinatorLayout的子view上之后才會被實際調用。設置Behaviors主要有三種方式:程序中動態設置,xml布局文件設置和使用注解設置。
在程序中設置Behavior
當你認為Behavior是一個被設置在CoordinatorLayout每個子view上的附加數據時,你就不會對Behavior其實是保存在每個view的LayoutParam中感到奇怪了( 如果你已經閱讀了我們 關于布局的文章 )- 這也是為什么Behaviors需要聲明在CoordinatorLayout的直接子View上的原因,因為只有那些子View才存有CoordinatorLayout.LayoutParams(根據自己的理解翻譯的)。
FancyBehavior fancyBehavior = new FancyBehavior(); CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) yourView.getLayoutParams(); params.setBehavior(fancyBehavior);
這里你會發現我們使用的是默認的無參構造函數。但這并不是說你就不能使用任何參數 - 如果你想,代碼里面,萬事皆有可能。
在xml里設置Behavior
當然,每次都在代碼里面把所有事情做完會顯得有點亂。就跟多數自定義的LayoutParam一樣,這里也有相應的layout_ attribute 與之對應。那就是layout_behavior 屬性:
<FrameLayout android:layout_height=”wrap_content” android:layout_width=”match_parent” app:layout_behavior=”.FancyBehavior” />
這里與前面不同的是,被調用的構造函數總是FancyBehavior(Context context, AttributeSet attrs)。因此,你可以在xml屬性中聲明你想要的其他自定義屬性。如果你想讓開發者能夠通過xml自定義Behavior的功能,這點是很重要的。
注意:類似于由父類負責解析和解釋的layout_ 屬性命名規則,使用behavior_ prefix來指定被專門Behavior使用的某個屬性。
例子(譯者結合評論做的補充):
<FrameLayout android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior=".MaxWidthBehavior" app:behavior_maxWidth="400dp" />
自動設置一個Behavior
如果你正在創建一個需要一個自定義Behavior的自定義View(就如Design Library中的許多控件那樣),那么你很可能希望view默認就設置了那個Behavior,而不需要每次都通過xml或者代碼去手動指定。為此,你只需在自定義View類的最上面設置一個簡單的注解:
@CoordinatorLayout.DefaultBehavior(FancyFrameLayoutBehavior.class) public class FancyFrameLayout extends FrameLayout { }
你會發現你的Behavior會隨著默認的構造函數被調用,這非常類似于與通過程序設置Behavior。注意任何 layout_behavior屬性所代表的Behavior都會重寫 DefaultBehavior。
攔截 Touch Events
一旦你設置好了所有的behavior,你就該準備做點實際工作了。Behavior能做的事情之一就是攔截觸摸事件。
如果沒有CoordinatorLayout,我們通常會被牽涉進 ViewGroup的子類中,就像 Managing Touch Events training一文所討論的那樣。但是如果有了CoordinatorLayout,CoordinatorLayout就會把它onInterceptTouchEvent() 中的參數(主要是MotionEvent)和調用傳遞到Behavior的onInterceptTouchEvent(),讓你的Behavior有一次攔截觸摸事件的機會。如果返回true,你的Behavior則會通過onTouchEvent()?收到所有的后續觸摸事件-而View完全不知道發生了什么事情。這也是SwipeDismissBehavior 在view上的工作原理。
ps:我以前專門分析過SwipeDismissBehavior,和這段話基本一致。另外CoordinatorLayout其實是遍歷了一遍自己的直接子View,一個一個的調用子view中的Behavior,見:SwipeDismissBehavior用法及實現原理 。
不過還有一個更粗暴的觸摸攔截:攔截所有的交互。只需在 blocksInteractionBelow() 里返回true即可(我們這個視圖下的其他視圖將獲取不到任何Touch事件)。當然,你可能希望在交互被阻止的情況下能有一些視覺效果 - 這就是為什么blocksInteractionBelow()實際上默認依賴 getScrimOpacity()?的值 - 返回一個非零將在View之上繪制一層overlay顏色并且屏蔽所有的交互。
攔截Window Insets
假設你讀了Why would I want to fitsSystemWindows? blog。那里深入討論了fitsSystemWindows到底干什么的,但是它歸納為:window insets 需要避免在 system windows(比如status bar 和 navigation bar)的下面繪制。
Behaviors在這里也有攔截的機會 - 如果你的View是fitsSystemWindows=“true”的,那么任何依附著的Behavior都將得到onApplyWindowInsets()調用,且優先級高于View自身。
注意:如果你的Behavior并沒有消費掉整個 window insets,它應該通過 ViewCompat.dispatchApplyWindowInsets() 傳遞insets,以確保任何子view都能有機會看到這個WindowInsets。
攔截Measurement 和 layout
測量與布局(Measurement and layout)是 安卓如何繪制View的關鍵組成部分。因此對于能夠攔截一切的Behavior來說,它應該能在第一時間攔截測量和布局才是合情合理的。 這要通過onMeasureChild() 和 onLayoutChild() 回調來完成。
比如, 我們找來任意一個普通的ViewGroup,并向它添加一個maxWidth:
/* * Copyright 2015 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.behaviors; import android.content.Context; import android.content.res.TypedArray; import android.support.design.widget.CoordinatorLayout; import android.util.AttributeSet; import android.view.ViewGroup; import static android.view.View.MeasureSpec; /** * Behavior that imposes a maximum width on any ViewGroup. * * <p />Requires an attrs.xml of something like * * <pre> * <declare-styleable name="MaxWidthBehavior_Params"> * <attr name="behavior_maxWidth" format="dimension"/> * </declare-styleable> * </pre> */ public class MaxWidthBehavior<V extends ViewGroup> extends CoordinatorLayout.Behavior<V> { private int mMaxWidth; public MaxWidthBehavior(Context context, AttributeSet attrs) { super(context, attrs); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MaxWidthBehavior_Params); mMaxWidth = a.getDimensionPixelSize( R.styleable.MaxWidthBehavior_Params_behavior_maxWidth, 0); a.recycle(); } @Override public boolean onMeasureChild(CoordinatorLayout parent, V child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { if (mMaxWidth <= 0) { // No max width means this Behavior is a no-op return false; } int widthMode = MeasureSpec.getMode(parentWidthMeasureSpec); int width = MeasureSpec.getSize(parentWidthMeasureSpec); if (widthMode == MeasureSpec.UNSPECIFIED || width > mMaxWidth) { // Sorry to impose here, but max width is kind of a big deal width = mMaxWidth; widthMode = MeasureSpec.AT_MOST; parent.onMeasureChild(child, MeasureSpec.makeMeasureSpec(width, widthMode), widthUsed, parentHeightMeasureSpec, heightUsed); // We've measured the View, so CoordinatorLayout doesn't have to return true; } // Looks like the default measurement will work great return false; } }
寫一個通用的Behavior固然有用,但我們需要知道的是有時候如果你想讓你的app簡單一點的話完全可以把Behavior的相關功能寫在自定義View的內部,沒必要為了使用Behavior而是用它。
理解View之間的依賴
以上的所有功能都只需要一個View。但是Behaviors的強大之處在于在View之間建立依賴關系-當另一個View改變的時候,你的Behavior會得到一個callback,根據外部條件改變它的功能。
Behaviors依賴于View有兩種形式:當它的View錨定于另外一個View(一種隱式的依賴)或者,當你在layoutDependsOn()中明確的返回true。
錨定發生于你使用了CoordinatorLayout的layout_anchor 屬性之時。它和layout_anchorGravity 屬性結合,可以讓你有效的把兩個View捆綁在一起。比如,你可以把一個FloatingActionButton錨定在一個AppBarLayout上,那么如果AppBarLayout滾動出屏幕,FloatingActionButton.Behavior將使用隱式的依賴去隱藏FAB。
不管什么形式,當一個依賴的View被移除的時候你的Behavior會得到回調 onDependentViewRemoved() ,當依賴的View發生變化的時候(比如:調整大小或者重置自己的position),得到回調 onDependentViewChanged()
這個把View綁定在一起的能力正是Design Library那些酷炫功能的工作原理 -以FloatingActionButton與Snackbar之間的交互為例。FAB的 Behavior依賴于被添加到CoordinatorLayout的Snackbar,然后它使用onDependentViewChanged() callback來將FAB向上移動,以避免和Snackbar重疊。
注意:如果你添加了一個依賴,不管child的順序如何,你的View將總是在所依賴的View放置之后才會被放置。
嵌套滾動
啊哈,嵌套滾動。在這篇博客中,我只會點到為止。記住幾點:
-
你不需要在嵌套滾動的View上面定義依賴。CoordinatorLayout的每個child都有機會接收到嵌套滾動事件。
-
嵌套滾動不僅可以開始于CoordinatorLayout的直接child,還可以開始于任何child(比如CoordinatorLayout的child的child)。
-
雖然我叫它嵌套滾動,但其實它包含滾動(scrolling)和劃動(flinging)兩種。
那么讓我們使用onStartNestedScroll()來定義你所感興趣的嵌套滾動(方向)。你將收到滾動的軸(比如橫向或者縱向-讓它可以輕易的忽略某個方向上的滾動)并且為了接收那個方向上的后續滾動事件必須返回true。
當你在onStartNestedScroll()中返回了true之后,嵌套滾動進入兩個階段:
-
onNestedPreScroll() 會在scrolling View獲得滾動事件前調用,它允許你消費部分或者全部的事件信息。
-
onNestedScroll() 會在scrolling View做完滾動后調用,通過回調可以知道scrolling view滾動了多少和它沒有消耗的滾動事件。
同樣,fling操作也有與之相對應的方法(雖然e pre-fling callback 必須消費完或者完全不消費fling - 沒有消費部分的情況)。
當嵌套滾動(或者flinging)結束,你將得到一個onStopNestedScroll()回調。這標志著滾動的結束 - 迎接在下一個滾動之前的onStartNestedScroll() 調用。
比如,當向下滾動的時候隱藏FloatingActionButton,向上滾動的時候顯示FloatingActionButton- 這只牽涉到重寫onStartNestedScroll() 和 onNestedScroll(),就如在ScrollAwareFABBehavior中所看到的那樣。
這只是開始
Behavior每個單獨的部分都很有趣,當他們結合起來就會發生很神奇的事情。為了了解更多的高級behavior,我強烈鼓勵你去查看Design Library的源碼-Android SDK Search Chrome extension是我探索AOSP源碼時最喜歡的資源(雖然包含在 <android-sdk>/extras/android/m2repository中的源碼總是最新的)。
在了解Behavior能做哪些事情這點上打下了堅實的基礎后,讓我知道你們是如何使用它們創建更優秀的app的。
要了解更多,請參與在 Google+ post 上的討論并關注 Android Development Patterns Collection !