CoordinatorLayout
在CoordinatorLayout出現之前,爲了處理嵌套滑動邏輯,一般需要繼承一個ViewGroup,重寫onInterceptTouchEvent和onTouchEvent等方法並實現相應的邏輯,然後在佈局中直接引用。這樣做有一點非常不好的地方就是:代碼冗餘,同時處理邏輯也比較繁瑣。爲此,CoordinatorLayout提供了一套非常完美的解決方案,具體來說:
解耦
爲了給開發者提供足夠的可擴展性,CoordinatorLayout以一個代理者的身份被設計的,具體來說,在嵌套滑動過程中,CoordinatorLayout儘可能的只做事件的轉發,將事件轉發給具體的處理邏輯,即:Behavior處理。因此,對於不同的嵌套處理方案編寫不同的Behavior,同時可以不考慮其直接子view類型時實現功能,保證該子view的完整性和封裝性。
Behavior
簡要介紹常用api的用途:
- layoutDependsOn:view之間依賴關係判斷接口
- onDependentViewChanged:依賴view變化時回調接口,在這裏可以做跟隨依賴view變化而變化的邏輯
- onLayoutChild:可以劫持並處理CoordinatorLayout佈局child的接口,在這裏可以實現自己的佈局方式
- onMeasureChild:和onLayoutChild類似,可以劫持CoordinatorLayout測量child的邏輯
- onStartNestedScroll:判斷是否需要處理滑動事件的接口,很重要、很重要、很重要。
- onNestedPreScroll:作爲CoordinatorLayout劫持直接子view滑動時的接口,可以優先處理滑動,保證父view優先於子view處理滑動,主要是爲了實現滑動的順序性,即誰先滑動誰後滑動。
- onNestedScroll:當子view不能滑動時,CoordinatorLayout如何處理後續滑動的接口,如SwipeLayout的攔截原理
- onNestedPreFling:與onNestedPreScroll類似,CoordinatorLayout可以提前劫持直接子view的慣性滑動事件
- onNestedFling:與onNestedScroll類似,繼續處理慣性滑動
- onStopNestedScroll:滑動停止時的回調,在這裏可以清理數據狀態、做滾動到指定位置等等。注意在一個滾動事件的生命週期內被回調次數
注意點:
- onStartNestedScroll:攔截事件點需要認真考慮清楚,例如:RecycleView跟隨headerView連續滑動的場景下,當headerview已經滑到底時,若RecycleView繼續向上滑動就不需要攔截事件,使用RecycleView自身的處理邏輯更合理。
- onStopNestedScroll:在一個滾動事件的生命週期內至少會被調用兩次(謎一樣的設計),若在這裏處理動畫時,需要判斷處理時機,否則可能出現如抖動等異常效果。
基本上處理好上述兩個api的使用,嵌套滑動不是難事
原理
上面說CoordinatorLayout主要是將事件轉發給具體的處理邏輯進行處理,那麼如何轉發的呢?這裏分析幾個主要api的源碼:
onDependentViewChanged – 如何監聽 View 的狀態
在onMeasure方法中調用:
void ensurePreDrawListener() {
boolean hasDependencies = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
if (hasDependencies(child)) {
hasDependencies = true;
break;
}
}
if (hasDependencies != mNeedsPreDrawListener) {
if (hasDependencies) {
addPreDrawListener();
} else {
removePreDrawListener();
}
}
}
void addPreDrawListener() {
if (mIsAttachedToWindow) {
// Add the listener
if (mOnPreDrawListener == null) {
mOnPreDrawListener = new OnPreDrawListener();
}
// 重點:給ViewTreeObserver 添加OnPreDrawListener,在draw方法中進行判斷是否依賴view有狀態變化
final ViewTreeObserver vto = getViewTreeObserver();
vto.addOnPreDrawListener(mOnPreDrawListener);
}
mNeedsPreDrawListener = true;
}
在 OnPreDrawListener 監聽裏面會調用 onChildViewsChanged 方法,在該方法裏面會根據 View的狀態回調 onDependentViewRemoved 或者 onDependentViewChanged 方法。
onStartNestedScroll – 嵌套滑動事件處理的開關
@Override
public boolean onStartNestedScroll(View child, View target, int axes, int type) {
boolean handled = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
if (view.getVisibility() == View.GONE) {
// If it's GONE, don't dispatch
continue;
}
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
//回調Behavior#onStartNestedScroll方法
final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child,target, axes, type);
handled |= accepted;
//標記後續事件是否傳遞給該view,可以查看下面onNestedPreScroll的源碼
lp.setNestedScrollAccepted(type, accepted);
} else {
lp.setNestedScrollAccepted(type, false);
}
}
return handled;
}
主要思路:遍歷所有直接子view,交給某直接子view的Behavior處理嵌套滑動,並根據處理結果標記是否繼續轉發後續事件。
onNestedPreScroll
@Override
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();
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();
//是否轉發事件開關
if (!lp.isNestedScrollAccepted(type)) {
continue;
}
//這裏是劫持子view優先滑動的關鍵,主要思路:
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
mTempIntPair[0] = mTempIntPair[1] = 0;
// 1、優先滑動
viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mTempIntPair, type);
// 2、計算未消費完的滑動距離
xConsumed = dx > 0 ? Math.max(xConsumed, mTempIntPair[0])
: Math.min(xConsumed, mTempIntPair[0]);
yConsumed = dy > 0 ? Math.max(yConsumed, mTempIntPair[1])
: Math.min(yConsumed, mTempIntPair[1]);
accepted = true;
}
}
// 3、將未消費完的滑動距離返還給子view,子view根據該距離進行後續滑動
consumed[0] = xConsumed;
consumed[1] = yConsumed;
if (accepted) {
onChildViewsChanged(EVENT_NESTED_SCROLL);
}
}
從改段源碼中可以看出:任何一個子view的Behavior都可以引起父view優先滑動,具體思路看代碼註解。
performIntercept
onInterceptTouchEvent 和 onTouchEvent最終都走到改方法中
private boolean performIntercept(MotionEvent ev, final int type) {
boolean intercepted = false;
boolean newBlock = false;
MotionEvent cancelEvent = null;
final int action = ev.getActionMasked();
final List<View> topmostChildList = mTempList1;
// 1、 對view進行排序,排序規則是:Android5.0以上系統,按照z屬性來排序;其它,按照添加順序或者自定義的繪製順序來排列。
getTopSortedChildren(topmostChildList);
// Let topmost child views inspect first
final int childCount = topmostChildList.size();
for (int i = 0; i < childCount; i++) {
final View child = topmostChildList.get(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Behavior b = lp.getBehavior();
// 3、若已有view攔截了事件,則其它view不能進行攔截處理,按照view事件處理流程需要給其它view發送cancle事件
if ((intercepted || newBlock) && action != MotionEvent.ACTION_DOWN) {
// Cancel all behaviors beneath the one that intercepted.
// If the event is "down" then we don't have anything to cancel yet.
if (b != null) {
if (cancelEvent == null) {
final long now = SystemClock.uptimeMillis();
cancelEvent = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
}
switch (type) {
case TYPE_ON_INTERCEPT:
b.onInterceptTouchEvent(this, child, cancelEvent);
break;
case TYPE_ON_TOUCH:
b.onTouchEvent(this, child, cancelEvent);
break;
}
}
continue;
}
// 2、遍歷子view,依次調用子view的Behavior進行事件攔截與否的處理
if (!intercepted && b != null) {
switch (type) {
case TYPE_ON_INTERCEPT:
intercepted = b.onInterceptTouchEvent(this, child, ev);
break;
case TYPE_ON_TOUCH:
intercepted = b.onTouchEvent(this, child, ev);
break;
}
if (intercepted) {
mBehaviorTouchView = child;
}
}
// Don't keep going if we're not allowing interaction below this.
// Setting newBlock will make sure we cancel the rest of the behaviors.
final boolean wasBlocking = lp.didBlockInteraction();
final boolean isBlocking = lp.isBlockingInteractionBelow(this, child);
newBlock = isBlocking && !wasBlocking;
if (isBlocking && !newBlock) {
// Stop here since we don't have anything more to cancel - we already did
// when the behavior first started blocking things below this point.
break;
}
}
topmostChildList.clear();
return intercepted;
}
這裏的處理邏輯遵循了view事件處理規則,在第2步驟中是優先處理被依賴的view的。如view A依賴view B,則B有優先處理攔截事件與處理事件的權利,具體可以看CoordinatorLayout的layout源碼,這裏不做分析。其他源碼分析可以看前人的博客:一步步帶你讀懂 CoordinatorLayout 源碼,比較容易理解。
demo地址:https://github.com/zjf71165/BehaviorDemo.git