项目中使用了嵌套滑动,要解决事件滑动问题的时候源码翻来翻去还是看不太清晰,觉得还是记录下来,边记录边分析吧。
问题产生
项目中个人主页效果需要嵌套滑动,主页下有三个以及以上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
可见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来处理
在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看调用堆栈信息:
(项目确实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没有触发
对比发现在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方法里的每一个子方法都打上方法断点,这样起码知道具体执行到哪个方法了..
经过仔细的对比,发现我不能滑的那份代码就没有进入赋值语句,因为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);
}
}
发现 往下滑scrollstate是1 ==DRAGGING,而往上滑是0,处于SCROLL_STATE_IDLE状态因此并没有进入scrollByInternal这个方法。
比较奇怪为何在嵌套滑动中上滑Rv ,state确实idle呢?有空再分析吧~