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

时间:2022-07-22
本文章向大家介绍淘宝首页Bug!嵌套滑动及NestedScroll,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

刚复习完View事件分发、滑动冲突--《Android开发艺术探索》阅读笔记——第三章part2,接着想起前段时间项目中首页重构,遇到的嵌套滑动问题,以及CoordinatorLayout 和 AppbarLayout 联动原理。去学习了下相关知识。在此记录一下,备忘~

学习嵌套滑动的相关文章: 自定义View事件之进阶篇(一)-NestedScrolling(嵌套滑动)机制. Android NestedScrolling机制完全解析 带你玩转嵌套滑动

一、项目实例--电商首页

1、嵌套滑动的问题点

看懂了以上文章后,现在来分享一下项目中的问题。因为公司项目同为电商,也恰好看了淘宝、京东的首页,就拿它俩举例吧。

通常首页都是一个RecyclerView,然后底部是Tab+frangment(内部recyclerview)组成的瀑布流商品---- 一起作为外部RecyclerView的最后一个item,很多电商都是这样。分别看下淘宝、京东的 外部RecyclerView(整个首页列表)、内部RecyclerView(底部tab中的商品流列表) 嵌套时的滑动效果。

在这里插入图片描述京东 VS 淘宝

可以清楚看到

京东:滑动很顺畅,没有停滞的情况,tab到顶部后就 紧接着 滑动内部商品列表了。整个过程手指是连续拖动的,没有抬起。

淘宝:在tab滑到顶部后,手指继续拖动,但商品流是不能滑动的。这时手指抬起然后再次拖动商品流 才会滑动。

很显然,我们认为京东的滑动更丝滑。那为啥淘宝会出现这个情况呢?

2、缺陷原因分析

原因分析:从view事件分发机制 我们知道,当parent View拦截事件后,那同一事件序列的事件会直接都给parent处理,子view不会接受事件了所以 按照正常处理滑动冲突的思路处理----当tab没到顶部时,parent拦截事件,tab到顶部时 parent就不拦截事件,但是由于手指没抬起来,所以这一事件序列还是继续给parent,不会到内部RecyclerView,所以商品流就不会滑动了。(这里不清楚的可以参考View事件分发、滑动冲突--《Android开发艺术探索》阅读笔记——第三章part2)

解决方案使用嵌套滑动,具体如下。

1、添加嵌套滑动父布局

<*.NestedScrollLayout2
            android:id="@+id/nest_scroll_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <android.support.v7.widget.RecyclerView
                android:id="@+id/orv_main_page"
                android:layout_width="match_parent"
                android:layout_height="match_parent" />

        </NestedScrollLayout2>

外部RecyclerView加上了父布局,NestedScrollLayout2,NestedScrollLayout2是实现==NestedScrollingParent2==接口的,这个很重要。

2、嵌套滑动父布局的实现原理

NestedScrollLayout2 extends FrameLayout implements NestedScrollingParent2

上面说了,实现==NestedScrollingParent2==接口很重要,目的就是 在 开始滑动 外部RecyclerView 时、开始滑动内部RecyclerView时,都询问NestedScrollLayout2是否处理且如何处理。

所以,根据我们的问题,在向上滑动内部RecyclerView时,如果tab没到顶就让parent消费事件,且滑动外部RecyclerView;到顶了,就滑内部RecyclerView。相对的, 向下滑动内部RecyclerView时,如果还能滑就滑内部RecyclerView;如果已经滑到顶部就让parent去滑动外部RecyclerView。

在滑外部外部RecyclerView时,也是一样逻辑。 具体看代码,这里贴NestedScrollLayout2关键代码,有注释说明,就是对上面文字的代码实现而已。其中mRootList是外部RecyclerView,mChildList是内部RecyclerView,childTop是tab这个view的top 用于判断是否到顶部。scrollListener是监听tab到顶部后设置其背景色用的。主要关注调用scrollBy时滚动的是哪个列表,滚动了多少。 (如果阅读这段理解不清晰,建议再去看上面提的嵌套滑动文章)

public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
            int type) {
        if (mChildView != null) {
            //target就是接受到事件的child,这里就是parent接受嵌套滑动后的pre处理
            if (target == mRootList) {
                //滑外部列表
                onParentScrolling(mChildView.getTop(), dy, consumed);
            } else {
                //滑内部列表
                onChildScrolling(mChildView.getTop(), dy, consumed);
            }
        }
    }


    /**
     * 父列表在滑动
     */
    private void onParentScrolling(int childTop, int dy, int[] consumed) {
        //列表已经置顶
        if (childTop == 0) {
            if (!isTabsTop) {
                isTabsTop = true;
                if (scrollListener != null) {
                    scrollListener.onTabsStateChanged(isTabsTop, mChildView);
                }
            }
            if (dy > 0 && mChildList != null) {
                consumed[1] = dy;
                if (!mChildList.canScrollVertically(dy)) {
                    //正在loading的时候不要响应上滑事件
                    RecyclerView.Adapter adapter = mChildList.getAdapter();
                    if (adapter instanceof BaseQuickAdapter) {
                        BaseQuickAdapter quickAdapter = (BaseQuickAdapter) adapter;
                        if (quickAdapter.isLoading()) {
                            mRootList.stopScroll();
                        }
                    }
                    if (!isBottom) {
                        isBottom = true;
                        if (scrollListener != null) {
                            scrollListener.onReachBottom(mChildList, mChildView);
                        }
                    }
                } else {
                    //还在向下滑动,此时滑动子列表
                    scrollBy(dy, mChildList);
                }
            } else {
                if (mChildList != null && mChildList.canScrollVertically(dy)) {
                    consumed[1] = dy;
                    scrollBy(dy, mChildList);
                }
            }
        } else {
            if (childTop < dy) {
                //tab没有置顶,parent就消耗下面这个差值,所以父列表就只能滑动childTop了。到顶后,就是上面的逻辑了。
                //childTop是tab到顶部的距离。
                consumed[1] = dy - childTop;
            }
            if (isTabsTop) {
                isTabsTop = false;
                if (scrollListener != null) {
                    scrollListener.onTabsStateChanged(isTabsTop, mChildView);
                }
            }
        }
    }

    /**
     * 滑动
     *
     * @param dy 滑动距离
     */
    private void scrollBy(int dy, RecyclerView recyclerView) {
        try {
            recyclerView.scrollBy(0, dy);
        } catch (Exception e) {
            ExceptionReporterHelper.reportException(e);
        }
    }

    /**
     * 内部列表 接受事件的处理
     */
    private void onChildScrolling(int childTop, int dy, int[] consumed) {
        if (childTop == 0) {
            if (dy < 0) {
                //tab在顶,向下滑动,如果子列表不能滑了,parent把dy都消耗掉,然后滑外部列表。
                if (!mChildList.canScrollVertically(dy)) {
                    consumed[1] = dy;
                    scrollBy(dy, mRootList);
                }
            }
        } else {
            if (dy < 0 || childTop > dy) {
                consumed[1] = dy;
                scrollBy(dy, mRootList);
            } else {
                //dy大于0
                consumed[1] = dy;
                scrollBy(childTop, mRootList);
            }
        }
    }

二、CoordinatorLayout 和 AppbarLayout 联动原理

我以前分享过CoordinatorLayout的使用:《Android进阶之光》Design Support Library常用控件(二):CoordinatorLayout,只懂基本的使用(其实平时开发够用了)。看了下面这两篇才是略懂原理。

《AppBarLayout滑动原理》

总结一:AppBarLayout滑动原理,手指滑动AppBarLayout时,滑动appBarlayout时,本身及内部子view不消费事件,然后事件走到CoordinatorLayout的OnTouchEvent中,接着遍历子view的behavior,因为appbarLayout通过注解添加的behavior实现了CoordinatorLayout.Behavior中定义的onStartNestedScroll/onNestedPreScroll等方法,所以appbarLayout可以通过behavior这些方法进行滑动内部子view。

《CoordinatorLayout 和 AppbarLayout 联动原理解析 》

总结二:联动原理,手指滑动recyclerView时,由于和CoordinatorLayout形成前套滑动,所以事件交给CoordinatorLayout处理,在CoordinatorLayout的OnTouchEvent中,处理方式就是总结一了,即交给AppBarLayout滑动了。那recyclerView此时也会跟着滑动,为啥呢?是因为recyclerView设置的behavior(“app:layout_behavior="@string/appbar_scrolling_view_behavior”),这个behavior的作用就纯粹为了让 recyclerView一直保持在AppBarLayout下方。(这个behavior没有实现onStartNestedScroll/onNestedPreScroll等方法。)

发文前,又看了下淘宝,貌似已经修复这个问题了,哈哈!