CoordinatorLayout解析

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:滑動停止時的回調,在這裏可以清理數據狀態、做滾動到指定位置等等。注意在一個滾動事件的生命週期內被回調次數

注意點:

  1. onStartNestedScroll:攔截事件點需要認真考慮清楚,例如:RecycleView跟隨headerView連續滑動的場景下,當headerview已經滑到底時,若RecycleView繼續向上滑動就不需要攔截事件,使用RecycleView自身的處理邏輯更合理。
  2. 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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章