理解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的相关滑动代码如下:

Behavior.java

	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:

Behavior.java

    @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:

Behavior.java

   /**
     * 如果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,后续的嵌套滑动事件才会响应。后续响应的函数指的是这几个函数:

Behavior.java

    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中被调用:

CoordinatorLayout.java

    @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是否拥有嵌套滑动的能力。

2.3 使用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接口。

2.4 想触发事件吗?你是NestedScrollingChild吗?

我们来看看NestedScrollingChild

public interface NestedScrollingChild {
  	/**
     * 启用或者禁用此View的嵌套滑动。
     * 注意:如果此属性设置为true,则允许该View使用当前层结构中兼容的父View启动嵌套滑动操作。
     * 		如果这个View没有任务嵌套滑动,则这个方法没有任何作用。
     */ 	
    void setNestedScrollingEnabled(boolean enabled);
	/**
     * 如果此View启用了嵌套滑动,则返回true。
     * 如果启动了嵌套滑动,并且这个View类实现支持嵌套滑动,那么这个View将在适用时充当嵌套滑动子View,
     * 将有关正在进行的滚动操作的数据转发到兼容且协作嵌套滑动的父View。
     */ 	
    boolean isNestedScrollingEnabled();
	/**
     * 沿着给定的方向(垂直或水平方向)开始一个可嵌套滚动的滑动操作。
     * 返回true表示能处理父View传递上去的嵌套滑动事件,实际这个方法里面调用了NestedScrollingParent的onStartNestedScroll()
     *@param axes axes表示方向
     */ 	
    boolean startNestedScroll(int axes);
	/**
     * 停止正在进行的嵌套滑动,嵌套滑动结束
     */ 
    void stopNestedScroll();
	/**
     * 如果该View有一个嵌套的滑动父View,则返回true
     */ 
    boolean hasNestedScrollingParent();

	/**
     * 调度一个正在执行的滑动步骤
     * 在产生嵌套滑动的View已经滑动完成之后调用,该方法的作用是将剩余没有消耗的距离继续分发到View里面去
     * @param dxConsumed 表示该View在x轴上消耗的距离
     * @param dyConsumed 表示该View在y轴上消耗的距离
     * @param dxUnconsumed 表示该View在x轴上未消耗的距离
     * @param dyUnconsumed 表示该View在y轴未消耗的距离
     * @param offsetInWindow 表示该该View在屏幕上滑动的距离,包括x轴上的距离和y轴上的距离
     * 返回true表示父线程消耗了部分或者全部滑动的量,false表示不消耗
     */ 
    boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,int dxUnconsumed, 
    							 int dyUnconsumed, int[] offsetInWindow);
	/**
     * 在View消耗滑动的任何部分之前调度嵌套滑动的一个步骤。为嵌套滑动中的父View提供一个机会,在子View使用滑动操作之前,使用部分或者全部滑动操作。
     * 在dispatchNestedScroll之前调用,也就是距离产生了,但是改View还没滑动。将滑动的距离报给父View,看父View是否优先消耗这部分距离
     * @param dx x轴上产生的距离
     * @param dy y轴上产生的距离
     * @param consumed index为0的值表示父View在x轴消耗的的距离,index为1的值表示父View在y轴上消耗的距离
     * @param offsetInWindow 该View在屏幕滑动的距离
     * 返回true表示父线程消耗了部分或者全部滑动的距离,false表示父View不消耗
     */ 
    boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
	/**
     * 如果父View不对惯性滑动做任何处理,那么子View会调用这个方法,作用是报告父View,子View此时正在fling
     * @param velocityX x轴上的速度
     * @param velocityY y轴的速度
     * @param consumed true表示子View对这个fling事件有所行动,false表示没有行动
     * 返回true表示嵌套滑动的父View消耗了或者以其他方式对惯性滑动做出反应,false则没有
     */ 
    boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);

	/**
     * 在视图处理之前,将一个抛投运动发送给嵌套滑动的父View
     * 在子View对fling有所行动之前,回调用这个方法,用于询问父View是否对fling有所行动
     */ 
    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,上面三个控件我们都认识,都是自带滑动的控件。

2.5 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);
    }

效果如下:

2.6 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,是在Android5.0才加进去的,如果要兼容的话需要分析ViewParentCompat这个类,它为View以及它的父类提供了执行嵌套滑动的初始配置的机会,如果有这个方法的实现则应该调用父类来实现这个方法:

ViewParentCompat.java

    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及以上
                return parent.onStartNestedScroll(child, target, nestedScrollAxes);
            } 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。

嵌套滑动的流程:
总的来说一个嵌套滑动事件的起始,它是由一个NestedScrollingChild发起,通过向上遍历parent,借助parent的嵌套滑动相关方法来完成交互。注意安卓5.0以下版本,parent必须NestedScrollingParent接口。

三、RecyclerView的嵌套滑动事件

  如果要支持嵌套滑动,子View必须实现NestedScrollingChild接口,父View必须实现NestedScrollingParent接口。而RecyclerView实现了NestedScrollingChild接口,CoordinatorLayout实现了NestedScrollingParent接口。

温馨提示:本文源码基于androidx.recyclerview:recyclerview:1.2.0-alpha01

嵌套滑动机制主要用到的接口和类:NestedScrollingChild,NestedScrollingParent,NestedScrollingParentHelper,NestedScrollingChildHelper。这里做个简单总结:

类名含义
NestedScrollingChild如果一个View想要产生滑动事件,这个View必须实现NestedScrollingChild接口, 从Android5.0开始View实现了这个接口,不需要手动实现了
NestedScrollingParent这个接口通常被ViewGroup来实现,表示能接收从子View发送过来的嵌套滑动事件
NestedScrollingChildHelper这个类通常在实现NestedScrollingChild接口的View里面辅助使用,它通常用来负责子View产生的嵌套滑动事件报告给父View。也就是说,一个子View想要把产生的嵌套滑动事件交给父View,那么NestedScrollingChildHelper会帮我们来处理。
NestedScrollingParentHelper与上述类似,都是传递事件的辅助类。通常用在实现了NestedScrollingParent接口的父View,如果父View不想处理这个事件,就通过NestedScrollingParentHelper传递

3.1 ACTION_DOWN事件

结合RecyclerView的源码来分析嵌套事件传递原理,先来看看RecyclerView的ACTION_DOWN事件的处理:

RecyclerView.java

   case MotionEvent.ACTION_DOWN: {
        int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
        if (canScrollHorizontally) {
            nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
        }
        if (canScrollVertically) {
            nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
        }
        //开始嵌套滑动
        startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
    } break;

初始化nestedScrollAxis(线性方向)变量后,就会调用startNestedScroll()告诉父View滑动事件已经开始,你是否需要有所行动,可以看到嵌套滑动事件是从下往上传递的。它是怎么将一个事件传递到父View的呢?

    @Override
    public boolean startNestedScroll(int axes, int type) {
        return getScrollingChildHelper().startNestedScroll(axes, type);
    }
    
    private NestedScrollingChildHelper getScrollingChildHelper() {
        if (mScrollingChildHelper == null) {
            mScrollingChildHelper = new NestedScrollingChildHelper(this);
        }
        return mScrollingChildHelper;
    }

事件是通过NestedScrollingChildHelper帮助类传递的,通过NestedScrollingChildHelper实例调用startNestedScroll()

NestedScrollingChildHelper.java

    public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
        if (hasNestedScrollingParent(type)) {//嵌套滑动进行中,直接返回true
            // Already in progress
            return true;
        }
        if (isNestedScrollingEnabled()) {//父View是否支持嵌套滑动
            ViewParent p = mView.getParent();
            View child = mView;
            //while循环获取上级的view,看是否支持嵌套滑动,如果支持则返回true,结束循环;不支持则继续访问上层View。
            while (p != null) {
            	//这里调用父View的方法,如果父View运行嵌套滑动则返回true,之后会消耗掉部分子View传上来的滑动距离。
                if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
                    setNestedScrollingParentForType(type, p);
                    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
                    return true;
                }
                if (p instanceof View) {
                    child = (View) p;
                }
                p = p.getParent();
            }
        }
        return false;
    }

这里首先经过hasNestedScrollingParent()来判断父View是否正在进行嵌套滑动,父View必须实现NestedScrollingParent接口,onStartNestedScroll()方法返回true,如果父View不能够处理嵌套滑动事件时,就会while()递归往上找。NestedScrollingChildHelper有依靠ViewParentCompat类帮助传递事件,实际上ViewParentCompat也帮我们调用父View的onStartNestedScroll()方法,这样做的目的是为了兼容不同的版本。前面说过,在Android5.0开始View实现了NestScrollingChild接口,但是5.0以下版本需要自己手动实现。

3.2 ViewParentCompat

事件是如何传递到父View的呢?ViewParentCompat作为系统兼容扮演着重要的角色,它是如何保证系统兼容的呢?

ViewParentCompat.java

   public static void onNestedScroll(ViewParent parent, View target, int dxConsumed,
            int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type,
            @NonNull int[] consumed) {
		//如果我们调用的是小于NestedScrollingParent3的函数,那么将未使用的距离添加到已使用的参数中,这样调用NestedScrollingChild3实现就会被告知整个滚动距离已被使用(用于向后比较)。
        if (parent instanceof NestedScrollingParent3) {
            ((NestedScrollingParent3) parent).onNestedScroll(target, dxConsumed, dyConsumed,
                    dxUnconsumed, dyUnconsumed, type, consumed);
        } else {
            if (parent instanceof NestedScrollingParent2) {
                ((NestedScrollingParent2) parent).onNestedScroll(target, dxConsumed, dyConsumed,
                        dxUnconsumed, dyUnconsumed, type);
            } else if (type == ViewCompat.TYPE_TOUCH) {
                //如果类型是默认的(触摸),尝试NestedScrollingParent API
                if (Build.VERSION.SDK_INT >= 21) {
                    parent.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed,
                                dyUnconsumed);
                } else if (parent instanceof NestedScrollingParent) {
                    ((NestedScrollingParent) parent).onNestedScroll(target, dxConsumed, dyConsumed,
                            dxUnconsumed, dyUnconsumed);
                }
            }
        }
    }

其中NestedScrollingParent3继承自NestedScrollingParent2NestedScrollingParent2继承自NestedScrollingParent,如果parent实现了NestedScrollingParent3的接口则调用NestedScrollingParent3中的onNestedScroll();如果parent实现了NestedScrollingParent2的接口则调用NestedScrollingParent2中的onNestedScroll();如果parent实现了NestedScrollingParent的接口则调用NestedScrollingParent中的onNestedScroll();如果版本在Android5.0以上则直接调用parent的onNestedScroll()

该方法是对正在进行的嵌套滑动做出响应,将滚动距离的已使用部分和未使用部分报告给ViewParent。所以一个父View想要接收子View传递过来的事件,就的实现NestedScrollingParent等相关接口。

3.3 ACTION_MOVE事件

ACTION_DOWN中涉及的嵌套滑动流程就是NestedScrollingChildHelper初始化,寻找支持嵌套滑动的父级View,接下来在ACTION_MOVE中将滑动的距离首先交给这个找到的父View,父View会将消耗的滑动距离写入到传入的参数中,之后RecycleView会知道滑动的距离是否被父View消耗掉,来决定RecycleView的滑动是否更新。

RecyclerView.java

    case MotionEvent.ACTION_MOVE:{
        if (dispatchNestedPreScroll(
                canScrollHorizontally ? dx : 0,
                canScrollVertically ? dy : 0,
                mReusableIntPair, mScrollOffset, TYPE_TOUCH
        )) 
    } break;
    
    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,
            int type) {
        return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow,
                type);
    }

主要是通过ScrollingChildHelper类调用dispatchNestedPreScroll()方法:

ScrollingChildHelper.java

   public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
            @Nullable int[] offsetInWindow, @NestedScrollType int type) {
        if (isNestedScrollingEnabled()) {
        	//之前嵌套滑动的父View不匹配就直接返回
            final ViewParent parent = getNestedScrollingParentForType(type);
            if (parent == null) {
                return false;
            }

            if (dx != 0 || dy != 0) {
                int startX = 0;
                int startY = 0;
                //offsetInWindow为非空就将view的坐标写入数组中
                if (offsetInWindow != null) {
                    mView.getLocationInWindow(offsetInWindow);
                    startX = offsetInWindow[0];
                    startY = offsetInWindow[1];
                }

                if (consumed == null) {
                    consumed = getTempNestedScrollConsumed();
                }
                consumed[0] = 0;
                consumed[1] = 0;
                //回调父View相关方法,将消耗的数据写入consumed数组中
                ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);

                if (offsetInWindow != null) {
                    mView.getLocationInWindow(offsetInWindow);
                    offsetInWindow[0] -= startX;
                    offsetInWindow[1] -= startY;
                }
                //父View消耗了dy或者dx就返回true
                return consumed[0] != 0 || consumed[1] != 0;
            } else if (offsetInWindow != null) {
                offsetInWindow[0] = 0;
                offsetInWindow[1] = 0;
            }
        }
        return false;
    }

3.4 ACTION_UP事件

ACTION_UP根据手指抬起的X,Y抬起的速度来判断是否需要执行惯性滑动fling()

RecyclerView.java

    case MotionEvent.ACTION_UP: {
        final float yvel = canScrollVertically
                ? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
        if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
            setScrollState(SCROLL_STATE_IDLE);
        }
        resetScroll();
    } break;

主要方法是在fling()中:

    public boolean fling(int velocityX, int velocityY) {
		······
		//如果父View未消耗本次的fling
        if (!dispatchNestedPreFling(velocityX, velocityY)) {
            final boolean canScroll = canScrollHorizontal || canScrollVertical;
            //嵌套的惯性滑动
            dispatchNestedFling(velocityX, velocityY, canScroll);

            if (mOnFlingListener != null && mOnFlingListener.onFling(velocityX, velocityY)) {
                return true;
            }
			//如果还可以滚动,就使用recyclerview自己的机制来实现一段惯性滑动
            if (canScroll) {
                int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
                if (canScrollHorizontal) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
                }
                if (canScrollVertical) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
                }
                startNestedScroll(nestedScrollAxis, TYPE_NON_TOUCH);

                velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
                velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
                //根据计算速度实现惯性滑动
                mViewFlinger.fling(velocityX, velocityY);
                return true;
            }
        }
        return false;
    }

这里如果父View的惯性滑动尚未分发的话就dispatchNestedFling()就将嵌套的fling操作分发给父View。

NestedScrollingChildHelper.java

    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        if (isNestedScrollingEnabled()) {
        	//根据类型获取parent
            ViewParent parent = getNestedScrollingParentForType(TYPE_TOUCH);
            if (parent != null) {
                return ViewParentCompat.onNestedFling(parent, mView, velocityX,
                        velocityY, consumed);
            }
        }
        return false;
    }

最后调用ViewParentCompat的onNestedFling()方法:

ViewParentCompat.java

    public static boolean onNestedFling(ViewParent parent, View target, float velocityX,
            float velocityY, boolean consumed) {
        if (Build.VERSION.SDK_INT >= 21) {
            return parent.onNestedFling(target, velocityX, velocityY, consumed);
        } else if (parent instanceof NestedScrollingParent) {
            return ((NestedScrollingParent) parent).onNestedFling(target, velocityX, velocityY,
                    consumed);
        }
        return false;
    }

如果在Android5.0以上则直接调用父view的onNestedFling()方法,如果在在Android5.0以下则调用父View实现的NestedScrollingParent接口中的onNestedFling()方法。(有关RecyclerView的惯性滑动相关原理可以参考理解RecyclerView(六))

画图总结RecyclerViewonTouchEvent()的嵌套滑动流程:
在这里插入图片描述
简单总结RecyclerView嵌套滑动:
1.嵌套滑动事件由子View传递给父View,从下到上分发;
2.如果一个view想要传递嵌套滑动事件,实现NestedScrollingChild接口,并且支持嵌套滑动;如果父View想要支持嵌套滑动事件,必须实现NestedScrollingParent接口。

四、总结

1.CoordinatorLayout是一个普通的ViewGroup,也是一个超级FrameLayout,能通过Behavior与child交互;

2.在Behavior中的layoutDepentOn()返回true或者通过xml指定目标控件来确定两个View的依赖关系,当依赖方的尺寸和位置发生变化时,Behavior中onDependentViewChanged()就会被调用,如果改变了主动依赖的View的尺寸和位置,方法返回true;

3.Behavior是一种插件机制,可以决定CoordinatorLayout对应childview的测量尺寸,布局位置,触摸响应;如果没有Behavior的存在CoordinatorLayout与普通的FrameLayout没有区别;

4.嵌套滑动分为nested scroll和 fling 两种,childView是否接受响应由onStartNestedScroll()返回值决定,一般在startNestedScroll()处理相应的nested scroll事件,在onNestedFling()处理fling事件;

5.RecyclerView能够产生嵌套滑动事件是因为实现了NestedScrollingChild接口,CoordinatorLayout能够响应嵌套滑动事件是因为实现了NestedScrollingParent接口;由childView发起,通过由下向上循环遍历parent,借助parent的嵌套滑动相关方法来完成交互。

  到这里一个嵌套滑动的起始才彻底水落石出,RecyclerView实现NestedScrollingChild接口,发起嵌套滑动时(Android5.0以下需要setNestedScrollEnable(true)),由下向上循环遍历自己的parent,parent实现NestedScrollingParent接口,parent调用onStartNestedScroll(),进而调用CoordinatorLayout 的onStartNestedScroll()方法,CoordinatorLayout里面其实调用了Behavior中的onStartNestedScroll(),告知ViewParent是否对嵌套滑动事件感兴趣,如果返回true,CoordinatorLayout 中的嵌套滑动事件才会被响应。(Android5.0以下parent必须实现NestedScrollingParent接口 )

用一张图来概念更加清晰:
在这里插入图片描述
源码地址

点关注,不迷路


好了各位,以上就是这篇文章的全部内容了,能看到这里的人呀,都是人才

我是suming,感谢各位的支持和认可,您的点赞、评论、收藏【一键三连】就是我创作的最大动力,我们下篇文章见!

如果本篇博客有任何错误,请批评指教,不胜感激 !

要想成为一个优秀的安卓开发者,这里有必须要掌握的知识架构,一步一步朝着自己的梦想前进!Keep Moving!

相关文章:

理解RecyclerView(五)

 ● RecyclerView的绘制流程

理解RecyclerView(六)

 ● RecyclerView的滑动原理

理解RecyclerView(七)

 ● RecyclerView的嵌套滑动机制

理解RecyclerView(八)

 ● RecyclerView的回收复用缓存机制详解

理解RecyclerView(九)

 ● RecyclerView的自定义LayoutManager

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值