美文网首页
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