CoordinatorLayout系列(二)AppBarLayout

系列文章:
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:BaseBehaviorScrollingViewBehavior,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,就能達到相應的效果。

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