美文网首页
NestScroll几个类的调用关系(解决嵌套滑动Recycle

NestScroll几个类的调用关系(解决嵌套滑动Recycle

作者: mrz_233333 | 来源:发表于2018-03-17 22:30 被阅读0次

    项目中使用了嵌套滑动,要解决事件滑动问题的时候源码翻来翻去还是看不太清晰,觉得还是记录下来,边记录边分析吧。

    问题产生

    项目中个人主页效果需要嵌套滑动,主页下有三个以及以上fragment需要左右滑动切换,且每个fragment都有独自数据列表需要loadmore和pullrefresh,且fragment的标签tab网上滑动时需要悬停(跟新浪微博个人主页相似效果)最终采用

                                 -->FragmentA-->SwipeRefreshView-->RecyclerView
    CoordinatorLayout-->ViewPager-->FragmentB-->SwipeRefreshView-->RecyclerView
                                 -->FragmentC-->SwipeRefreshView-->RecyclerView
    

    这种结构。
    但是当网络错误或者空数据时,会隐藏RecyclerView展示提示Textview来提示用户。然而展示TextView时,无法触发嵌套滑动了。

    Debug源码

    因为有一份同样结构(布局略微不同),但可以正常滑动TextView的代码,所以我可以放心Debug dispatchTouchEvent,直接看那份不能滑动的代码到底在哪里dispatchTouchEvent被中断了。
    很快找到了原因,能滑动的那份代码在RecyclerView里dispatchTouchEvent调用onTouchEvent处理的代码,然后触发了parent的嵌套滑动,而不能滑动的那份代码在CoordinatorLayout之后MotionEvent就没有往下传递了。
    查看布局,确实是因为我把Rv给gone掉了,显示了Tv,而能滑的是Tv遮罩在Rv之上,导致虽然展示的是Tv,但是滑动事件是被Rv处理并触发嵌套滑动的(还能这样?被遮罩的view也能获取到事件?)。
    Ok,我改动布局,不gone掉Rv确实可以滑动了,但是...可以往上滑,但下滑还是无法滑动。
    这个就比较奇怪了,再继续debug,发现正常滑动的 和我这个不能滑动的 滑动事件都能传递到Rv的onTouchEvent,且调用传值似乎是一致的,看来通过Debug 事件处理函数 是无法解决问题了。
    没办法,硬着头皮看看nestscroll的机制的源码吧。
    参考hongyang的博文http://blog.csdn.net/lmj623565791/article/details/52204039

    分析源码

    应该是android 5.0(?)开始引入了这几个类和接口来更好的实现嵌套滑动:
    两个接口NestedScrollingChild ,NestedScrollingParent
    两个类NestedScrollingChildHelper ,NestedScrollingParentHelper


    image.png image.png

    可见Parent是事件接收方,都是on... 而child是事件的分派方。

    由于项目中主要是用了RecyclerView/SwipeRefreshLayout/CoordinatorLayout,所以主要来借用这几个布局来分析。

    实现接口 持有对象
    RecyclerView NestedScrollingChild NestedScrollingChildHelper (mScrollingChildHelper)
    SwipeRefreshLayout NestedScrollingChild NestedScrollingParent mNestedScrollingParentHelper mNestedScrollingChildHelper
    CoordinatorLayout NestedScrollingParent mNestedScrollingParentHelper

    RecyclerView在onTouchEvent中:
    Down事件调用了startNestedScroll(nestedScrollAxis)
    Move事件调用了

    dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset) 
    scrollByInternal(..)-->dispatchNestedScroll(..)
    

    等等
    内部都是通过mScrollingChildHelper来处理


    image.png

    在ChildHelper的

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

    方法中,获得了当前view的parent来作为NestedScrollingParent?
    debug看调用堆栈信息:


    image.png

    (项目确实view层级有点过深 dispatchTouchEvent被递归调用9次..)
    可以看到Rv通过其持有的mScrollingChildHelper调用了ViewParentCompat的

    IMPL.onStartNestedScroll(parent, child, target, nestedScrollAxes);
    

    此时IMPL的具体实现是ViewParentCompatLollipopImpl

        static final ViewParentCompatImpl IMPL;
        static {
            final int version = Build.VERSION.SDK_INT;
            if (version >= 21) {
                IMPL = new ViewParentCompatLollipopImpl();
            } else if (version >= 19) {
                IMPL = new ViewParentCompatKitKatImpl();
            } else if (version >= 14) {
                IMPL = new ViewParentCompatICSImpl();
            } else {
                IMPL = new ViewParentCompatStubImpl();
            }
        }
        public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
                int nestedScrollAxes) {
            try {
                return parent.onStartNestedScroll(child, target, nestedScrollAxes);
            } catch (AbstractMethodError e) {
                Log.e(TAG, "ViewParent " + parent + " does not implement interface " +
                        "method onStartNestedScroll", e);
                return false;
            }
        }
    

    parent是CoordinatorLayout,通过Compat就调用到了parent的onStartNestedScroll方法。
    CoordinatorLayout的onStartNestedScroll方法通过behavior来处理事件

        public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
            boolean handled = false;
    
            final int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                final View view = getChildAt(i);
                final LayoutParams lp = (LayoutParams) view.getLayoutParams();
                final Behavior viewBehavior = lp.getBehavior();
                if (viewBehavior != null) {
                    final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target,
                            nestedScrollAxes);
                    handled |= accepted;
    
                    lp.acceptNestedScroll(accepted);
                } else {
                    lp.acceptNestedScroll(false);
                }
            }
            return handled;
        }
    

    ok,至此可以看出整个调用关系就是child---dispatchTouchEvent--startNestedScroll--找到实现了NestedParent的父view,调用父view的onStartNestedScroll,父view来处理滑动逻辑。
    onNestedScroll也大概是同样的调用关系,经过多次debug,发现应该只在move事件的时候,会调用onNestedScroll。
    最后发现在下滑的时候onStartNestedScroll调用完成后,onNestedScroll没有继续调用,导致无法下滑。在正常滑动的代码中,onNestedScroll是可以触发的。

    进一步定位到底是哪里导致了onNestedScroll没有触发


    image.png image.png image.png

    对比发现在helper的dispatchNestedScroll方法中

    if(dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0)
    

    这个判断是false,所以没有进入nestedScroll的派发逻辑,进一步跟踪,正确那份代码true的原因是dyUnconsumed!=0 这个为true,dyUnconsumed在scrollByInternal这个方法中被赋值

    我的api25的虚拟机 和真机(小米note)的代码都跟android25 的V7包的RecyclerView 不一致,debug起来十分蛋疼(虚拟机也不一致,不知道怎么去哪找匹配虚拟机版本support包的代码)。
    没办法,只能使出绝技,scrollByInternal方法里的每一个子方法都打上方法断点,这样起码知道具体执行到哪个方法了..


    image.png

    经过仔细的对比,发现我不能滑的那份代码就没有进入赋值语句,因为mAdapter=null,如上图..
    所以,终于真相大白。原来竟然是adapter没有设置 导致的无法下滑,你敢信?
    因为是空数据展示的View,所以索性我就没有创建adapter,直接展示Textview提醒用户,没想到是这个原因。
    加上adapter...

    果然可以往下滑动了,Problem resolved,but cost me plenty of hours,shit。

    总结

    Debug源码虽好,但是源码调用层级太深,加上部分support包 行数不一致,确实很耗时间。还是需要修炼学习更牛逼的debug技巧,快速定位问题的姿势。
    可以上滑,但无法下滑,我觉得正常人都会想到的是下滑的某个事件被判断 return了,肯定会在代码里找各种事件处理的逻辑。然而背后的原因竟然是adapter... 我觉得我不花点时间debug源码,肯定发现不了这个。

    再挖一下

    既然都debug到这儿了,那再看看上滑为何没有adapter也可以滑?
    仔细观察Rv在MOVE事件中处理的代码:

                    if (mScrollState != SCROLL_STATE_DRAGGING) {
                        boolean startScroll = false;
                        if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
                            if (dx > 0) {
                                dx -= mTouchSlop;
                            } else {
                                dx += mTouchSlop;
                            }
                            startScroll = true;
                        }
                        if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
                            if (dy > 0) {
                                dy -= mTouchSlop;
                            } else {
                                dy += mTouchSlop;
                            }
                            startScroll = true;
                        }
                        if (startScroll) {
                            setScrollState(SCROLL_STATE_DRAGGING);
                        }
                    }
    
                    if (mScrollState == SCROLL_STATE_DRAGGING) {
                        mLastTouchX = x - mScrollOffset[0];
                        mLastTouchY = y - mScrollOffset[1];
    
                        if (scrollByInternal(
                                canScrollHorizontally ? dx : 0,
                                canScrollVertically ? dy : 0,
                                vtev)) {
                            getParent().requestDisallowInterceptTouchEvent(true);
                        }
                        if (ALLOW_PREFETCHING) {
                            mViewPrefetcher.postFromTraversal(dx, dy);
                        }
                    }
    
    发现 往下滑 image.png

    scrollstate是1 ==DRAGGING,而往上滑是0,处于SCROLL_STATE_IDLE状态因此并没有进入scrollByInternal这个方法。
    比较奇怪为何在嵌套滑动中上滑Rv ,state确实idle呢?有空再分析吧~

    相关文章

      网友评论

          本文标题:NestScroll几个类的调用关系(解决嵌套滑动Recycle

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