CoordinatorLayout
一、實現(xiàn)滑動 RecyclerView 實現(xiàn) FAB 以及 Toolbar 的顯示和隱藏
(一)傳統(tǒng)實現(xiàn)思路:
- 監(jiān)聽 RecyclerView 的滑動
- 根據(jù)滑動距離及狀態(tài)執(zhí)行顯示和隱藏的動畫
(二)CoordinatorLayout 方式
二、CoordinatorLayout
CoordinatorLayout 繼承自 ViewGroup,通過協(xié)調(diào)并調(diào)度里面的子控件或者子布局來實現(xiàn)觸摸 (一般指滑動) 產(chǎn)生一些相關(guān)的動畫效果??梢酝ㄟ^設(shè)置 View 的 Behavior 屬性來實現(xiàn)觸摸的動畫調(diào)度。
1. CoordinatorLayout 中使用 SnackBar
可以解決 SnackBar 出現(xiàn)時遮擋 FloatingActionButton 的情況,其 Behavior 實現(xiàn)類是 FloatingActionButton.Behavior
2. AppBarLayout
AppBarLayout 繼承了 LinearLayout,并且是垂直方向,里面可以放多個 View, 在 CoordinatorLayout 中的 Scrolling View 滑動時,AppBarLayout 中的 View 可以實現(xiàn)多種隱藏、顯示效果。
Scrolling 是指:RecyclerView、NestedScrollView 等實現(xiàn)了 NestedScrollChild 接口的類
CoordinatorLayout 中,使用 AppBarLayout 包裹 Toolbar,再為 Scrolling View 設(shè)置 app:layout_behavior 屬性為 appbar_scrolling_view_behavior ,設(shè)置 Toolbar 的 layout_scrollFlags 屬性的值為 scroll,就實現(xiàn)了 RecyclerView 滑動時 Toolbar 自動的顯示隱藏的效果。為 layout_scrollFlags 參數(shù)設(shè)置不同的值就可以實現(xiàn)不同的效果。
scroll: 里面所有的子控件想要滑出屏幕的時候都需要設(shè)置這個 Flag,里面沒有設(shè)置這個 Flag 的 View 都將被固定在頂部,效果為:隱藏的時候,先整體向上滾動,直到 AppBarLayout 完全隱藏,再開始滾動 Scrolling View;顯示的時候,直到 Scrolling View 頂部完全出現(xiàn)后,再開始滾動整體直到 AppBarLayout 完全顯示。
enterAlways ,快速返回,設(shè)置這個屬性后,與 scroll 類似,只不過向下滾動先顯示子控件到完全,再滾動 Scrolling View了,需要與 scroll 配合使用
enterAlwaysCollapsed: 需要和 enterAlways 一起使用(scroll|enterAlways|enterAlwaysCollapsed),還需要為子控件設(shè)置 minHight 屬性,和 enterAlways 不一樣的是,不會顯示子控件到完全再滾動 Scrolling View,而是先滾動 子控件到最小高度,再滾動 Scrolling View,最后再滾動 AppBarLayout 到完全顯示。
exitUnitilCollapsed: 定義了子控件 消失的規(guī)則。發(fā)生向上滾動事件時,子控件向上滾動退出直至最小高度(minHeight),然后 Scrolling View 開始滾動。也就是,子控件不會完全退出屏幕
snap: 定義了是子控件滾動比例的一個吸附效果。也就是說,子控件不會存在局部顯示的情況,滾動子控件的部分高度,當(dāng)我們松開手指時,子控件要么向上全部滾出屏幕,要么向下全部滾進(jìn)屏幕,有點類似 ViewPager 的左右滑動。而向上還是向下滑動取決于顯示和隱藏部分的比例,顯示的多就會向下全部顯示,隱藏的多就會向上完全隱藏。
可以使用 CoordinatorLayout + View + Toolbar + TabLayout + ViewPager(內(nèi)容可垂直滑動) 組合實現(xiàn)的 TabLayout 貼頂效果。
3. CollapsingToolbarLayout
可以實現(xiàn) Toolbar 的折疊效果,使用 AppBarLayout 嵌套 CollapsingToolbarLayout,再使用 CollapsingToolbarLayout 嵌套 Toolbar,
注意:
AppBarLayout 需要設(shè)置固定的高度,實現(xiàn)折疊效果時要大于 Toolbar 高度
CollapsingToolbarLayout 設(shè)置 height 為占滿父布局
CollapsingToolbarLayout 為 AppBarLayout 的直接子控件,因為需要折疊 CollapsingToolbarLayout ,所以需要為 CollapsingToolbarLayout 設(shè)置 layout_scrollFlags 屬性設(shè)置為可隱藏
-
在 CollapsingToolbarLayout 中添加其他 View 放在 Toolbar 上面,并且為這個 View 和 Toolbar 設(shè)置 layout_collapseMode 屬性
- parallax: 效果為視差模式,折疊的時候會有視差效果??梢源钆?layout_collapseParallaxMultiplier 屬性,值的區(qū)間為 0 - 1,用來設(shè)置視差效果的明顯程度,為 1 時候的表現(xiàn)為 CollapsingToolbarLayout 其余部分被折疊后再折疊 toolbar 也就是無視差效果,0 的時候為先折疊 toolbar 再折疊其余部分也就是視差效果最明顯
- none:沒有任何效果,往上滑動的時候 Toolbar 會被首先退出去
- pin:固定模式,toolbar 設(shè)置該模式,在滑動時會有一個融合效果,融合完成后 toolbar 會固定在頂端
CollapsingToolbarLayout 可以設(shè)置 expandedTitleMargin 屬性控制展開時的文字 margin,collapsedTitleTextAppearance 屬性控制折疊時的文字樣式等
contentScrim 是一個顏色,內(nèi)容部分的沉浸式效果,可以讓 Toolbar 和其他 View 有一個漸變的過渡效果,statusBarScrim 是為狀態(tài)欄設(shè)置顏色(5.0+ 才有效果)
還有其他很多屬性設(shè)置不同的效果
4. Behavior (CoordinatorLayout.Behavior) 需配合 CoordinatorLayout 使用
四、Behavior + CoordinatorLayout
Behavior 可以看作一個橋梁或者監(jiān)聽者,實現(xiàn)包裹在 CoordinatorLayout 里面的所有子控件或者容器產(chǎn)生聯(lián)動效果
自定義 Behavior 的兩種效果,繼承 CoordinatorLayout.Behavior
為觀察者設(shè)置 Behavior,這樣被觀察者中 Behavior 監(jiān)聽的狀態(tài)發(fā)生變化時,Behavior 中的對應(yīng)方法會被回調(diào)。
1. 某個 View 需要監(jiān)聽另一個 View 的狀態(tài)(比如:位置、大小、顯示狀態(tài))
需要重寫方法:layoutDependsOn,onDependentViewChanged
- layoutDependsOn
用來決定需要監(jiān)聽哪些控件或者容器的狀態(tài),參數(shù) parent 是 CoordinatorLayout, child 指定了當(dāng)前 behavior 的需要監(jiān)其他 View 的觀察者,dependency 是被觀察的 View;返回值是 dependency 是否是 child 需要監(jiān)聽的 View (通過 id 或者 tag 等方式來判斷) 以及是否是觀察者需要監(jiān)聽的狀態(tài)發(fā)生的改變
- onDependentViewChanged
當(dāng)被監(jiān)聽的 View 發(fā)生改變時回調(diào),可以在此方法里面做一些相應(yīng)的聯(lián)動動畫等效果
例如 AppBarLayout 與 RecyclerView 的聯(lián)動,就是 AppBarLayout 監(jiān)聽了 RecyclerView 的滑動??梢愿鶕?jù)這個規(guī)則自定義更多的效果!?。?/strong>
2. 某個 View 需要監(jiān)聽 CoordinatorLayout 里面所有控件的滑動狀態(tài) ( Google 專門提供的對滑動效果的處理,主要是計算了滑動的距離等屬性 )
能被 CoordinatorLayout 捕獲到的滑動狀態(tài)的控件有:RecyclerView、NestedScrollView 等實現(xiàn)了 NestedScrollChild 接口的類
需要重寫的方法:onStartNestedScroll, onNestedPreScroll, onNestedScroll、onNestedFling 等
onNestedFling 方法是 Fling 狀態(tài)時回調(diào),其中可以通過 NestedScrollView 的 fling 方法直接傳入?yún)?shù)中計算好的速度值進(jìn)行滑動
五、Behavior 機(jī)制的實現(xiàn)原理
分析 Behavior 主要是為了探索 CoordinatorLayout 是如何做到監(jiān)聽里面子控件的狀態(tài)改變并執(zhí)行 Behavior 中回調(diào)方法的過程。在這個過程中我們也可以將 Behavior 中的所有回調(diào)方法做一個梳理,明確每一個方法的作用,在自定義 Behavior 時也就更能最高效、穩(wěn)定的實現(xiàn)我們想要的效果。
1. CoordinatorLayout 中的 LayoutParams 及 Behavior 的實例化
CoordinatorLayout 類中有一個內(nèi)部類 LayoutParams 繼承了 ViewGroup.MarginLayoutParams,LayoutParams 中保存了 CoordinatorLayout 中子 View 的布局信息。
在 LayoutInflater 的 inflate 方法中,對 ViewGroup 添加 View 時會調(diào)用 addView 方法其中為 Child 指定的 LayoutParams 由 generateLayoutParams 方法創(chuàng)建。CoordinatorLayout 重寫了 generateLayoutParams 方法,會創(chuàng)建一個新的 LayoutParams 對象并返回。
// CoordinatorLayout
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
在 LayoutParams 的創(chuàng)建過程中會解析 View 在 xml 中配置的屬性,除了解析普通 View 的屬性外還包括 AnchorId、Behavior 等,在 Behavior 的解析過程中,會根據(jù)配置的 Behavior 屬性的值,先通過規(guī)律找到對應(yīng)的 Behavior 類,然后通過反射創(chuàng)建指定的 Behavior 實例,并將這個實例保存在 LayoutParams 中。如若 Behavior 對象不為空,還會調(diào)用其 onAttachedToLayoutParams 方法。
2. Behavoir 的 onMeasureChild、onLayoutChild 方法
在 CoordinatorLayout 的 onMeasure 方法中,在對子 View 進(jìn)行測量時,如果 View 綁定了 Behavior,會先調(diào)用該 Behavior 的 onMeasureChild 方法,由 Behavior 對當(dāng)前 View 進(jìn)行自定義的測量并返回是否測量完成,如果 Behavior 測量完成 CoordinatorLayout 將不會測量該 View。
CoordinatorLayout 的 onlayout 方法同 onMeasure 方法,會調(diào)用 Behavior 的 onLayoutChild 方法
3. Behavoir 的 onInterceptTouchEvent 和 onTouchEvent 方法
在 CoordinatorLayout 的 onInterceptTouchEvent 方法中,如果子 View 有 Behavior 就會調(diào)用該 Behavior 的 onInterceptTouchEvent 方法,也就是在 CoordinatorLayout 將事件分發(fā)到子 View 之前,先由 Behavoir 進(jìn)行攔截判斷,DOWN、UP、CANCLE 時 CoordinatorLayout 的 onInterceptTouchEvent 方法不會使用 Behavoir 的返回結(jié)果,其他事件時會使用 Behavoir 的返回結(jié)果作為自己的返回結(jié)果。如果 Behavoir 攔截事件,還會為 CoordinatorLayout 的 mBehaviorTouchView 屬性賦值為攔截事件的 Behavoir 綁定的 View
這里需要注意,CoordinatorLayout 的 onInterceptTouchEvent 方法的返回值決定了 CoordinatorLayout 是否攔截當(dāng)前事件,Behavoir 決定攔截也是作用在 CoordinatorLayout 上。
CoordinatorLayout 的 onTouchEvent 中會判斷 mBehaviorTouchView 的值是否為空,不為空時會調(diào)用其綁定的 Behavior 的 onTouchEvent 方法,然后返回 Behavoir 的 onTouchEvent 方法的返回值和 CoordinatorLayout 的 super.onTouchEvent 的的返回值進(jìn)行求或運(yùn)算后的結(jié)果。如果 mBehaviorTouchView 為空則直接返回 CoordinatorLayout 的 super.onTouchEvent 的結(jié)果。
4. Behavior 的 layoutDependsOn 、onDependentViewRemoved、 onDependentViewChanged 方法
在 CoordinatorLayout 的 onMeasure 方法中,在對子 View 進(jìn)行測量之前,調(diào)用了一個 prepareChildren 方法,其中通過兩個 List 將 View 間的依賴進(jìn)行整理
// CoordinatorLayout
// 保存了存在依賴的 View
private final List<View> mDependencySortedChildren = new ArrayList<>();
// DirectedAcyclicGraph 是一個其中無環(huán)的圖結(jié)構(gòu)
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,判斷當(dāng)前 View 是否依賴遍歷到的 View,如果依賴,則將遍歷到的 View 加入 mChildDag 中
for (int j = 0; j < count; j++) {
if (j == i) {
continue;
}
final View other = getChildAt(j);
// dependsOn 方法中會調(diào)用 lp 中 Behavior 的 layoutDependsOn 來決定是否依賴 other
if (lp.dependsOn(this, view, other)) {
if (!mChildDag.contains(other)) {
// 如果依賴則將遍歷到的 View 加入 mChildDag 中
mChildDag.addNode(other);
}
// 為圖添加一條邊
mChildDag.addEdge(other, view);
}
}
}
// 將 mChildDag 圖構(gòu)造成集合然后添加到 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 方法執(zhí)行完畢后,mDependencySortedChildren 集合中就保存了有依賴的 View。prepareChildren 方法執(zhí)行后,CoordinatorLayout 的 onMeasure 方法中會接著調(diào)用 ensurePreDrawListener 方法,該方法中會判斷 View 間是否有依賴,如果有則會調(diào)用 addPreDrawListener() 方法,如果沒有會調(diào)用 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 發(fā)生變化時,ViewTreeObserver 會將變化分發(fā)到所有已經(jīng)注冊的 OnPreDrawListener 中。
如果 View 間有依賴,那么 View 狀態(tài)變化時 CoordinatorLayout 的 onChildViewsChanged 就會被調(diào)用
// CoordinatorLayout
final void onChildViewsChanged(@DispatchChangeEvent final int type) {
// ... 其他代碼
// 遍歷 View 將事件分發(fā)下去
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 的變化分發(fā)到通過卯點依賴的 View
for (int j = 0; j < i; j++) {
final View checkChild = mDependencySortedChildren.get(j);
if (lp.mAnchorDirectChild == checkChild) {
// 存在卯點依賴時,卯點發(fā)生狀態(tài)改變時同時將對應(yīng)的 View 狀態(tài)修改
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 的移除事件,則調(diào)用 Behavoir 的 onDependentViewRemoved 方法
b.onDependentViewRemoved(this, checkChild, child);
handled = true;
break;
default:
// 其他事件,調(diào)用 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 方法就會被調(diào)用,該方法中會將卯點變化事件處理,卯點改變時對應(yīng)的 View 狀態(tài)也要改變。還會將事件改變分發(fā)到依賴的 Behavior 中,這樣在 Behavior 中就可以處理啊依賴的 View 狀態(tài)的變化事件了。
5. Behavior 的嵌套滑動系列方法
CoordinatorLayout 實現(xiàn)了 NestedScrollingParent 接口,所以當(dāng)其中的子 View 存在實現(xiàn)了 NestedScrollingChild 接口的類時,子 View 的滑動事件都會分發(fā)到 CoordinatorLayout 中。這部分嵌套滑動機(jī)制有專門文章講解。
我們就以 onNestedScroll 方法為例來分析滑動事件分發(fā)到 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 分發(fā)滑動事件
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 ,則調(diào)用 Bihavior 的 onNestedScroll 方法將滑動事件分發(fā)給子 View
viewBehavior.onNestedScroll(this, view, target, dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, type);
accepted = true;
}
}
if (accepted) {
onChildViewsChanged(EVENT_NESTED_SCROLL);
}
}
這里的分發(fā)就比較簡單了,只要 CoordinatorLayout 中檢測到了子 View 產(chǎn)生了滑動,就會將對應(yīng)的滑動事件分發(fā)給所有配置了 Behavior 的 View 中,這樣在 Behavior 的滑動系列方法中,當(dāng)前 View 就可以根據(jù)滑動做不同的相應(yīng)。
其他 onNestedPreScroll、onStopNestedScroll、onNestedFling 等一系列方法同 onNestedScroll 過程類似,就不一一分析了。
6. Behavior 總結(jié)
到這里 Behavior 的工作過程就分析完了,在分析的過程中也逐漸發(fā)現(xiàn)設(shè)計的巧妙。通過 Behavior 我們也能實現(xiàn)更多的 View 間的依賴效果。Google 也給我們提供了 AppBarLayout、CollapsingToolbarLayout 等類提供了很多效果。當(dāng)然我們不僅要會用這些類實現(xiàn)我們的需求,還要了解其深層次的原理與工作機(jī)制,這樣在自定義、修改的時候就會更加得心應(yīng)手。
思路延伸
通過 CoordinatorLayout 對其中配置了 Behavior 的 View 的處理方式我們可以得到一些思路,在自定義 ViewGroup 的時候,如果其中的子 View 可以配置一些由 Viewroup 提供的自定義的一些屬性,當(dāng)然系統(tǒng)的子 View 是無法感知的,我們可以通過 ViewGroup 在 inflat 解析布局時解析到 View 的 attr 中配置的自定義屬性
然后由 ViewGroup 來管理配置了這些屬性的 View ,在 ViewGroup 想要這些屬性生效或者根據(jù)這些屬性作出一定的效果時,就可以直接操作這些配置了自定義屬性的 View,并且可以根據(jù)配置的自定義屬性的值作出不同的效果。