查看原文
其他

嵌套滑动通用解决方案--NestedScrollingParent2

胡飞洋 胡飞洋 2022-07-18

之前写过一篇嵌套滑动--NestedScroll-项目实例(淘宝首页缺陷),及CoordinatorLayout 和 AppbarLayout 联动原理,比较了淘宝和京东首页的滑动效果,分析了效果呈现差别的原因,给出了大致的解决方案。
当时没有给出demo,只有代码片段,可能导致阅读起来不很清晰,所以这篇就专门再来详细分析相关知识,给出通用的嵌套滑动的解决方案,且附上GitHub的Demo。


本文相关代码Demo Github地址https://github.com/hufeiyang/demo01/tree/master/app/src/main/java/com/hfy/demo01/module/home/touchevent

一、问题及解决方案

先来看一张图:

京东首页

这是京东的首页,忽略顶部和顶部,大致理解视图结构就是:最外层为多布局的RecyclerView,最后一个item是tabLayout+ViewPager,ViewPager的每个fragment内也是RecyclerView。这是电商App首页常用的布局方式。


再来看下滑动起来的效果图:

京东首页

可见,在向上滑动页面时,当tabLayout滑动到顶部时,外层RecyclerView停止滑动,此时tabLayout即为吸顶状态,接着会 滑动ViewPager中的内层RecyclerView。向下滑动时,如果tabLayout是吸顶状态,那么会先滑动内层RecyclerView,然后再滑外层RecyclerView。


那么,如果我们 直接 按上述布局结构来实现,会是京东这种效果吗?答案是否定的,效果如下:

按分析的view结构直接实现

可见,在tabLayout是吸顶状态,无法继续滑动内层RecyclerView(抬起手指继续滑也不行)。(点击查看相关代码


那么该咋办呢?根据滑动冲突的相关知识,我们知道一定是外层RecyclerView拦截了触摸事件,内层RecyclerView无法获取事件,就无法滑动了。那么是否可以在tabLayout吸顶时,外层不要拦截事件,从而内层RecyclerView获取事件进而滑动呢?

这是可行的,但是在tabLayout滑动到顶部后,必须抬起手指,重新滑动,内层RecyclerView才能继续滑动。这是为啥呢?开头提到的博客中有说明:

从view事件分发机制 我们知道,当parent View拦截事件后,那同一事件序列的事件会直接都给parent处理,子view不会接受事件了。所以按照正常处理滑动冲突的思路处理--当tab没到顶部时,parent拦截事件,tab到顶部时 parent就不拦截事件,但是由于手指没抬起来,所以这一事件序列还是继续给parent,不会到内部RecyclerView,所以商品流就不会滑动了。

解决方案只能是嵌套滑动布局了。代码如下:

1<?xml version="1.0" encoding="utf-8"?>
2<com.hfy.demo01.module.home.touchevent.view.NestedScrollingParent2LayoutImpl3 xmlns:android="http://schemas.android.com/apk/res/android"
3    android:id="@+id/nested_scrolling_parent2_layout"
4    android:layout_width="match_parent"
5    android:layout_height="match_parent">
6
7    <androidx.recyclerview.widget.RecyclerView
8        android:id="@+id/recyclerView_parent"
9        android:layout_width="match_parent"
10        android:layout_height="match_parent" />
11
12</com.hfy.demo01.module.home.toucheve

看到我们把外层RecyclerView的根布局换成了NestedScrollingParent2LayoutImpl3,运行后发现确实解决了上述问题,滑动效果同京东一致。
那NestedScrollingParent2LayoutImpl3这是啥呢?NestedScrollingParent2LayoutImpl3是继承NestedScrollingParent2的LinearLayout,用于处理上述嵌套滑动带来的问题。(点击查看NestedScrollingParent2LayoutImpl3的实现)

效果如下:

嵌套滑动实现效果

如果不关心原理及实现,到这了就结束了,因为NestedScrollingParent2LayoutImpl3就可以解决以上问题。

二、NestedScrollingParent2LayoutImpl3的实现原理

2.1 先来回顾下嵌套滑动机制。

如果还不了解嵌套滑动以及NestedScrollingParent2,建议先阅读此篇博客自定义View事件之进阶篇(一)-NestedScrolling(嵌套滑动)机制,再接着往下阅读。

NestedScrolling(嵌套滑动)机制,简单说来就是:产生嵌套滑动的子view,在滑动前,先询问 嵌套滑动对应的父view 是否优先处理 事件、以及消费多少事件,然后把消费后剩余的部分 继续给到 子view。 可以理解为一个事件序列分发两次。产生嵌套滑动的子view要实现接口NestedScrollingChild2、父view要实现接口NestedScrollingParent2。

常用的RecyclerView就是实现了NestedScrollingChild2,而NestedScrollView则是既实现了NestedScrollingChild2又实现了NestedScrollingParent2。

通常我们要自行手动处理的就是RecyclerView作为嵌套滑动子view的情况。NestedScrollView一般直接作为根布局用来解决嵌套滑动。

2.2 再来看看NestedScrollView嵌套RecyclerView

关于NestedScrollView嵌套RecyclerView的情况,即头部和列表可以一起滑动。如下图:

NestedScrollView嵌套RecyclerView

参考这篇实名反对《阿里巴巴Android开发手册》中NestedScrollView嵌套RecyclerView的用法。从此篇文章分析结论得知,NestedScrollView嵌套RecyclerView虽然可以实现效果,但是RecyclerView会瞬间加载所有item,RecyclerView失去的view回收的特性。作者最后建议使用RecyclerView多布局。
但其实在真实应用中,可能 头部 和 列表 的数据来自不同的接口,当列表的数据请求失败时要展示缺省图,但头部还是会展示。这时头部和列表 分开实现 是比较好的选择。

这里给出解决方案:

1<?xml version="1.0" encoding="utf-8"?>
2<com.hfy.demo01.module.home.touchevent.view.NestedScrollingParent2LayoutImpl2 xmlns:android="http://schemas.android.com/apk/res/android"
3    xmlns:tools="http://schemas.android.com/tools"
4    android:layout_width="match_parent"
5    android:layout_height="match_parent"
6    android:orientation="vertical">
7
8    <TextView
9        android:id="@+id/tv_head"
10        android:layout_width="match_parent"
11        android:layout_height="200dp"
12        android:background="@color/colorAccent"
13        android:gravity="center"
14        android:padding="15dp"
15        android:text="我是头部。 最外层是NestedScrollingParent2LayoutImpl2"
16        android:textColor="#fff"
17        android:textSize="20dp" />
18
19    <androidx.recyclerview.widget.RecyclerView
20        android:id="@+id/recyclerView"
21        android:layout_width="match_parent"
22        android:layout_height="match_parent"
23        android:background="@color/design_default_color_primary" />
24
25</com.hfy.demo01.module.home.touchevent.view.NestedScrollingParent2LayoutImpl2>

NestedScrollingParent2LayoutImpl2同样是实现了NestedScrollingParent2。(点击查看NestedScrollingParent2LayoutImpl2的实现

效果如下,可见滑动流畅,临界处不用抬起手指重新滑,且查看日志不是一次加载完item。

嵌套滑动

先看下NestedScrollingParent2LayoutImpl2的实现,要简单一些,接着再看NestedScrollingParent2LayoutImpl3实现原理,整体思路是一致的。

1/**
2 * 处理 header + recyclerView
3 * Description:NestedScrolling2机制下的嵌套滑动,实现NestedScrollingParent2接口下,处理fling效果的区别
4 *
5 */

6public class NestedScrollingParent2LayoutImpl2 extends NestedScrollingParent2Layout implements NestedScrollingParent2 {
7
8
9    private View mTopView;
10    private View mRecylerVIew;
11
12    private int mTopViewHeight;
13
14
15    public NestedScrollingParent2LayoutImpl2(Context context) {
16        this(context, null);
17    }
18
19    public NestedScrollingParent2LayoutImpl2(Context context, @Nullable AttributeSet attrs) {
20        this(context, attrs, 0);
21    }
22
23    public NestedScrollingParent2LayoutImpl2(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
24        super(context, attrs, defStyleAttr);
25        setOrientation(VERTICAL);
26    }
27
28
29    @Override
30    public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) {
31        return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
32    }
33
34
35    /**
36     * 在嵌套滑动的子View未滑动之前,判断父view是否优先与子view处理(也就是父view可以先消耗,然后给子view消耗)
37     *
38     * @param target   具体嵌套滑动的那个子类
39     * @param dx       水平方向嵌套滑动的子View想要变化的距离
40     * @param dy       垂直方向嵌套滑动的子View想要变化的距离 dy<0向下滑动 dy>0 向上滑动
41     * @param consumed 这个参数要我们在实现这个函数的时候指定,回头告诉子View当前父View消耗的距离
42     *                 consumed[0] 水平消耗的距离,consumed[1] 垂直消耗的距离 好让子view做出相应的调整
43     * @param type     滑动类型,ViewCompat.TYPE_NON_TOUCH fling效果,ViewCompat.TYPE_TOUCH 手势滑动
44     */

45    @Override
46    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
47        //这里不管手势滚动还是fling都处理
48        boolean hideTop = dy > 0 && getScrollY() < mTopViewHeight;
49        boolean showTop = dy < 0 && getScrollY() >= 0 && !target.canScrollVertically(-1);
50        if (hideTop || showTop) {
51            scrollBy(0, dy);
52            consumed[1] = dy;
53        }
54    }
55
56
57    @Override
58    public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
59        //当子控件处理完后,交给父控件进行处理。
60        if (dyUnconsumed < 0) {
61            //表示已经向下滑动到头
62            scrollBy(0, dyUnconsumed);
63        }
64    }
65
66    @Override
67    public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) {
68        return false;
69    }
70
71
72    @Override
73    public boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean consumed) {
74        return false;
75    }
76
77    @Override
78    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
79        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
80
81        //这里修改mRecylerVIew的高度为屏幕高度,否则底部会出现空白。(因为scrollTo方法是滑动子view,就把mRecylerVIew滑上去了)
82        ViewGroup.LayoutParams layoutParams = mRecylerVIew.getLayoutParams();
83        layoutParams.height = getMeasuredHeight();
84        mRecylerVIew.setLayoutParams(layoutParams);
85        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
86    }
87
88    @Override
89    protected void onFinishInflate() {
90        super.onFinishInflate();
91        mTopView = findViewById(R.id.tv_head);
92        mRecylerVIew = findViewById(R.id.recyclerView);
93        if (!(mRecylerVIew instanceof RecyclerView)) {
94            throw new RuntimeException("id RecyclerView should be RecyclerView!");
95        }
96    }
97
98    @Override
99    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
100        super.onSizeChanged(w, h, oldw, oldh);
101        mTopViewHeight = mTopView.getMeasuredHeight();
102    }
103
104    @Override
105    public void scrollTo(int x, int y) {
106        if (y < 0) {
107            y = 0;
108        }
109        if (y > mTopViewHeight) {
110            y = mTopViewHeight;
111        }
112        super.scrollTo(x, y);
113    }
114
115}

主要就是再onNestedPreScroll中对临界处做了处理:滑动RecyclerView时先滑动根布局,使得头部隐藏或显示,然后再交给RecyclerView滑动。

2.3 NestedScrollingParent2LayoutImpl3的实现原理

代码如下

1/**
2 * 处理RecyclerView 套viewPager, viewPager内的fragment中 也有RecyclerView,处理外层、内层 RecyclerView的嵌套滑动问题
3 * 类似淘宝、京东首页
4 *
5 */

6public class NestedScrollingParent2LayoutImpl3 extends NestedScrollingParent2Layout {
7
8    private final String TAG = this.getClass().getSimpleName();
9
10    private RecyclerView mParentRecyclerView;
11
12
13    private RecyclerView mChildRecyclerView;
14
15    private View mLastItemView;
16
17
18    public NestedScrollingParent2LayoutImpl3(Context context) {
19        super(context);
20    }
21
22    public NestedScrollingParent2LayoutImpl3(Context context, @Nullable AttributeSet attrs) {
23        super(context, attrs);
24    }
25
26    public NestedScrollingParent2LayoutImpl3(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
27        super(context, attrs, defStyleAttr);
28        setOrientation(VERTICAL);
29    }
30
31
32    /**
33     * 有嵌套滑动到来了,判断父view是否接受嵌套滑动
34     *
35     * @param child            嵌套滑动对应的父类的子类(因为嵌套滑动对于的父View不一定是一级就能找到的,可能挑了两级父View的父View,child的辈分>=target)
36     * @param target           具体嵌套滑动的那个子类
37     * @param nestedScrollAxes 支持嵌套滚动轴。水平方向,垂直方向,或者不指定
38     * @param type             滑动类型,ViewCompat.TYPE_NON_TOUCH fling 效果ViewCompat.TYPE_TOUCH 手势滑动
39     */

40    @Override
41    public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int nestedScrollAxes, int type) {
42        //自己处理逻辑
43        //这里处理是接受 竖向的 嵌套滑动
44        return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL;
45    }
46
47    /**
48     * 在嵌套滑动的子View未滑动之前,判断父view是否优先与子view处理(也就是父view可以先消耗,然后给子view消耗)
49     *
50     * @param target   具体嵌套滑动的那个子类,就是手指滑的那个 产生嵌套滑动的view
51     * @param dx       水平方向嵌套滑动的子View想要变化的距离
52     * @param dy       垂直方向嵌套滑动的子View想要变化的距离 dy<0向下滑动 dy>0 向上滑动
53     * @param consumed 这个参数要我们在实现这个函数的时候指定,回头告诉子View当前父View消耗的距离
54     *                 consumed[0] 水平消耗的距离,consumed[1] 垂直消耗的距离 好让子view做出相应的调整
55     * @param type     滑动类型,ViewCompat.TYPE_NON_TOUCH fling 效果ViewCompat.TYPE_TOUCH 手势滑动
56     */

57    @Override
58    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
59        //自己处理逻辑
60
61        if (mLastItemView == null) {
62            return;
63        }
64
65        int lastItemTop = mLastItemView.getTop();
66
67        if (target == mParentRecyclerView) {
68            handleParentRecyclerViewScroll(lastItemTop, dy, consumed);
69        } else if (target == mChildRecyclerView) {
70            handleChildRecyclerViewScroll(lastItemTop, dy, consumed);
71        }
72    }
73
74    /**
75     * 滑动外层RecyclerView时,的处理
76     *
77     * @param lastItemTop tab到屏幕顶部的距离,是0就代表到顶了
78     * @param dy          目标滑动距离, dy>0 代表向上滑
79     * @param consumed
80     */

81    private void handleParentRecyclerViewScroll(int lastItemTop, int dy, int[] consumed) {
82        //tab上边没到顶
83        if (lastItemTop != 0) {
84            if (dy > 0) {
85                //向上滑
86                if (lastItemTop > dy) {
87                    //tab的top>想要滑动的dy,就让外部RecyclerView自行处理
88                } else {
89                    //tab的top<=想要滑动的dy,先滑外部RecyclerView,滑距离为lastItemTop,刚好到顶;剩下的就滑内层了。
90                    consumed[1] = dy;
91                    mParentRecyclerView.scrollBy(0, lastItemTop);
92                    mChildRecyclerView.scrollBy(0, dy - lastItemTop);
93                }
94            } else {
95                //向下滑,就让外部RecyclerView自行处理
96            }
97        } else {
98            //tab上边到顶了
99            if (dy > 0){
100                //向上,内层直接消费掉
101                mChildRecyclerView.scrollBy(0, dy);
102                consumed[1] = dy;
103            }else {
104                int childScrolledY = mChildRecyclerView.computeVerticalScrollOffset();
105                if (childScrolledY > Math.abs(dy)) {
106                    //内层已滚动的距离,大于想要滚动的距离,内层直接消费掉
107                    mChildRecyclerView.scrollBy(0, dy);
108                    consumed[1] = dy;
109                }else {
110                    //内层已滚动的距离,小于想要滚动的距离,那么内层消费一部分,到顶后,剩的还给外层自行滑动
111                    mChildRecyclerView.scrollBy(0, -(Math.abs(dy)-childScrolledY));
112                    consumed[1] = -(Math.abs(dy)-childScrolledY);
113                }
114            }
115        }
116
117    }
118
119    /**
120     * 滑动内层RecyclerView时,的处理
121     *
122     * @param lastItemTop tab到屏幕顶部的距离,是0就代表到顶了
123     * @param dy
124     * @param consumed
125     */

126    private void handleChildRecyclerViewScroll(int lastItemTop, int dy, int[] consumed) {
127        //tab上边没到顶
128        if (lastItemTop != 0) {
129            if (dy > 0) {
130                //向上滑
131                if (lastItemTop > dy) {
132                    //tab的top>想要滑动的dy,外层直接消耗掉
133                    mParentRecyclerView.scrollBy(0, dy);
134                    consumed[1] = dy;
135                } else {
136                    //tab的top<=想要滑动的dy,先滑外层,消耗距离为lastItemTop,刚好到顶;剩下的就滑内层了。
137                    mParentRecyclerView.scrollBy(0, lastItemTop);
138                    consumed[1] = dy - lastItemTop;
139                }
140            } else {
141                //向下滑,外层直接消耗
142                mParentRecyclerView.scrollBy(0, dy);
143                consumed[1] = dy;
144            }
145        }else {
146            //tab上边到顶了
147            if (dy > 0){
148                //向上,内层自行处理
149            }else {
150                int childScrolledY = mChildRecyclerView.computeVerticalScrollOffset();
151                if (childScrolledY > Math.abs(dy)) {
152                    //内层已滚动的距离,大于想要滚动的距离,内层自行处理
153                }else {
154                    //内层已滚动的距离,小于想要滚动的距离,那么内层消费一部分,到顶后,剩的外层滑动
155                    mChildRecyclerView.scrollBy(0, -childScrolledY);
156                    mParentRecyclerView.scrollBy(0, -(Math.abs(dy)-childScrolledY));
157                    consumed[1] = dy;
158                }
159            }
160        }
161    }
162
163
164    @Override
165    protected void onFinishInflate() {
166        super.onFinishInflate();
167
168        //直接获取外层RecyclerView
169        mParentRecyclerView = getRecyclerView(this);
170        Log.i(TAG, "onFinishInflate: mParentRecyclerView=" + mParentRecyclerView);
171
172        //关于内层RecyclerView:此时还获取不到ViewPager内fragment的RecyclerView,需要在加载ViewPager后 fragment可见时 传入
173    }
174
175    private RecyclerView getRecyclerView(ViewGroup viewGroup) {
176        int childCount = viewGroup.getChildCount();
177        for (int i = 0; i < childCount; i++) {
178            View childAt = getChildAt(i);
179            if (childAt instanceof RecyclerView) {
180                if (mParentRecyclerView == null) {
181                    return (RecyclerView) childAt;
182                }
183            }
184        }
185        return null;
186    }
187
188    /**
189     * 传入内部RecyclerView
190     *
191     * @param childRecyclerView
192     */

193    public void setChildRecyclerView(RecyclerView childRecyclerView) {
194        mChildRecyclerView = childRecyclerView;
195    }
196
197
198    /**
199     * 外层RecyclerView的最后一个item,即:tab + viewPager
200     * 用于判断 滑动 临界位置
201     *
202     * @param lastItemView
203     */

204    public void setLastItem(View lastItemView) {
205        mLastItemView = lastItemView;
206    }
207}

NestedScrollingParent2LayoutImpl3 继承自 NestedScrollingParent2Layout。NestedScrollingParent2Layout是继承自 LinearLayout implements 并实现了NestedScrollingParent2,主要处理了通用的方法实现。

1/**
2 * Description:  通用 滑动嵌套处理布局,用于处理含有{@link androidx.recyclerview.widget.RecyclerView}的嵌套套滑动
3 */

4public class NestedScrollingParent2Layout extends LinearLayout implements NestedScrollingParent2 {
5
6
7    private NestedScrollingParentHelper mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
8
9    public NestedScrollingParent2Layout(Context context) {
10        super(context);
11    }
12
13    public NestedScrollingParent2Layout(Context context, @Nullable AttributeSet attrs) {
14        super(context, attrs);
15    }
16
17    public NestedScrollingParent2Layout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
18        super(context, attrs, defStyleAttr);
19    }
20
21    /**
22     * 有嵌套滑动到来了,判断父view是否接受嵌套滑动
23     *
24     * @param child            嵌套滑动对应的父类的子类(因为嵌套滑动对于的父View不一定是一级就能找到的,可能挑了两级父View的父View,child的辈分>=target)
25     * @param target           具体嵌套滑动的那个子类
26     * @param nestedScrollAxes 支持嵌套滚动轴。水平方向,垂直方向,或者不指定
27     * @param type             滑动类型,ViewCompat.TYPE_NON_TOUCH fling 效果ViewCompat.TYPE_TOUCH 手势滑动
28     */

29    @Override
30    public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int nestedScrollAxes, int type) {
31        //自己处理逻辑
32        return true;
33    }
34
35    /**
36     * 当父view接受嵌套滑动,当onStartNestedScroll方法返回true该方法会调用
37     *
38     * @param type 滑动类型,ViewCompat.TYPE_NON_TOUCH fling 效果ViewCompat.TYPE_TOUCH 手势滑动
39     */

40    @Override
41    public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) {
42        mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes, type);
43    }
44
45    /**
46     * 在嵌套滑动的子View未滑动之前,判断父view是否优先与子view处理(也就是父view可以先消耗,然后给子view消耗)
47     *
48     * @param target   具体嵌套滑动的那个子类
49     * @param dx       水平方向嵌套滑动的子View想要变化的距离
50     * @param dy       垂直方向嵌套滑动的子View想要变化的距离 dy<0向下滑动 dy>0 向上滑动
51     * @param consumed 这个参数要我们在实现这个函数的时候指定,回头告诉子View当前父View消耗的距离
52     *                 consumed[0] 水平消耗的距离,consumed[1] 垂直消耗的距离 好让子view做出相应的调整
53     * @param type     滑动类型,ViewCompat.TYPE_NON_TOUCH fling 效果ViewCompat.TYPE_TOUCH 手势滑动
54     */

55    @Override
56    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
57        //自己处理逻辑
58    }
59
60    /**
61     * 嵌套滑动的子View在滑动之后,判断父view是否继续处理(也就是父消耗一定距离后,子再消耗,最后判断父消耗不)
62     *
63     * @param target       具体嵌套滑动的那个子类
64     * @param dxConsumed   水平方向嵌套滑动的子View滑动的距离(消耗的距离)
65     * @param dyConsumed   垂直方向嵌套滑动的子View滑动的距离(消耗的距离)
66     * @param dxUnconsumed 水平方向嵌套滑动的子View未滑动的距离(未消耗的距离)
67     * @param dyUnconsumed 垂直方向嵌套滑动的子View未滑动的距离(未消耗的距离)
68     * @param type         滑动类型,ViewCompat.TYPE_NON_TOUCH fling 效果ViewCompat.TYPE_TOUCH 手势滑动
69     */

70    @Override
71    public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
72        //自己处理逻辑
73    }
74
75    /**
76     * 嵌套滑动结束
77     *
78     * @param type 滑动类型,ViewCompat.TYPE_NON_TOUCH fling 效果ViewCompat.TYPE_TOUCH 手势滑动
79     */

80    @Override
81    public void onStopNestedScroll(@NonNull View child, int type) {
82        mNestedScrollingParentHelper.onStopNestedScroll(child, type);
83    }
84
85    @Override
86    public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) {
87        //自己判断是否处理
88        return false;
89    }
90
91    @Override
92    public boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean consumed) {
93        //自己处理逻辑
94        return false;
95    }
96
97
98    @Override
99    public int getNestedScrollAxes() {
100        return mNestedScrollingParentHelper.getNestedScrollAxes();
101    }
102
103}

实现原理主要在onNestedPreScroll方法,即嵌套滑动的子view滑动前,询问对应的父view是否优先处理,以及处理多少。

所以无论滑动外城RecyclerView还是内层RecyclerView,都会询问NestedScrollingParent2LayoutImpl3,即都会走到onNestedPreScroll方法。然后根据tabLayout的位置以及滑动的方向,决定是滑动外层RecyclerView还是滑内层,以及滑动多少。相当于一个事假序列分发了两次,避免了常规事件分发 父view拦截后子view无法处理的问题。

onNestedPreScroll中的具体处理,请看代码,有详细注释。要结合滑动实际情况去理解,便于遇到其他情况也能同样处理。

这里列出已经实现的处理三种嵌套滑动场景的方案

  • NestedScrollingParent2LayoutImpl1:处理 header + tab + viewPager + recyclerView

  • NestedScrollingParent2LayoutImpl2:处理 header + recyclerView

  • NestedScrollingParent2LayoutImpl3:处理RecyclerView 套viewPager, viewPager内的fragment中 也有RecyclerView,处理外层、内层 RecyclerView的嵌套滑动问题,类似淘宝、京东首页。

demo截图

Demo Github地址,有帮助的话Star一波吧。

(文章涉及链接可点击 “阅读原文” 查看



推荐阅读:

淘宝首页Bug!嵌套滑动及NestedScroll

View事件分发、滑动冲突 详解

带你彻底搞懂-View的工作原理!




点个在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存