什么是CoordinatorLayout
Google在 android.support.design 包中新增的 CoordinatorLayout 布局, 可以簡(jiǎn)單理解為一個(gè)升級(jí)版本的 FrameLayout .
基本用法例如:
<android.support.design.widget.CoordinatorLayout>
<FrameLayout android:id="@+id/contentFrame" />
<android.support.design.widget.FloatingActionButton
app:layout_anchor="@id/contentFrame"
app:layout_anchorGravity="bottom|right" />
</android.support.design.widget.CoordinatorLayout>
這里將其他的屬性去掉了, 便于閱讀. 可以看出這里的關(guān)鍵在于 layout_anchor 和 layout_anchorGravity 兩個(gè)參數(shù), 意思也很簡(jiǎn)單:
就是這個(gè)View以指定的 anchor 作為參照物來(lái)定位, anchorGravity 設(shè)置為 bottom|right 則表示將FloatingActionButton放置于參照物(FrameLayout)的右下角.
互相依賴的Child
目前看來(lái)其用法和 FrameLayout 挺像的, 但其實(shí)它要比這要強(qiáng)大很多.
這里首先提出一個(gè)概念, CoordinatorLayout 其實(shí)是將其下的所有子View都抽象成:
互相依賴(depends)的關(guān)系.
因此某個(gè)view可以基于另一個(gè)view來(lái)定位, 但這只是冰山一角, 這樣抽象的好處更強(qiáng)大的地方在于:
每一個(gè)view的所有屬性, 坐標(biāo), 樣式, 狀態(tài)等一切都可以依賴于另一個(gè)view, 因此使得parentView和所有childView之間都可以互相聯(lián)動(dòng)起來(lái).
想象一下, 不僅僅是定位, 所有可以設(shè)置在View上面的屬性都可以依賴于另一個(gè)view的變化而變化, 某一個(gè)View可以跟隨另一個(gè)View一起滾動(dòng), 某一個(gè)View可以跟隨另一個(gè)View的狀態(tài)改變而改變, 是不是瞬間覺(jué)得很強(qiáng)大了.
實(shí)現(xiàn)滑動(dòng)列表自動(dòng)隱藏標(biāo)題欄(沉浸式效果)
這里先舉一個(gè)很常見(jiàn)的例子來(lái)說(shuō)明如何讓一個(gè)View跟隨另一個(gè)View滑動(dòng)而變化.
最著名的例子應(yīng)該就是向下滑動(dòng)列表的時(shí)候, 標(biāo)題欄可以自動(dòng)隱藏掉, 而向上滑動(dòng)時(shí)標(biāo)題欄又自動(dòng)顯示出來(lái), 即大家所說(shuō)的沉浸式閱讀體驗(yàn).
CoordinatorLayout 的 Behavior 是它更為強(qiáng)大的地方, 它使得CoordinatorLayou的若干個(gè)ChildView之間產(chǎn)生交互, 例如滑動(dòng)某個(gè)子View, 另一個(gè)子View跟著滑動(dòng)/隱藏.
這里需要 AppBarLayout 來(lái)配合實(shí)現(xiàn)該效果, 如下:
<android.support.design.widget.CoordinatorLayout>
<android.support.design.widget.AppBarLayout>
<android.support.v7.widget.Toolbar
app:layout_scrollFlags="scroll|enterAlways" />
</android.support.design.widget.AppBarLayout>
<RecyclerView
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</android.support.design.widget.CoordinatorLayout>
其中使用 RecyclerView 和 AppBarLayout 作為 CoordinatorLayout 的子View. 因此三者的行為是可以影響的, 當(dāng) RecyclerView 向下滑動(dòng)的時(shí)候, AppBarLayout 可以跟著向上擠出屏幕外, 使得列表可以全屏展示(即沉浸式), 而向下滑動(dòng)的時(shí)候, 標(biāo)題欄又跟著滑動(dòng)回來(lái).
layout_behavior 屬性定義了這個(gè)View如何和其他View互相交互的行為, 其值填寫的是一個(gè)class的名字(全稱帶包名), 例如這里例子的值其實(shí)為:
android.support.design.widget.AppBarLayout$ScrollingViewBehavior
這個(gè)值指定的類必須是 CoordinatorLayout.Behavior<V> 的子類, 我們也可以自定義一個(gè)該類繼承于它, 以此來(lái)寫自己想要的交互效果. 這個(gè)我們將在后面詳情講如何繼承該類.
需要注意的是 layout_behavior 是CoordinatorLayout的layoutParams屬性, 因此只有它的直接子View才能設(shè)置, 而在我們這個(gè)例子里, 可以看見(jiàn)RecyclerView是設(shè)置了該屬性的, 但AppBarLayout是沒(méi)有在xml里面設(shè)置的, 這很奇怪, 但其實(shí)是因?yàn)檫@個(gè)屬性不僅僅可以在layout里面設(shè)置, 還可以在代碼里通過(guò)注解來(lái)設(shè)置, AppBarLayout就是在java代碼里設(shè)置的. 如:
@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
public class AppBarLayout extends LinearLayout {
// ...
}
這個(gè) @CoordinatorLayout.DefaultBehavior(value) 注解等同于在xml設(shè)置 layout_behavior=value 屬性.
另外關(guān)于 AppBarLayout 的用法則超出了本文范圍, 這里只需要將其理解為CoordinatorLayout的子View, 并且設(shè)置了layout_behavior, 因此可以和CoordinatorLayout的其他view聯(lián)動(dòng).
到此你對(duì)CoordinatorLayout.Behavior應(yīng)該有了一個(gè)初步的認(rèn)識(shí), 下面我們通過(guò)自定義一個(gè)Behavior來(lái)加深了解.
自定義CoordinatorLayout.Behavior
這里我們要實(shí)現(xiàn)的效果是讓一個(gè)浮動(dòng)按鈕在列表向下滑動(dòng)的時(shí)候自動(dòng)隱藏, 而向下滑動(dòng)的時(shí)候自動(dòng)顯示回來(lái).
首先我們來(lái)看布局的結(jié)構(gòu):
<android.support.design.widget.CoordinatorLayout>
<RecyclerView android:id="@+id/list" />
<android.support.design.widget.FloatingActionButton
app:layout_anchor="@id/list"
app:layout_anchorGravity="bottom|right|end"
app:layout_behavior="com.test.FloatingActionButtonAutoHideBehavider" />
</android.support.design.widget.CoordinatorLayout>
我們有一個(gè) RecyclerView 列表可以上下滑動(dòng), 另外有一個(gè) FloatingActionButton 浮動(dòng)按鈕.
然后我們來(lái)看浮動(dòng)按鈕上面定義的 layout_behavior 類, 這個(gè)自定義的 Behavior 類繼承自 CoordinatorLayout.Behavior<V extends View> .
其中的泛型 <V> 為: 目標(biāo)View的類型, 即需要設(shè)置 app:layout_behavior 的View類型. 如果在某個(gè)View上設(shè)置了某個(gè)Behavior, 但該Behavior的泛型卻不是該View的類型, 是會(huì)報(bào)錯(cuò)的.
public class FloatingActionButtonAutoHideBehavider extends CoordinatorLayout.Behavior<FloatingActionButton> {
public FloatingActionButtonAutoHideBehavider(Context context, AttributeSet attrs) {
super(context, attrs);
}
}
這里需要注意的是在繼承Behavior類的時(shí)候必須實(shí)現(xiàn) Behavior(Context context, AttributeSet attrs) 這個(gè)構(gòu)造器, 否則會(huì)報(bào)錯(cuò). 因?yàn)?CoordinatorLayout 里面是通過(guò)反射來(lái)實(shí)例化 Behavior 對(duì)象的, 而反射的時(shí)候使用了這個(gè)帶兩個(gè)參數(shù)的構(gòu)造器來(lái)實(shí)例化的.
然后我們來(lái)重載兩個(gè)方法來(lái)實(shí)現(xiàn)想要的效果:
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,
FloatingActionButton child, View directTargetChild,
View target, int nestedScrollAxes) {
return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL;
}
這里表示我們只相應(yīng)Y軸的嵌套滑動(dòng)( nestedScroll ), 這里要講清楚這個(gè)方法就必須先講清楚 support.v4 包里面新加的嵌套滑動(dòng)概念, 而這個(gè)概念可以講一天, 因此這里只是簡(jiǎn)單講講和 CoordinatorLayout 有關(guān)的內(nèi)容.
首先我們來(lái)看v4包的里面嵌套滑動(dòng), 以及支持嵌套滑動(dòng)的幾個(gè)類, 因?yàn)橐胱屢粋€(gè)View跟著另一個(gè)View滑動(dòng)而變化, 那么這個(gè)可以滑動(dòng)的View就必須實(shí)現(xiàn)了 NestedScrollingChild 接口, 這樣CoordinatorLayout才能收到這個(gè)子類的滑動(dòng)事件.
我們只需要知道CoordinatorLayout是實(shí)現(xiàn)了 NestedScrollingParent 接口的, 而要監(jiān)聽(tīng)其子View的滑動(dòng)事件, 那么該子View就必須實(shí)現(xiàn)了 NestedScrollingChild 接口, 具體實(shí)現(xiàn)了該接口子類如下:
-
android.support.v4.view.NestedScrollingParent- CoordinatorLayout
-
android.support.v4.view.NestedScrollingChild- HorizontalGridView
- NestedScrollView
- RecyclerView
- SwipeRefreshLayout
- VerticalGridView
由此可見(jiàn), ListView 和 ScrollView 是不支持的, 只能換成 RecyclerView 和 NestedScrollView 才行.
關(guān)于嵌套滑動(dòng)的概念理解到這里就夠了, 更多內(nèi)容可以查詢相關(guān)資料.
接下來(lái)繼續(xù)看另一個(gè)重載的方法:
@Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child, View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed) {
if (dyConsumed > 0) {
// 手勢(shì)從下向上滑動(dòng)(列表往下滾動(dòng)), 隱藏
CoordinatorLayout.LayoutParams layoutParams =
(CoordinatorLayout.LayoutParams) child.getLayoutParams();
int fab_bottomMargin = layoutParams.bottomMargin;
setAnimateTranslationY(child, child.getHeight() + fab_bottomMargin);
} else if (dyConsumed < 0) {
// 手勢(shì)從上向下滑動(dòng)(列表往上滾動(dòng)), 顯示
setAnimateTranslationY(child, 0);
}
}
private void setAnimateTranslationY(View view, int y) {
view.animate().translationY(y).setInterpolator(new LinearInterpolator()).start();
}
這里監(jiān)聽(tīng) RecyclerView 的滑動(dòng), 當(dāng)它向下滑動(dòng)的時(shí)候, 隱藏掉浮動(dòng)按鈕, 反之顯示.
其中 onNestedScroll 有幾個(gè)參數(shù), 依次是:
- CoordinatorLayout, 即parentView,
- child, 即Behavior對(duì)應(yīng)的這個(gè)ChildView, 這里是
FloatingActionButton - target, 即觸發(fā)嵌套滑動(dòng)的子View, 這里是
RecyclerView - dxConsumed, 橫向滑動(dòng)距離
- dyConsumed, 縱向滑動(dòng)距離
其實(shí)后面的幾個(gè)參數(shù)都是和 NestedScrollingParent 接口下的 public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) 一模一樣的, 感興趣的可以深入了解一下嵌套滑動(dòng). (感覺(jué)又給自己挖了一個(gè)坑...)
會(huì)用了就夠了嗎?
到此為止, 你應(yīng)該對(duì) CoordinatorLayout 有一定理解了, 簡(jiǎn)單的用應(yīng)該是沒(méi)有問(wèn)題了, 但一個(gè)新的控件放在我們面前的時(shí)候, 我們不應(yīng)該僅僅停留在學(xué)會(huì)怎么用一個(gè)控件, 而應(yīng)該深入到一個(gè)控件的源碼中去, 學(xué)習(xí)一個(gè)好的控件是怎么寫出來(lái)的, 這樣即可以加深對(duì)于該控件的理解, 也可以在自己寫自定義控件, 甚至設(shè)計(jì)別的模塊的時(shí)候都有很大幫助, 這就是android開(kāi)源的好處, 你不僅可以用它的代碼, 還可以學(xué)它的代碼.
而android后面新加的控件在設(shè)計(jì)上都非常值得學(xué)習(xí), 解耦得非常漂亮.
閱讀源碼, 深入了解
在 android.support.design.widget 包里面還有一些自帶的Behavior, 可以通過(guò)閱讀它們來(lái)學(xué)習(xí)更多關(guān)于 CoordinatorLayout 的概念, 如下:
- CoordinatorLayout.Behavior<V>
- BottomSheetBehavior
- SwipeDismissBehavior
- Snackbar.Behavior
- ViewOffsetBehavior
- HeaderScrollingViewBehavior
- HeaderBehavior
- FloatingActionButton.Behavior
CoordinatorLayout的事件分發(fā)
我們從 Behavior 的代碼開(kāi)始讀, 會(huì)首先看到下面兩個(gè)方法:
public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
return false;
}
public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
return false;
}
這兩個(gè)方法很熟悉了, 一個(gè)是 ViewGroup 的方法, 一個(gè)是 View 的方法, 概念上也是一樣的, 并且 CoordinatorLayout 也是在自己的 onInterceptTouchEvent 方法里面去調(diào)用其子view的Behavior的 onInterceptTouchEvent 方法, onTouchEvent 同理.
為什么要提供這兩個(gè)方法, Behavior 和 ViewGroup 的 onInterceptTouchEvent 有區(qū)別嗎?
這里先暫不表, 首先要贊一下這個(gè)地方的設(shè)計(jì).
我相信有經(jīng)驗(yàn)的同學(xué)一定使用過(guò) onInterceptTouchEvent 方法, 一般是需要阻攔滑動(dòng)事件的時(shí)候, 繼承某個(gè) ViewGroup , 然后重載這個(gè)方法, 返回 true 來(lái)實(shí)現(xiàn)攔截事件.
其實(shí)這種設(shè)計(jì)是很蛋疼的, 為了改變事件的分發(fā), 必須去繼承. 而 CoordinatorLayout 使用了一種更解耦的設(shè)計(jì). 它可以改變事件的分發(fā), 而不需去繼承某個(gè)View, 而只是給某個(gè)View指定一個(gè) Behavior 即可.
那么我們看看CoordinatorLayout是怎么實(shí)現(xiàn)事件的分發(fā)的.
在講CoordinatorLayout之前, 我們必須老生重彈一下, 講一個(gè) ViewGroup/View 的事件分派, 事件分派是有兩個(gè)過(guò)程的:
捕獲過(guò)程: 從根元素到子元素的依次調(diào)用 onInterceptTouchEvent , 看有沒(méi)人要攔截事件, 如果有人攔截了立即進(jìn)入冒泡過(guò)程, 否則一直傳遞到最末尾的元素再進(jìn)入冒泡過(guò)程.
冒泡過(guò)程: 從底層往上冒泡, 依次調(diào)用 onTouchEvent , 如果有人消耗了事件, 則不再繼續(xù)向上傳遞, 否則一直傳遞到根元素.
而默認(rèn)的 ViewGroup 是不會(huì)再捕獲任何事件的, 因此在處理手勢(shì)事件沖突的時(shí)候, 我們需要繼承外層的 ViewGroup 來(lái)在捕獲過(guò)程的時(shí)候攔截事件, 讓事件不再向子元素傳遞.
CoordinatorLayout 則不需要這樣, 它在自己的 onInterceptTouchEvent 方法里面去遍歷所有的子View, 調(diào)用它們的 Behavior.onInterceptTouchEvent() 方法, 如果有一個(gè)子View攔截了該事件, 則事件進(jìn)入冒泡過(guò)程.
而這樣的設(shè)計(jì)可以使得處理例如手勢(shì)的邏輯可以完全從具體的某個(gè)View解耦出來(lái), 例如你想要在某個(gè)View上實(shí)現(xiàn)向右滑動(dòng)消失的手勢(shì)操作, 那么你就必須繼承這個(gè)View, 然后重載其的事件分發(fā)回調(diào)方法. 但在 CoordinatorLayout 里面, 你只需要寫:
public class MySwipeDismissBehavior extends SwipeDismissBehavior<View> {
public MySwipeDismissBehavior(Context context, AttributeSet attrs) {
super();
}
}
其中 SwipeDismissBehavior 是 android.support.design.widget 自帶的一個(gè)手勢(shì)識(shí)別行為類.
然后將其作為 Behavior 設(shè)置在某個(gè)View上即可實(shí)現(xiàn)該手勢(shì)的效果:
<View
app:layout_behavior="com.test.MySwipeDismissBehavior" />
由此可以看出來(lái), 當(dāng)出現(xiàn)了另一個(gè)View也需要識(shí)別該手勢(shì)的時(shí)候, 只需要設(shè)置同樣的 Behavior 即可, 代碼復(fù)用率極高. 而當(dāng)這個(gè)View想要換一個(gè)手勢(shì)的時(shí)候, 也只需要替換一個(gè)新的 Behavior 即可. 可以看出來(lái)這樣的設(shè)計(jì)將具體的View和事件的分發(fā)邏輯徹底解耦, 并且實(shí)現(xiàn)了 "組合優(yōu)于繼承" 的思想. 有很多時(shí)候 is-a 和 has-a 是都說(shuō)得通的, 比如這個(gè)例子里可以理解成: "一個(gè)可以識(shí)別側(cè)滑手勢(shì)的View", 也可以理解成 "一個(gè)有識(shí)別側(cè)滑手勢(shì)功能的View", 但大部分時(shí)候使用 has-a 一定會(huì)優(yōu)于 is-a , 除非它們有非常強(qiáng)的繼承關(guān)系, 才應(yīng)該用 is-a , 否則都應(yīng)該優(yōu)先使用組合來(lái)代替繼承關(guān)系.
CoordinatorLayout子View的依賴關(guān)系
在最開(kāi)始我們說(shuō)過(guò) CoordinatorLayout 之所以這么強(qiáng)大, 就是因?yàn)樗鼘⑵渌械腸hildView都抽象成 互相依賴 的關(guān)系.
在 CoordinatorLayout.LayoutParams 中定義了一個(gè)View是否依賴( dependsOn ) 另一個(gè)View:
boolean dependsOn(CoordinatorLayout parent, View child, View dependency) {
return dependency == mAnchorDirectChild
|| (mBehavior != null && mBehavior.layoutDependsOn(parent, child, dependency));
}
由此我們可以看出, 如果一個(gè)View在layout的時(shí)候?qū)⒁粋€(gè)view作為了定位的參考物( 之前提到過(guò)的 app:layout_anchor 屬性, 那么這個(gè)View則視為依賴于參照物的View.
而另一種定義依賴關(guān)系的方法, 就是在 Behavior 中如下方法:
boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency);
其中child參數(shù)表示自己, dependency參數(shù)表示對(duì)照的View, 如果返回true則表示childView依賴于dependencyView.
而在CoordinatorLayout中, 它會(huì)遍歷所有的子View, 將每個(gè)子View和其他的子View都使用 layoutDependsOn 來(lái)比較一下, 確保所有互相依賴的子View都可以聯(lián)動(dòng)起來(lái).
例如我們之前提過(guò)的 AppBarLayout 里面的 AppBarLayout$ScrollingViewBehavior 就重載了該方法:
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
// We depend on any AppBarLayouts
return dependency instanceof AppBarLayout;
}
因此將這個(gè) Behavior 設(shè)置到任何View上面, 則該View在布局上則依賴于 AppBarLayout , 因此在布局的會(huì)將View放置到 AppBarLayout 的下面, 例如 AppBarLayout 高度為48dp, 那么這個(gè)View的offsetTopAndBottom就會(huì)被自動(dòng)設(shè)置為48dp, 使其剛好布局到 AppBarLayout 的下面, 并且讓這個(gè)View在AppBarLayout的高度變化的時(shí)候自動(dòng)跟隨, 因此可以實(shí)現(xiàn)自動(dòng)伸縮的頂部欄, 以及 parallax effect 的頭圖效果.
而 Behavior 的另一個(gè)方式是和 layoutDependsOn 方法相關(guān)聯(lián)的, 如果 layoutDependsOn 方法返回true則會(huì)調(diào)用這個(gè)方法:
boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency);
當(dāng)它依賴的View發(fā)現(xiàn)變化的時(shí)候, 則會(huì)回調(diào)這個(gè)方法, 那么使其可以和依賴的變化而變化.
例如 FloatingActionButton.Behavior 定義的:
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton child,
View dependency) {
if (dependency instanceof Snackbar.SnackbarLayout) {
updateFabTranslationForSnackbar(parent, child, dependency);
} else if (dependency instanceof AppBarLayout) {
// If we're depending on an AppBarLayout we will show/hide it automatically
// if the FAB is anchored to the AppBarLayout
updateFabVisibility(parent, (AppBarLayout) dependency, child);
}
return false;
}
大概想要實(shí)現(xiàn)的效果就是:
如果這個(gè)浮動(dòng)按鈕依賴于某個(gè)
Snackbar的時(shí)候, 而這個(gè)Snackbar顯示的出來(lái), 因?yàn)榭赡芎虵AB都是在底部出現(xiàn), 而出現(xiàn)互相遮蓋, 因此可以在SnackBar顯示出來(lái)的時(shí)候, 自動(dòng)將FAB按鈕向上移動(dòng)一點(diǎn), 使其不會(huì)遮蓋住, 而在SnackBar自動(dòng)隱藏的時(shí)候, 浮動(dòng)按鈕也還原到原來(lái)的位置.如果這個(gè)浮動(dòng)按鈕定位的時(shí)候依賴于
AppBarLayout, 那么當(dāng)其變化的時(shí)候自動(dòng)顯示或隱藏浮動(dòng)按鈕.
到此大家應(yīng)該就可以理解, 之前我說(shuō)過(guò)的CoordinatorLayout的中心思想就是關(guān)于抽象了子View之間的依賴關(guān)系.