CoordinatorLayout
一、實現滑動 RecyclerView 實現 FAB 以及 Toolbar 的顯示和隱藏
(一)傳統實現思路:
- 監聽 RecyclerView 的滑動
- 根據滑動距離及狀態執行顯示和隱藏的動畫
(二)CoordinatorLayout 方式
二、CoordinatorLayout
CoordinatorLayout 繼承自 ViewGroup,通過協調并調度里面的子控件或者子布局來實現觸摸 (一般指滑動) 產生一些相關的動畫效果。可以通過設置 View 的 Behavior 屬性來實現觸摸的動畫調度。
1. CoordinatorLayout 中使用 SnackBar
可以解決 SnackBar 出現時遮擋 FloatingActionButton 的情況,其 Behavior 實現類是 FloatingActionButton.Behavior
2. AppBarLayout
AppBarLayout 繼承了 LinearLayout,并且是垂直方向,里面可以放多個 View, 在 CoordinatorLayout 中的 Scrolling View 滑動時,AppBarLayout 中的 View 可以實現多種隱藏、顯示效果。
Scrolling 是指:RecyclerView、NestedScrollView 等實現了 NestedScrollChild 接口的類
CoordinatorLayout 中,使用 AppBarLayout 包裹 Toolbar,再為 Scrolling View 設置 app:layout_behavior 屬性為 appbar_scrolling_view_behavior ,設置 Toolbar 的 layout_scrollFlags 屬性的值為 scroll,就實現了 RecyclerView 滑動時 Toolbar 自動的顯示隱藏的效果。為 layout_scrollFlags 參數設置不同的值就可以實現不同的效果。
scroll: 里面所有的子控件想要滑出屏幕的時候都需要設置這個 Flag,里面沒有設置這個 Flag 的 View 都將被固定在頂部,效果為:隱藏的時候,先整體向上滾動,直到 AppBarLayout 完全隱藏,再開始滾動 Scrolling View;顯示的時候,直到 Scrolling View 頂部完全出現后,再開始滾動整體直到 AppBarLayout 完全顯示。
enterAlways ,快速返回,設置這個屬性后,與 scroll 類似,只不過向下滾動先顯示子控件到完全,再滾動 Scrolling View了,需要與 scroll 配合使用
enterAlwaysCollapsed: 需要和 enterAlways 一起使用(scroll|enterAlways|enterAlwaysCollapsed),還需要為子控件設置 minHight 屬性,和 enterAlways 不一樣的是,不會顯示子控件到完全再滾動 Scrolling View,而是先滾動 子控件到最小高度,再滾動 Scrolling View,最后再滾動 AppBarLayout 到完全顯示。
exitUnitilCollapsed: 定義了子控件 消失的規則。發生向上滾動事件時,子控件向上滾動退出直至最小高度(minHeight),然后 Scrolling View 開始滾動。也就是,子控件不會完全退出屏幕
snap: 定義了是子控件滾動比例的一個吸附效果。也就是說,子控件不會存在局部顯示的情況,滾動子控件的部分高度,當我們松開手指時,子控件要么向上全部滾出屏幕,要么向下全部滾進屏幕,有點類似 ViewPager 的左右滑動。而向上還是向下滑動取決于顯示和隱藏部分的比例,顯示的多就會向下全部顯示,隱藏的多就會向上完全隱藏。
可以使用 CoordinatorLayout + View + Toolbar + TabLayout + ViewPager(內容可垂直滑動) 組合實現的 TabLayout 貼頂效果。
3. CollapsingToolbarLayout
可以實現 Toolbar 的折疊效果,使用 AppBarLayout 嵌套 CollapsingToolbarLayout,再使用 CollapsingToolbarLayout 嵌套 Toolbar,
注意:
AppBarLayout 需要設置固定的高度,實現折疊效果時要大于 Toolbar 高度
CollapsingToolbarLayout 設置 height 為占滿父布局
CollapsingToolbarLayout 為 AppBarLayout 的直接子控件,因為需要折疊 CollapsingToolbarLayout ,所以需要為 CollapsingToolbarLayout 設置 layout_scrollFlags 屬性設置為可隱藏
-
在 CollapsingToolbarLayout 中添加其他 View 放在 Toolbar 上面,并且為這個 View 和 Toolbar 設置 layout_collapseMode 屬性
- parallax: 效果為視差模式,折疊的時候會有視差效果。可以搭配 layout_collapseParallaxMultiplier 屬性,值的區間為 0 - 1,用來設置視差效果的明顯程度,為 1 時候的表現為 CollapsingToolbarLayout 其余部分被折疊后再折疊 toolbar 也就是無視差效果,0 的時候為先折疊 toolbar 再折疊其余部分也就是視差效果最明顯
- none:沒有任何效果,往上滑動的時候 Toolbar 會被首先退出去
- pin:固定模式,toolbar 設置該模式,在滑動時會有一個融合效果,融合完成后 toolbar 會固定在頂端
CollapsingToolbarLayout 可以設置 expandedTitleMargin 屬性控制展開時的文字 margin,collapsedTitleTextAppearance 屬性控制折疊時的文字樣式等
contentScrim 是一個顏色,內容部分的沉浸式效果,可以讓 Toolbar 和其他 View 有一個漸變的過渡效果,statusBarScrim 是為狀態欄設置顏色(5.0+ 才有效果)
還有其他很多屬性設置不同的效果
4. Behavior (CoordinatorLayout.Behavior) 需配合 CoordinatorLayout 使用
四、Behavior + CoordinatorLayout
Behavior 可以看作一個橋梁或者監聽者,實現包裹在 CoordinatorLayout 里面的所有子控件或者容器產生聯動效果
自定義 Behavior 的兩種效果,繼承 CoordinatorLayout.Behavior
為觀察者設置 Behavior,這樣被觀察者中 Behavior 監聽的狀態發生變化時,Behavior 中的對應方法會被回調。
1. 某個 View 需要監聽另一個 View 的狀態(比如:位置、大小、顯示狀態)
需要重寫方法:layoutDependsOn,onDependentViewChanged
- layoutDependsOn
用來決定需要監聽哪些控件或者容器的狀態,參數 parent 是 CoordinatorLayout, child 指定了當前 behavior 的需要監其他 View 的觀察者,dependency 是被觀察的 View;返回值是 dependency 是否是 child 需要監聽的 View (通過 id 或者 tag 等方式來判斷) 以及是否是觀察者需要監聽的狀態發生的改變
- onDependentViewChanged
當被監聽的 View 發生改變時回調,可以在此方法里面做一些相應的聯動動畫等效果
例如 AppBarLayout 與 RecyclerView 的聯動,就是 AppBarLayout 監聽了 RecyclerView 的滑動。可以根據這個規則自定義更多的效果!!!
2. 某個 View 需要監聽 CoordinatorLayout 里面所有控件的滑動狀態 ( Google 專門提供的對滑動效果的處理,主要是計算了滑動的距離等屬性 )
能被 CoordinatorLayout 捕獲到的滑動狀態的控件有:RecyclerView、NestedScrollView 等實現了 NestedScrollChild 接口的類
需要重寫的方法:onStartNestedScroll, onNestedPreScroll, onNestedScroll、onNestedFling 等
onNestedFling 方法是 Fling 狀態時回調,其中可以通過 NestedScrollView 的 fling 方法直接傳入參數中計算好的速度值進行滑動
五、Behavior 機制的實現原理
分析 Behavior 主要是為了探索 CoordinatorLayout 是如何做到監聽里面子控件的狀態改變并執行 Behavior 中回調方法的過程。在這個過程中我們也可以將 Behavior 中的所有回調方法做一個梳理,明確每一個方法的作用,在自定義 Behavior 時也就更能最高效、穩定的實現我們想要的效果。
1. CoordinatorLayout 中的 LayoutParams 及 Behavior 的實例化
CoordinatorLayout 類中有一個內部類 LayoutParams 繼承了 ViewGroup.MarginLayoutParams,LayoutParams 中保存了 CoordinatorLayout 中子 View 的布局信息。
在 LayoutInflater 的 inflate 方法中,對 ViewGroup 添加 View 時會調用 addView 方法其中為 Child 指定的 LayoutParams 由 generateLayoutParams 方法創建。CoordinatorLayout 重寫了 generateLayoutParams 方法,會創建一個新的 LayoutParams 對象并返回。
// CoordinatorLayout
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
在 LayoutParams 的創建過程中會解析 View 在 xml 中配置的屬性,除了解析普通 View 的屬性外還包括 AnchorId、Behavior 等,在 Behavior 的解析過程中,會根據配置的 Behavior 屬性的值,先通過規律找到對應的 Behavior 類,然后通過反射創建指定的 Behavior 實例,并將這個實例保存在 LayoutParams 中。如若 Behavior 對象不為空,還會調用其 onAttachedToLayoutParams 方法。
2. Behavoir 的 onMeasureChild、onLayoutChild 方法
在 CoordinatorLayout 的 onMeasure 方法中,在對子 View 進行測量時,如果 View 綁定了 Behavior,會先調用該 Behavior 的 onMeasureChild 方法,由 Behavior 對當前 View 進行自定義的測量并返回是否測量完成,如果 Behavior 測量完成 CoordinatorLayout 將不會測量該 View。
CoordinatorLayout 的 onlayout 方法同 onMeasure 方法,會調用 Behavior 的 onLayoutChild 方法
3. Behavoir 的 onInterceptTouchEvent 和 onTouchEvent 方法
在 CoordinatorLayout 的 onInterceptTouchEvent 方法中,如果子 View 有 Behavior 就會調用該 Behavior 的 onInterceptTouchEvent 方法,也就是在 CoordinatorLayout 將事件分發到子 View 之前,先由 Behavoir 進行攔截判斷,DOWN、UP、CANCLE 時 CoordinatorLayout 的 onInterceptTouchEvent 方法不會使用 Behavoir 的返回結果,其他事件時會使用 Behavoir 的返回結果作為自己的返回結果。如果 Behavoir 攔截事件,還會為 CoordinatorLayout 的 mBehaviorTouchView 屬性賦值為攔截事件的 Behavoir 綁定的 View
這里需要注意,CoordinatorLayout 的 onInterceptTouchEvent 方法的返回值決定了 CoordinatorLayout 是否攔截當前事件,Behavoir 決定攔截也是作用在 CoordinatorLayout 上。
CoordinatorLayout 的 onTouchEvent 中會判斷 mBehaviorTouchView 的值是否為空,不為空時會調用其綁定的 Behavior 的 onTouchEvent 方法,然后返回 Behavoir 的 onTouchEvent 方法的返回值和 CoordinatorLayout 的 super.onTouchEvent 的的返回值進行求或運算后的結果。如果 mBehaviorTouchView 為空則直接返回 CoordinatorLayout 的 super.onTouchEvent 的結果。
4. Behavior 的 layoutDependsOn 、onDependentViewRemoved、 onDependentViewChanged 方法
在 CoordinatorLayout 的 onMeasure 方法中,在對子 View 進行測量之前,調用了一個 prepareChildren 方法,其中通過兩個 List 將 View 間的依賴進行整理
// CoordinatorLayout
// 保存了存在依賴的 View
private final List<View> mDependencySortedChildren = new ArrayList<>();
// DirectedAcyclicGraph 是一個其中無環的圖結構
private final DirectedAcyclicGraph<View> mChildDag = new DirectedAcyclicGraph<>();
private void prepareChildren() {
mDependencySortedChildren.clear();
mChildDag.clear();
for (int i = 0, count = getChildCount(); i < count; i++) {
final View view = getChildAt(i);
final LayoutParams lp = getResolvedLayoutParams(view);
lp.findAnchorView(this, view);
mChildDag.addNode(view);
// 遍歷 CoordinatorLayout 中的其他 View,判斷當前 View 是否依賴遍歷到的 View,如果依賴,則將遍歷到的 View 加入 mChildDag 中
for (int j = 0; j < count; j++) {
if (j == i) {
continue;
}
final View other = getChildAt(j);
// dependsOn 方法中會調用 lp 中 Behavior 的 layoutDependsOn 來決定是否依賴 other
if (lp.dependsOn(this, view, other)) {
if (!mChildDag.contains(other)) {
// 如果依賴則將遍歷到的 View 加入 mChildDag 中
mChildDag.addNode(other);
}
// 為圖添加一條邊
mChildDag.addEdge(other, view);
}
}
}
// 將 mChildDag 圖構造成集合然后添加到 mDependencySortedChildren 中
mDependencySortedChildren.addAll(mChildDag.getSortedList());
// We also need to reverse the result since we want the start of the list to contain
// Views which have no dependencies, then dependent views after that
Collections.reverse(mDependencySortedChildren);
}
prepareChildren 方法執行完畢后,mDependencySortedChildren 集合中就保存了有依賴的 View。prepareChildren 方法執行后,CoordinatorLayout 的 onMeasure 方法中會接著調用 ensurePreDrawListener 方法,該方法中會判斷 View 間是否有依賴,如果有則會調用 addPreDrawListener() 方法,如果沒有會調用 removePreDrawListener 方法。
// Coordinatorlayout
void addPreDrawListener() {
if (mIsAttachedToWindow) {
// Add the listener
if (mOnPreDrawListener == null) {
mOnPreDrawListener = new OnPreDrawListener();
}
final ViewTreeObserver vto = getViewTreeObserver();
vto.addOnPreDrawListener(mOnPreDrawListener);
}
// Record that we need the listener regardless of whether or not we're attached.
// We'll add the real listener when we become attached.
mNeedsPreDrawListener = true;
}
/**
* Remove the pre-draw listener if we're attached to a window and mark that we currently
* do not need it when attached.
*/
void removePreDrawListener() {
if (mIsAttachedToWindow) {
if (mOnPreDrawListener != null) {
final ViewTreeObserver vto = getViewTreeObserver();
vto.removeOnPreDrawListener(mOnPreDrawListener);
}
}
mNeedsPreDrawListener = false;
}
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
@Override
public boolean onPreDraw() {
onChildViewsChanged(EVENT_PRE_DRAW);
return true;
}
}
addPreDrawListener 方法中是將一個 OnPreDrawListener 對象注冊到了 ViewTreeObserver 中,removePreDrawListener 則是將 OnPreDrawListener 對象從 ViewTreeObserver 中取消注冊。ViewTreeObserver 是管理 View 樹的觀察者,View 發生變化時,ViewTreeObserver 會將變化分發到所有已經注冊的 OnPreDrawListener 中。
如果 View 間有依賴,那么 View 狀態變化時 CoordinatorLayout 的 onChildViewsChanged 就會被調用
// CoordinatorLayout
final void onChildViewsChanged(@DispatchChangeEvent final int type) {
// ... 其他代碼
// 遍歷 View 將事件分發下去
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (type == EVENT_PRE_DRAW && child.getVisibility() == View.GONE) {
// Do not try to update GONE child views in pre draw updates.
continue;
}
// 先遍歷依賴的 View 中是否有卯點依賴,如果有,則先將 View 的變化分發到通過卯點依賴的 View
for (int j = 0; j < i; j++) {
final View checkChild = mDependencySortedChildren.get(j);
if (lp.mAnchorDirectChild == checkChild) {
// 存在卯點依賴時,卯點發生狀態改變時同時將對應的 View 狀態修改
offsetChildToAnchor(child, layoutDirection);
}
}
// ... 其他代碼
// 遍歷所有存在依賴的 View
for (int j = i + 1; j < childCount; j++) {
final View checkChild = mDependencySortedChildren.get(j);
final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
final Behavior b = checkLp.getBehavior();
// layoutDependsOn 判斷是否依賴
if (b != null && b.layoutDependsOn(this, checkChild, child)) {
if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
// If this is from a pre-draw and we have already been changed
// from a nested scroll, skip the dispatch and reset the flag
checkLp.resetChangedAfterNestedScroll();
continue;
}
final boolean handled;
switch (type) {
case EVENT_VIEW_REMOVED:
// View 的移除事件,則調用 Behavoir 的 onDependentViewRemoved 方法
b.onDependentViewRemoved(this, checkChild, child);
handled = true;
break;
default:
// 其他事件,調用 Behavoir 的 onDependentViewRemoved 方法
handled = b.onDependentViewChanged(this, checkChild, child);
break;
}
if (type == EVENT_NESTED_SCROLL) {
// If this is from a nested scroll, set the flag so that we may skip
// any resulting onPreDraw dispatch (if needed)
checkLp.setChangedAfterNestedScroll(handled);
}
}
}
}
// ... 其他代碼
}
通過代碼分析知道了 CoordinatorLayout 中只要 View 間存在依賴,那么 View 變化時 onChildViewsChanged 方法就會被調用,該方法中會將卯點變化事件處理,卯點改變時對應的 View 狀態也要改變。還會將事件改變分發到依賴的 Behavior 中,這樣在 Behavior 中就可以處理啊依賴的 View 狀態的變化事件了。
5. Behavior 的嵌套滑動系列方法
CoordinatorLayout 實現了 NestedScrollingParent 接口,所以當其中的子 View 存在實現了 NestedScrollingChild 接口的類時,子 View 的滑動事件都會分發到 CoordinatorLayout 中。這部分嵌套滑動機制有專門文章講解。
我們就以 onNestedScroll 方法為例來分析滑動事件分發到 CoordinatorLayout 后的處理。
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed) {
onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
ViewCompat.TYPE_TOUCH);
}
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int type) {
final int childCount = getChildCount();
boolean accepted = false;
// 遍歷子 View 分發滑動事件
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
if (view.getVisibility() == GONE) {
// If the child is GONE, skip...
continue;
}
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
// 如果 LayoutParams 沒有接受這個事件序列,則不用處理
if (!lp.isNestedScrollAccepted(type)) {
continue;
}
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
// 如果子 View 有 Behavior ,則調用 Bihavior 的 onNestedScroll 方法將滑動事件分發給子 View
viewBehavior.onNestedScroll(this, view, target, dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, type);
accepted = true;
}
}
if (accepted) {
onChildViewsChanged(EVENT_NESTED_SCROLL);
}
}
這里的分發就比較簡單了,只要 CoordinatorLayout 中檢測到了子 View 產生了滑動,就會將對應的滑動事件分發給所有配置了 Behavior 的 View 中,這樣在 Behavior 的滑動系列方法中,當前 View 就可以根據滑動做不同的相應。
其他 onNestedPreScroll、onStopNestedScroll、onNestedFling 等一系列方法同 onNestedScroll 過程類似,就不一一分析了。
6. Behavior 總結
到這里 Behavior 的工作過程就分析完了,在分析的過程中也逐漸發現設計的巧妙。通過 Behavior 我們也能實現更多的 View 間的依賴效果。Google 也給我們提供了 AppBarLayout、CollapsingToolbarLayout 等類提供了很多效果。當然我們不僅要會用這些類實現我們的需求,還要了解其深層次的原理與工作機制,這樣在自定義、修改的時候就會更加得心應手。
思路延伸
通過 CoordinatorLayout 對其中配置了 Behavior 的 View 的處理方式我們可以得到一些思路,在自定義 ViewGroup 的時候,如果其中的子 View 可以配置一些由 Viewroup 提供的自定義的一些屬性,當然系統的子 View 是無法感知的,我們可以通過 ViewGroup 在 inflat 解析布局時解析到 View 的 attr 中配置的自定義屬性
然后由 ViewGroup 來管理配置了這些屬性的 View ,在 ViewGroup 想要這些屬性生效或者根據這些屬性作出一定的效果時,就可以直接操作這些配置了自定義屬性的 View,并且可以根據配置的自定義屬性的值作出不同的效果。