美文网首页AndroidAndroid应用开发那些事优秀案例
嵌套滑动--NestedScroll-项目实例(淘宝首页缺陷),

嵌套滑动--NestedScroll-项目实例(淘宝首页缺陷),

作者: 胡飞洋 | 来源:发表于2019-11-26 10:20 被阅读0次

    刚复习完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等方法。)

    相关文章

      网友评论

        本文标题:嵌套滑动--NestedScroll-项目实例(淘宝首页缺陷),

        本文链接:https://www.haomeiwen.com/subject/kkcewctx.html