系列文章:
CoordinatorLayout系列(一):Behavior
CoordinatorLayout系列(二)AppBarLayout
CoordinatorLayout系列(三)AppBarLayout之layout_scrollFlags
CoordinatorLayout系列(四)CollapsingToolbarLayout
CoordinatorLayout系列(五)例子
這一篇文章結合AppBarLayout來實現頭部滑動的跟隨。
先看實際效果:
上面就是實現了recyclerview和ToolBar的聯動效果。
其實要實現這個功能很簡單,因爲CoordinatorLayout和AppBarLayout將大部分功能實現了。
佈局文件:
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/coordinator"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/colorPrimary"
app:layout_scrollFlags="scroll|enterAlways"
app:title="AppBarLayoutExample"
app:titleTextAppearance="@style/ToolbarTitleStyle"></androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
上面就是在AppBarLayout中包了一個ToolBar,然後將Recyclerview的:layout_behavior
屬性設置成string/appbar_scrolling_view_behavior"
,還有一點,就是ToolBar的滑動標誌位:layout_scrollFlags,設置完這些,就能實現上面的功能,就是這麼簡單。
demo地址:https://github.com/whoami-I/CoordinatorLayoutExample
下面我們來看AppBarLayout和CoordinatorLayout爲我們做了什麼。首先在AppBarLayout中有兩個Behavior:BaseBehavior
和ScrollingViewBehavior
,BaseBehavior屬於AppBarLayout;ScrollingViewBehavior屬於Recyclerview。其中ScrollingViewBehavior.layoutDependsOn是這樣的;
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
// We depend on any AppBarLayouts
return dependency instanceof AppBarLayout;
}
所以說,在這裏,Recyclerview是依賴於AppBarLayout的,也就是如果AppBarLayout發生了變化,那麼Recyclerview就要發生變化,這裏就有一個問題,我們的點擊事件明明是作用在Recyclerview上的,要動那也是Recyclerview先動啊,怎麼變成了Recyclerview跟隨AppBarLayout動呢?這裏就要說一說NestedScroll機制了。
NestedScroll機制
NestedScroll機制其實是和View的點擊事件傳遞機制相反,view的點擊事件是從ViewGroup一層層往子View的方向傳,直到這個事件被消費,而NestedScroll機制一般用於滑動,在子view滑動之前,它先問父View需不需要消耗滑動事件,如果被父view消費了一部分之後,子View再消費剩下的部分,如果子view還是沒有消費完事件,那麼將剩下的滑動事件全部交由父view滑動。就這樣父view、子view就滑動事件形成了一個很好的互動,CoordinatorLayout就利用這麼一點來形成聯動效果。
下面簡單看一下CoordinatorLayout和Recyclerview就NestedScroll機制具體是如何實現的,實現的核心就是兩個接口,一個是CoordinatorLayout實現的NestedScrollingParent,一個是Recyclerview實現的NestedScrollingChild。
NestedScrollingParent定義的一系列接口用於消費事件,而NestedScrollingChild定義的接口用於分發事件,正好符合上面的邏輯。
具體流程,在Recyclerview的onTouchEvent裏面,Move事件來臨時執行下面一段代碼:
if (mScrollState == SCROLL_STATE_DRAGGING) {
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
// 將move事件先交由parent處理
if (dispatchNestedPreScroll(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
mReusableIntPair, mScrollOffset, TYPE_TOUCH
)) {
// mReusableIntPair裏面記錄了parent具體消耗了多少x,y方向的move事件
dx -= mReusableIntPair[0];
dy -= mReusableIntPair[1];
// Updated the nested offsets
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
// Scroll has initiated, prevent parents from intercepting
getParent().requestDisallowInterceptTouchEvent(true);
}
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];
// 剩下的move事件,dx,dy自己嘗試消化,自己不一定能全部消費,因爲recyclerview會滑倒頂部,剩下沒有消費完的還會交由parent處理
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
e)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
if (mGapWorker != null && (dx != 0 || dy != 0)) {
mGapWorker.postFromTraversal(this, dx, dy);
}
}
}
從上面的邏輯就可以看到CoordinatorLayout先消費事件了,最後調用到CoordinatorLayout.onNestedPreScroll,這個方法裏面其實就是對子View的Behavior進行遍歷了,誰需要消費就交給誰消費:
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int type) {
int xConsumed = 0;
int yConsumed = 0;
boolean accepted = false;
final int childCount = getChildCount();
//遍歷子view,獲取相應的Behavior,由Behavior來代理消費事件
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();
// Recyclerview其實會進入這個if,因此也就不消耗事件,都交給AppBarLayout
if (!lp.isNestedScrollAccepted(type)) {
continue;
}
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
mBehaviorConsumed[0] = 0;
mBehaviorConsumed[1] = 0;
// AppBarLayout就會根據內部view的scrollFlags進行相應的滑動處理,注意AppBarLayout本身是不滑動的,只是對內部滑動
viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mBehaviorConsumed, type);
xConsumed = dx > 0 ? Math.max(xConsumed, mBehaviorConsumed[0])
: Math.min(xConsumed, mBehaviorConsumed[0]);
yConsumed = dy > 0 ? Math.max(yConsumed, mBehaviorConsumed[1])
: Math.min(yConsumed, mBehaviorConsumed[1]);
accepted = true;
}
}
consumed[0] = xConsumed;
consumed[1] = yConsumed;
//因爲dependency改變了,因此child也要相應的改變位置,在這裏就是調用recyclerview對應Behavior的onDependentViewChanged,具體表現就是跟隨AppBarLayout滑動
if (accepted) {
onChildViewsChanged(EVENT_NESTED_SCROLL);
}
}
上面具體是誰來消費事件,主要由
if (!lp.isNestedScrollAccepted(type)) {
continue;
}
這個方法來決定,AppBarLayout複寫了這個方法,會返回true,因此不會進入continue。
總結
1、CoordinatorLayout之所以能夠能形成上面的聯動效果,是基於NestedScroll機制,實現了NestedScrollParent接口
2、CoordinatorLayout的Behavior也是至關重要的,因爲CoordinatorLayout內部對子view的操作,幾乎所有的流程都被Behavior代理了,無論是measure、layout還是OnInterceptTouchEvent、事件的消費等等。這也就間接的使得傳統的事件傳遞是對單個的獨立子view操作,變成了對Behavior的操作,然後由Behavior間接操作子view,這樣也就充分的解耦了parent view和child view,只要child view實現了相應的Behavior,就能達到相應的效果。