理解RecyclerView(七)—RecyclerView配合使用CoordinatorLayout及Behavior的嵌套滑動機制

前言: 並不是熱淚盈眶才叫青春,也不是莽撞熱血才叫年輕。不忘初心,便始終都是年輕。多少人把放縱當熱血,並把早熟和自律當做陳腐來嬉笑。歲月還未過多流逝之前,他們的身體和精神就已經被掏空,提早告別了青春。

一、概述

  上一篇文章分析了RecyclerView的滑動原理,依然是由onTouchEvent()觸控事件響應的,最終通過遍歷所有子View,每個子View調用了底層View的offsetTopAndBottom()或者offsetLeftAndRight()方法來實現滑動的。不同的是RecyclerView採用嵌套滑動機制,會把滑動事件通知給支持嵌套滑動的父View先做決定。那麼什麼是嵌套滑動呢?RecyclerView是怎麼處理嵌套滑動的呢?

什麼是嵌套滑動?

我們來看看一個RecyclerView的滑動效果圖:

這就是嵌套滑動的效果,當一個View產生嵌套滑動事件時,會先通知他的父View,詢問父View是否處理這個事件,如果處理那麼子View不處理,如果不處理那麼子View處理,實際上父View只處理部分滑動距離的情況。可以看到由下往上滑動RecyclerView的時候,先處理頭部的滑動事件,然後才處理RecyclerView自身的滑動事件,由上往下滑時,也是先詢問父View是否處理滑動事件,如果不處理則交給RecyclerView自身的處理滑動事件。

<?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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/app_bar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:fitsSystemWindows="true">

        <com.google.android.material.appbar.CollapsingToolbarLayout
            android:id="@+id/toolbar_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:contentScrim="#FF5722"
            app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">

            <ImageView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:src="@mipmap/flower"
                android:scaleType="centerCrop"
                app:layout_scrollFlags="scroll|enterAlwaysCollapsed" />

            <androidx.appcompat.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="40dp"
                app:layout_collapseMode="pin"
                app:title="二級標題" />

        </com.google.android.material.appbar.CollapsingToolbarLayout>
    </com.google.android.material.appbar.AppBarLayout>

    <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" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

這種高大上的視覺交互效果叫做“協調者佈局”,實現這種效果的核心類就是一個CoordinatorLayout,它遵循Material Design風格,結合AppbarLayout,CollapsingToolbarLayout等可以產生各種炫酷的摺疊懸浮效果。

二、CoordinatorLayout與Behavior

照着上面的xml佈局,效果是完成了,但是你是否有很多問號,CoordinatorLayout是啥,幹什麼的?AppBarLayout有什麼用?CollapsingToolbarLayout又是啥?上面的佈局控件除了ImageView,Toolbar,RecyclerView這幾個控件外,其他的我基本不認識。
在這裏插入圖片描述
這是官網CoordinatorLayout的解析截圖,它是一個超級的FrameLayout,注意它繼承自ViewGroup,並沒有繼承FrameLayout,然後實現了NestedScrollingParent接口。

CoordinatorLayout可以作爲一個容器與一個或多個子View進行特定的交互。通過指定Behaviors爲子視圖,可以在單個父視圖中提供許多不同的交互,這些視圖可以彼此交互。

Behavior是CoordinatorLayout的子視圖的交互行爲插件,一個行爲實現了一個或多個用戶可以在子視圖上進行的交互。這些交互可能包括拖動,滑動,甩動或任何其他手勢。
在這裏插入圖片描述
插件也就代表如果一個子View需要某種交互,它就需要加載對應的Behavior,否則它就是不具備交互能力的。而Behavior是一個抽象類,它的實現類都是爲了讓用戶作用在一個View上面進行拖拽、滑動、快速滑動等手勢操作。如果你需要其他的交互動作,則需要自定義Behavior。

但是,我們有了解到Behavior的真正含義嗎?它到底是具體幹什麼的?

前面內容有講過,CoordinatorLayout可以定義與它子View的交互或某些子View之間的交互。先來看看Behavior的源碼:

public static abstract class Behavior<V extends View> {

    public Behavior() {}

    public Behavior(Context context, AttributeSet attrs) {}
	
    public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) { 
    	return false;
    }
	
    public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {  
    	return false;
    }

    public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) {
    }

    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild,
                                       View target, int axes, int type) {
        return false;
    }

    public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, View child, View directTargetChild,
                                       View target, int axes, int type) {
    	// Do nothing
    }

    public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target, int type) {
        // Do nothing
    }

    public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed,
                               int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
        // Do nothing                           
    }

    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx,
                                  int dy, int[] consumed, int type) {
        // Do nothing                              
    }

    public boolean onNestedFling(CoordinatorLayout coordinatorLayout, View child, View target,
                                 float velocityX, float velocityY, boolean consumed) {
        return false;
    }

    public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target,
                                    float velocityX, float velocityY) {
        return false;
    }
}

我們自定義Behavior的一般目的只有兩個,一是根據某些依賴的View的位置改變進行相應的操作;二是響應CoordinatorLayout中某些組件的滑動事件。先來看第一種情況:

2.1 兩個View之間的依賴關係

如果一個View需要依賴另一個View,可能需要操作下面的API:

//確定提供的子視圖是否有另一個特定的同級視圖作爲佈局依賴項
public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) { return false; }
//子View依賴的View的改變做出響應,當依賴視圖在標準佈局流之外的大小或者位置發生變化,此方法被調用。
//Behavior可以使用此方法更新子視圖,如果子視圖大小或者位置發生變化則返回true。
public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {  return false; }
//響應子View在依賴的視圖中移除
public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) {}

通過layoutDependsOn()確定一個View是否對另一個View進行依賴,注意:child是測試的子View,dependency該依賴的子View,如果child的佈局依賴於dependency的佈局則返回true,即依賴成立;反之不成立。當然你可以複寫該方法對dependency進行類型判斷然後再決定是否依賴,只有在layoutDependsOn()返回true的時候後面的onDependentViewChanged()onDependentViewRemoved()纔會執行。

註釋上面有說,無論子View 的順序如何,總是在依賴的子View被佈局後再佈局這個子View,當依賴視圖的佈局發生改變時回調Behavior的onDependentViewChanged()方法,可以適當更新響應的子視圖。如果Behavior改變了child的位置和尺寸時,則返回true,默認返回false。

onDependentViewRemoved()響應child從屬性視圖中被刪除,從parent中移除dependency後調用此方法,
Behavior也可以使用此方法適當更新子視圖來作爲響應。

但是這樣說有點抽象,到底是誰依賴誰?何爲依賴?

下面舉個例子加深理解:首先定義一個可以響應屏幕拖拽的View,DependencyView效果如下:

它的代碼很簡單,繼承自TextView,在觸摸事件onTouchEvent()根據觸摸點移動對自身位置進行位移。

public class DependencyView extends AppCompatTextView {

    private int mTouchSlop;
    private float mLastY;
    private float mLastX;

    public DependencyView(Context context) {
        this(context, null);
    }

    public DependencyView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public DependencyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setClickable(true);
        //在用戶發生滾動之前,以像素爲單位的觸摸距離可能會發生飄移
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN://按下
                mLastX = event.getX();
                mLastY = event.getY();
                break;
            case MotionEvent.ACTION_MOVE://移動
                float moveX = event.getX() - mLastX;
                float moveY = event.getY() - mLastY;
                if (Math.abs(moveX) > mTouchSlop || Math.abs(moveY) > mTouchSlop) {
                    ViewCompat.offsetLeftAndRight(this, (int) moveX);
                    ViewCompat.offsetTopAndBottom(this, (int) moveY);
                    mLastX = event.getX();
                    mLastY = event.getY();
                }
                break;
            case MotionEvent.ACTION_UP://擡起
                mLastX = event.getX();
                mLastY = event.getY();
                break;
        }
        return true;
    }
}

然後實現一個Behavior,讓它只配一個View,去緊緊依賴所支配的View。這裏我們讓依賴的View顯示在被依賴的View的下面,不論被依賴的View位置如何變化,依賴的View都跟着變化:

public class MyBehavior extends CoordinatorLayout.Behavior<View> {
    private static final String TAG = "MyBehavior";

    public MyBehavior() {
    }

    public MyBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    //確定提供的子視圖是否有另一個特定的同級視圖作爲佈局依賴項
    @Override
    public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
        return dependency instanceof DependencyView;
    }

    //子View依賴的View的改變做出響應,當依賴視圖在標準佈局流之外的大小或者位置發生變化,此方法被調用。
    //Behavior可以使用此方法更新子視圖,如果子視圖大小或者位置發生變化則返回true。
    @Override
    public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
        int bottom = dependency.getBottom();
        child.setY(bottom );
        child.setX(dependency.getLeft());
        
        Log.d(TAG, "onDependentViewChanged: " + dependency);
        return true;
    }
}

layoutDependsOn()中通過判斷dependency是否爲DependencyView類型決定是否對其進行依賴,然後再onDependentViewChanged()獲取dependency的位置參數來設置child的位置參數,從而實現了child跟隨dependency的位置改變而發生位置改變。

下面來驗證MyBehavior,我們在佈局文件中對ImageView設置了MyBehavior,然後觀察它的現象:

<?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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_head"
        app:layout_behavior="com.antiphon.recyclerviewdemo.weight.MyBehavior" />

    <com.antiphon.recyclerviewdemo.weight.DependencyView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@color/colorAccent"
        android:padding="4dp"
        android:text="DependencyView"
        android:textColor="#fff"
        android:textSize="18sp" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

注意:佈局最外層需要是CoordinatorLayout作爲父佈局,效果如下:
在這裏插入圖片描述
可以看到,我們在拖動DependencyView的時候,ImageView也跟隨着DependencyView移動。當然這種依賴並非只有一對一的關係,也可能是一對多或者多對多。

我們再修改一下MyBehavior中的代碼,如果child是一個TextView則顯示在 dependency的上方,否則顯示在下方:

   	@Override
    public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
        //child座標
        float childX = child.getX();
        float childY = child.getY();

        //dependency頂部底部座標
        int dependencyTop = dependency.getTop();
        int dependencyBottom = dependency.getBottom();

        childX = dependency.getX();

        if (child instanceof TextView) {//如果是TextView則顯示在dependency上面,否則顯示在下面
            childY = dependencyTop + child.getHeight();
        } else {
            childY = dependencyBottom;
        }

        child.setX(childX);
        child.setY(childY);

        return true;

我們在xml佈局文件再添加一個TextView,設置MyBehavior:

   <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_head"
        app:layout_behavior="com.antiphon.recyclerviewdemo.weight.MyBehavior" />
    
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="helloWord"
        android:textColor="#000"
        android:textSize="14sp"
        app:layout_behavior="com.antiphon.recyclerviewdemo.weight.MyBehavior" />

效果如下:
在這裏插入圖片描述
到這裏我們知道,在Behavior中針對被依賴對象尺寸及位置變化時,依賴方該如何處理的流程,接下來就是處理滑動相關操作了。

2.2 Behavior對滑動事件的響應

我們一般接觸到的滑動控件一般是 ScrollView和 RecyclerView,而CoordinatorLayout本身能滑動嗎?如果能是怎麼滑動的,到底是誰響應誰的滑動。

Behavior的相關滑動代碼如下:

	public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild,
                                       View target, int axes, int type) {
        return false;
    }

    public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, View child, View directTargetChild,
                                       View target, int axes, int type) {
    	// Do nothing
    }

    public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target, int type) {
        // Do nothing
    }

    public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed,
                               int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
        // Do nothing                           
    }

    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx,
                                  int dy, int[] consumed, int type) {
        // Do nothing                              
    }

    public boolean onNestedFling(CoordinatorLayout coordinatorLayout, View child, View target,
                                 float velocityX, float velocityY, boolean consumed) {
        return false;
    }

    public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target,
                                    float velocityX, float velocityY) {
        return false;
    }

爲了觀察Behavior行爲,我在相關滑動方法添加了log:

    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild,
                                       View target, int axes, int type) {
        Log.e(TAG, "onStartNestedScroll:axes == " + axes);
        return super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, axes, type);
    }

    @Override
    public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, View child, View directTargetChild,
                                       View target, int axes, int type) {
        Log.e(TAG, "onNestedScrollAccepted:axes == " + axes + " | type == " + type);
        super.onNestedScrollAccepted(coordinatorLayout, child, directTargetChild, target, axes, type);
    }

    @Override
    public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed,
                               int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
        Log.e(TAG, "onNestedScrollAccepted:dxConsumed == " + dxConsumed + " | dyConsumed == "
                + dyConsumed + " | dxUnconsumed == " + dxUnconsumed + " | dyUnconsumed == " + dyUnconsumed);
        super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type);
    }

    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx,
                                  int dy, int[] consumed, int type) {
        Log.e(TAG, "onNestedScrollAccepted:dx == " + dx + " | dy == " + dy + " | type == " + type);
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
    }

    @Override
    public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int type) {
        Log.e(TAG, "onNestedScrollAccepted:type == " + type);
        super.onStopNestedScroll(coordinatorLayout, child, target, type);
    }

    @Override
    public boolean onNestedFling(CoordinatorLayout coordinatorLayout, View child, View target,
                                 float velocityX, float velocityY, boolean consumed) {
        Log.e(TAG, "onNestedScrollAccepted:velocityX == " + velocityX + " | velocityY == "
                + velocityY + " | consumed == " + consumed);
        return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
    }

    @Override
    public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target,
                                    float velocityX, float velocityY) {
        Log.e(TAG, "onNestedScrollAccepted:velocityX == " + velocityX + " | velocityY == " + velocityY);
        return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
    }

然後我在模擬器上面用鼠標滑動CoordinatorLayout製造一些滑動 事件,觀看MyBehavior相關的滑動函數API是否能觸發,然後觀察log:
在這裏插入圖片描述
可以看到,並沒有觸發任何的函數。那麼先來了解嵌套滑動事件的API:

   /**
     * 如果CoordinatorLayout後代的View嘗試發起嵌套滑動時調用
     * 任何與CoordinatorLayout的任何直接子元素相關聯的Behavior都可以響應這個事件並返回true,
     * 以指示CoordinatorLayout應該作爲這個滑動嵌套滑動的父View,只有返回true才能接收後續的嵌套滑動事件。
     */
	public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild,
                                       View target, int axes, int type) {
        return false;
    }

註釋中說明,當一個CoordinatorLayout的後代想要觸發嵌套滑動事件時,這個方法被調用,只有onStartNestedScroll()返回true,後續的嵌套滑動事件纔會響應。後續響應的函數指的是這幾個函數:

    public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, View child, View directTargetChild,
                                       View target, int axes, int type) {}

    public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target, int type) {}

    public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed,
                               int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {}

    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx,
                                  int dy, int[] consumed, int type) {}

    public boolean onNestedFling(CoordinatorLayout coordinatorLayout, View child, View target,
                                 float velocityX, float velocityY, boolean consumed) {}

    public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target,
                                    float velocityX, float velocityY) {}

那麼,我們先從onStartNestedScroll()開始分析,查找在哪裏調用到這個 方法:
在這裏插入圖片描述
可以看到MyBehavior的onStartNestedScroll()在哪裏被調用了,原來它是在CoordinatorLayout中被調用:

    @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) {
                final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child,
                        target, axes, type);
                handled |= accepted;
                lp.setNestedScrollAccepted(type, accepted);
            } else {
                lp.setNestedScrollAccepted(type, false);
            }
        }
        return handled;
    }

這是CoordinatorLayout中的一個方法,先獲取CoordinatorLayout下的子View,再獲取子View中的Behavior,然後調用Behavior的onStartNestedScroll()方法。

繼續深入,那麼誰調用了CoordinatorLayout的onStartNestedScroll()呢?

繼續追蹤下去:
在這裏插入圖片描述
可以看到有多個地方調用它,其實歸根到底最終都是View或者ViewParentCompat,這裏以View爲例:

   public boolean startNestedScroll(int axes) {
        if (hasNestedScrollingParent()) {
            // Already in progress
            return true;
        }
        if (isNestedScrollingEnabled()) {//是否啓用嵌套滑動
            ViewParent p = getParent();
            View child = this;
            while (p != null) {
                try {
                    if (p.onStartNestedScroll(child, this, axes)) {
                        mNestedScrollingParent = p;
                        p.onNestedScrollAccepted(child, this, axes);
                        return true;
                    }
                }
                if (p instanceof View) {
                    child = (View) p;
                }
                p = p.getParent();
            }
        }
        return false;
    }

當一個View觸發onStartNestedScroll() 的時候,如果符合嵌套滑動,則獲取Parent通過while循環調用Parent的onStartNestedScroll()方法。因爲CoordinatorLayout就是一個ViewGroup,所以它就是一個ViewParent對象,如果一個CoordinatorLayout的後代View觸發了onStartNestedScroll() 方法,如果符合某種條件,那麼CoordinatorLayout 的onStartNestedScroll()方法就會被調用,再進一步調用Behavior的onStartNestedScroll()方法。

當isNestedScrollingEnabled() = true時,它的ViewParent的onStartNestedScroll()才能被觸發,它是被判斷自身是否能嵌套滑動,如果爲true則在使用時作爲嵌套滑動的子視圖,可以通過setNestedScrollingEnabled(boolean enabled)來設置View是否擁有嵌套滑動的能力。

使用TextView產生嵌套滑動事件

如果一個View符合嵌套滑動的事件,也就是通過setNestedScrollingEnabled(true),然後調用它的onStartNestedScroll() 方法,理論上是可以產生嵌套滑動事件的。我們來嘗試一下,在佈局裏面添加一個普通的TextView,然後設置點擊事件。

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        mTv_nested_scroll.setNestedScrollingEnabled(true);
        mTv_nested_scroll.startNestedScroll(View.SCROLL_AXIS_HORIZONTAL);
    }

之前在MyBehavior中對應的嵌套滑動方法打印了log,所以如果CoordinatorLayout中發生嵌套滑動的事件,log就有輸出:
在這裏插入圖片描述可以看到在一個View符合嵌套滑動的事件,則調用它的onStartNestedScroll() 方法。不過在安卓版本21(LOLLIPOP,5.0)及以上時才能調用View的嵌套滑動相關的API,那麼在低於5.0版本呢?其實系統做了兼容:

    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP){
    	ViewCompat.setNestedScrollingEnabled(mTv_nested_scroll, true);
        ViewCompat.startNestedScroll(mTv_nested_scroll, ViewCompat.SCROLL_AXIS_HORIZONTAL);
    }

我們知道5.0以上版本View已經自帶嵌套滑動功能和相關屬性,可以根據ViewCompat這個類完成低版本的兼容操作,繼續跟蹤ViewCompat.startNestedScroll()

   public static void setNestedScrollingEnabled(@NonNull View view, boolean enabled) {
        if (Build.VERSION.SDK_INT >= 21) {
            view.setNestedScrollingEnabled(enabled);
        } else {
            if (view instanceof NestedScrollingChild) {
                ((NestedScrollingChild) view).setNestedScrollingEnabled(enabled);
            }
        }
    }
    
  public static boolean startNestedScroll(@NonNull View view, @ScrollAxis int axes) {
        if (Build.VERSION.SDK_INT >= 21) {
            return view.startNestedScroll(axes);
        }
        if (view instanceof NestedScrollingChild) {
            return ((NestedScrollingChild) view).startNestedScroll(axes);
        }
        return false;
    }

可以看到ViewCompat的源碼裏面也做了版本兼容,在API>=21則會直接調用view.startNestedScroll()相關嵌套滑動方法,否則會判斷view是否爲NestedScrollingChild的實例,如果是則調用NestedScrollingChildstartNestedScroll()方法,NestedScrollingChild是一個接口。

所以如果在5.0以上版本我們可以view.startNestedScroll(),如果在5.0以下版本,如果一個View想發起嵌套滑動事件,你得保證這個View實現NestedScrollingChild接口。

想觸發事件嗎?你是NestedScrollingChild嗎?

我們來看看NestedScrollingChild

public interface NestedScrollingChild {
  	/**
     * 啓用或者禁用此View的嵌套滑動。
     * 注意:如果此屬性設置爲true,則允許該View使用當前層結構中兼容的父View啓動嵌套滑動操作。
     * 		如果這個View沒有任務嵌套滑動,則這個方法沒有任何作用。
     */ 	
    void setNestedScrollingEnabled(boolean enabled);
	/**
     * 如果此View啓用了嵌套滑動,則返回true。
     * 如果啓動了嵌套滑動,並且這個View類實現支持嵌套滑動,那麼這個View將在適用時充當嵌套滑動子View,
     * 將有關正在進行的滾動操作的數據轉發到兼容且協作嵌套滑動的父View。
     */ 	
    boolean isNestedScrollingEnabled();
	/**
     * 沿着給定的方向(垂直或水平方向)開始一個可嵌套滾動的滑動操作。
     * 返回true表示找到一個協作的父節點,並且爲當前手勢窮嵌套滑動。
     */ 	
    boolean startNestedScroll(int axes);
	/**
     * 停止正在進行的嵌套滑動
     */ 
    void stopNestedScroll();
	/**
     * 如果該View有一個嵌套的滑動父View,則返回true
     */ 
    boolean hasNestedScrollingParent();

    boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,int dxUnconsumed, 
    							 int dyUnconsumed, int[] offsetInWindow);

    boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);

    boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);

    boolean dispatchNestedPreFling(float velocityX, float velocityY);
}

這個接口應該由View的子類實現並且希望支持將嵌套滑動操作分派到協作的父View(ViewGroup)。實現這個接口的類應該創建NestedScrollingChildHelper並將任何View方法委託給它。調用嵌套滑動功能的View應該始終從ViewCompat,ViewGroupCompat,ViewParentCompat兼容性墊板靜態方法執行,確保Android 5.0及以上版本的嵌套滑動視圖的相護操作性。

通過AndroidStudio快捷鍵Ctrl+H,可以看到NestedScrollingChild目前的實現者:
在這裏插入圖片描述
NestedScrollingChild的實現者有兩個SwipleRefreshLayoutNestedScrollingChild2,而NestedScrollingChild2也有兩個實現者RecyclerViewNestedScrollingChild3,那麼RecyclerView也是NestedScrollingChild間接實現者。而NestedScrollingChild3的實現者是RecyclerView和NestedScrollView。
在這裏插入圖片描述
那麼來到這裏,NestedScrollingChild可以說有三個實現類,RecyclerView,NestedScrollView,SwipleRefreshLayout,上面三個控件我們都認識,都是自帶滑動的控件。

Behavior是如何響應滑動事件的?

我們在上面驗證了child之間的依賴互動關係,那麼Behavior是如果響應滑動事件的?我們需要找到挑起嵌套滑動的View,我們往CoordinatorLayout佈局中添加RecyclerView,滑動RecyclerView的內容,能產生嵌套滑動事件:

<?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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="5dp"
        android:layout_marginTop="10dp"
        android:src="@mipmap/ic_gesture_down"
        app:layout_behavior="com.antiphon.recyclerviewdemo.weight.MyBehavior" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginLeft="50dp" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

Behavior需要針對自身的業務邏輯進行處理,當我們滑動RecyclerView內容的時候,MyBehavior規定關聯的ImageView進行相應的位移,主要是在垂直方向上。在MyBehavior的onStartNestedScroll()做一些特別的處理:

    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild,
                                       View target, int axes, int type) {
        Log.e(TAG, "onStartNestedScroll:axes == " + axes);
        return child instanceof ImageView && axes == ViewCompat.SCROLL_AXIS_VERTICAL;
    }

只要child是ImageView並且滑動方向是垂直方向,返回true響應後續的嵌套滑動事件,針對滑動事件返回的位移對child進行操作:

    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx,
                                  int dy, int[] consumed, int type) {
        Log.e(TAG, "onNestedScrollAccepted:dx == " + dx + " | dy == " + dy + " | type == " + type);
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
        ViewCompat.offsetTopAndBottom(child, dy);//讓child進行垂直方向移動
    }

複寫onNestedPreScroll()方法,通過讀取dy值,讓child進行垂直方向移動。dx是滑動水平方向的位移,dy是滑動垂直方向的位移,它是在滑動事件滑動onNestedScroll()之前調用,然後把消耗的距離傳遞給consumed數組中。而onNestedScroll()是滑動事件時調用,它的參數包括位移信息,以及已經在onNestedPreScroll()消耗過的位移數。實現onNestedPreScroll()方法就可以了。

    @Override
    public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
        return false;
//        return dependency instanceof DependencyView;
    }

將MyBehavior之前做的一些處理,將它與DependencyView接觸依賴。效果如下:

這裏還有兩個和慣性滑動相關的API:

    public boolean onNestedFling(CoordinatorLayout coordinatorLayout, View child, View target,
                                 float velocityX, float velocityY, boolean consumed) {
        return false;
    }

    public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target,
                                    float velocityX, float velocityY) {
        return false;
    }

Fling慣性滑動,RecyclerView和NestedScrollView快速滑動的時候,手指停下來的時候,滑動操作並沒有停止,還會滑動一段距離。同樣我們在Fling動作即將發生的時候,通過onNestedPreFling()如果返回true則會攔截這次Fling動作,表明響應中的child自己處理這裏fling事件,那麼RecyclerView反而操作不了這個動作,因爲child消耗了這個fling事件。

我們驗證一下,將MyBehavior響應fling事件的時候,如果滑動向下,則ImageView放大,滑動向上,則ImageView縮小。

    @Override
    public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target,
                                    float velocityX, float velocityY) {
        Log.e(TAG, "onNestedScrollAccepted:velocityX == " + velocityX + " | velocityY == " + velocityY);
        if (velocityY > 0) {//向下慣性滑動
            child.animate().scaleX(2f).scaleY(2f).setDuration(2000).start();
        } else {//向上慣性滑動
            child.animate().scaleX(1f).scaleY(1f).setDuration(2000).start();
        }
        return false;
//        return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
    }

效果如下:

Behavior的總結

1.確定CoordinatorLayout中View與View之間的關係,通過layoutDependsOn()方法返回true則表示依賴,否則不依賴;
2.當一個被依賴項dependency尺寸或者位置發生變化時,依賴方會通過Behavior獲取到,然後在onDependentViewChanged()中處理,如果方法中child的尺寸或者位置發生了變化,則需要返回true;
3.當Behavior中的View準備響應嵌套滑動時,它不需要通過layoutDependsOn()來進行依賴綁定,只需要在onStartNestedScroll()通過返回值告訴ViewParent,是否開啓嵌套滑動功能,返回true後續的嵌套滑動事件才能響應。
4.嵌套滑動包括普通滑動(scroll)和慣性滑動(fling)兩種。

前面文章我有嘗試找出誰能產生嵌套滑動事件,結果發現他需要是NestedScrollChild對象,但是NestedScrollChild在調用startNestedScroll()方法時,它需要藉助它父View的力量,只有父View的startNestedScroll()返回true的時候,它的後續事件才能延續下去。

View.java

   public boolean startNestedScroll(int axes) {
        if (hasNestedScrollingParent()) {
            // Already in progress
            return true;
        }
        if (isNestedScrollingEnabled()) {
            ViewParent p = getParent();
            View child = this;
            while (p != null) {
                try {
                    if (p.onStartNestedScroll(child, this, axes)) {
                        mNestedScrollingParent = p;
                        p.onNestedScrollAccepted(child, this, axes);
                        return true;
                    }
                }
                if (p instanceof View) {
                    child = (View) p;
                }
                p = p.getParent();
            }
        }
        return false;
    }

可以看到ViewParent充當了非常重要的角色,回調了父View的onStartNestedScroll()方法:

public interface ViewParent {
  
    public void requestLayout();

    public ViewParent getParent();

    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);

    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);

    public void onStopNestedScroll(View target);
    
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed);

    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);

    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);

    public boolean onNestedPreFling(View target, float velocityX, float velocityY);
}

ViewParent是一個接口,常見的實現類是ViewGroup,它提供了嵌套滑動的相關API,實在安卓5.0才加進去的,如果要兼容的話需要分析ViewParentCompat這個類,它爲View以及它的父類提供了執行嵌套滑動的初始配置的機會,如果有這個方法的實現則應該調用父類來實現這個方法:

    public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
            int nestedScrollAxes, int type) {
        if (parent instanceof NestedScrollingParent2) {
            // First try the NestedScrollingParent2 API
            return ((NestedScrollingParent2) parent).onStartNestedScroll(child, target,
                    nestedScrollAxes, type);
        } else if (type == ViewCompat.TYPE_TOUCH) {
            // Else if the type is the default (touch), try the NestedScrollingParent API
            if (Build.VERSION.SDK_INT >= 21) {//5.0及以上
                try {
                    return parent.onStartNestedScroll(child, target, nestedScrollAxes);
                } catch (AbstractMethodError e) {
                    Log.e(TAG, "ViewParent " + parent + " does not implement interface "
                            + "method onStartNestedScroll", e);
                }
            } else if (parent instanceof NestedScrollingParent) {//5.0以下
                return ((NestedScrollingParent) parent).onStartNestedScroll(child, target,
                        nestedScrollAxes);
            }
        }
        return false;
    }

可以看到在安卓5.0版本以下,如果ViewParent需要響應嵌套滑動事件,則需要保證自己是一個NestedScrollingParent對象:

public interface NestedScrollingParent {
    
    boolean onStartNestedScroll(View child, View target, int axes);

    void onNestedScrollAccepted(View child, View target, int axes);

    void onStopNestedScroll(View target);

    void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);

    void onNestedPreScroll(View target, int dx, int dy, int[] consumed);

    boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);

    boolean onNestedPreFling(View target, float velocityX, float velocityY);

    int getNestedScrollAxes();
}

NestedScrollingParent的實現類有以下幾個:
在這裏插入圖片描述
因爲NestedScrollingParent2也是NestedScrollingParent的一個實現類,CoordinatorLayout間接實現了NestedScrollingParent,所以有四個實現了類:CoordinatorLayout、NestedScrollView、SwipeRefreshLayout、ActionBarOverlayLayout。所以CoordinatorLayout之所以能處理嵌套滑動事件,是因爲它本身就是一個NestedScrollingParent。

嵌套滑動的流程:
RecyclerView產生的嵌套滑動事件響應Behavior的相關嵌套滑動事件方法,接着響應CoordinatorLayout 的onStartNestedScroll()等嵌套滑動方法。

總的來說一個嵌套滑動事件的起始,它是由一個NestedScrollingChild發起,通過向上遍歷parent,藉助parent的嵌套滑動相關方法來完成交互。注意安卓5.0以下版本,parent要保證是一個NestedScrollingParent對象。

三、RecyclerView的嵌套滑動事件

<未完待續,後續會抓緊時間補上>


請尊重原創者版權,轉載請標明出處:https://blog.csdn.net/m0_37796683/article/details/105065358 謝謝!

相關文章:

理解RecyclerView(五)

 ● RecyclerView的繪製流程

理解RecyclerView(六)

 ● RecyclerView的滑動原理

理解RecyclerView(七)

 ● RecyclerView的嵌套滑動機制

理解RecyclerView(八)

 ● RecyclerView的回收複用緩存機制詳解

理解RecyclerView(九)

 ● RecyclerView的自定義LayoutManager

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