NestScroll几个类的调用关系(解决嵌套滑动RecyclerView无法下滑的问题)

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

问题产生

项目中个人主页效果需要嵌套滑动,主页下有三个以及以上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呢?有空再分析吧~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342