嵌套滑动造成的滑动冲突原理分析
场景复现:
CoordinatorLayout + AppBarLayout + Vertical RecyclerView + Horizontal RecycleView
Horizontal RecycleView 是Vertical RecyclerView的一个子view, CoordinatorLayout 实现了AppBarLayout 和 RecyclerView的协调联动,在向上滑动RecyclerView的时候,会先滑动AppBarLayout,再滑动RecyclerView,问题场景是在点击Horizontal RecycleView,然后向上滑动时,造成了Vertical RecyclerView 滑动,而AppBarLayout 没有滑动。
原理解析:
Behavior的概念
Behavior用于为特定的子 View 定义自定义的交互行为,它通常与 CoordinatorLayout 一起使用,允许开发者在 View 之间创建复杂的滚动、滑动、拖拽等行为。
Behavior 提供了一些关键的回调方法,用于处理事件:
方法 | 描述 |
---|---|
onInterceptTouchEvent | 是否拦截触摸事件。 |
onTouchEvent | 处理触摸事件。 |
onStartNestedScroll | 是否开始处理嵌套滑动事件。 |
onNestedScrollAccepted | 当嵌套滑动被接受时调用。 |
onNestedPreScroll | 嵌套滑动事件之前调用,用于消费部分或全部滑动。 |
onNestedScroll | 在嵌套滑动期间调用,用于处理滑动的剩余部分。 |
onStopNestedScroll | 嵌套滑动结束时调用。 |
layoutDependsOn | 定义该 Behavior 是否依赖另一个 View。 |
onDependentViewChanged | 当依赖的 View 发生变化时调用(如位置或大小变化)。 |
CoordinationLayout + AppBarLayout + Vertical RecycleView是如何实现联动的呢?
1.点击AppBarLayout 位置时
AppBarLayout 不会消费事件,会将事件传递给CoordinationLayout#onTouchEvent()
if (mBehaviorTouchView != null || (cancelSuper = performIntercept(ev, TYPE_ON_TOUCH))) {// Safe since performIntercept guarantees that// mBehaviorTouchView != null if it returns true//mBehaviorTouchView 就是AppBarLayoutfinal LayoutParams lp = (LayoutParams) mBehaviorTouchView.getLayoutParams();final Behavior b = lp.getBehavior();if (b != null) {handled = b.onTouchEvent(this, mBehaviorTouchView, ev);}}
mBehaviorTouchView 就是AppBarLayout,这时就调用了AppbarLayout#Behavior#onTouchEvent()事件,在这里处理了滑动事件scroll。
2.点击RecyclerView位置滑动时,
首先先进行事件传递,在Action_Down的时候会调用 startNestedScroll(nestedScrollAxis, TYPE_TOUCH)
,去询问嵌套布局CoordinatorLayout是否可以滑动,嵌套布局怎么来的呢?
if (isNestedScrollingEnabled()) {ViewParent p = mView.getParent();View child = mView;while (p != null) {if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {setNestedScrollingParentForType(type, p);ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);return true;}if (p instanceof View) {child = (View) p;}p = p.getParent();}
}
//ViewParentCompat.classpublic static boolean onStartNestedScroll(@NonNull ViewParent parent, @NonNull View child,@NonNull View target, int nestedScrollAxes, int type) {if (parent instanceof NestedScrollingParent2) {return ...}}
重点看上面的while循环,你会发现它会遍历自己的父view 直到找到实现了NestedScrollingParent2的View ,这里就是指CoordinationLayout了,当找到的时候,将会遍历CoordinationLayout 所有子View 的Behavior是否可以实现嵌套联动的滑动,
// CoordinationLayout.class
public boolean onStartNestedScroll(View child, View target, int axes, int type) {boolean handled = false;final int childCount = getChildCount();for (int i = 0; i < childCount; i++) {final View view = getChildAt(i);if (view.getVisibility() == View.GONE) {// If it's GONE, don't dispatchcontinue;}final LayoutParams lp = (LayoutParams) view.getLayoutParams();final Behavior viewBehavior = lp.getBehavior();if (viewBehavior != null) {// 在这里会去调用子view的Behavior去判断是否支持嵌套滚动final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child,target, axes, type);handled |= accepted;// 设置子View是否支持嵌套滚动,指AppBarlayout是否支持lp.setNestedScrollAccepted(type, accepted);} else {lp.setNestedScrollAccepted(type, false);}}return handled;}
设置完是否支持嵌套滚动后,滚动事件就会到Action_Move事件中,这时RecyclerView 就会消费这个事件,主要是以下两行代码:
//RecyclerView.class
//这行代码将x,y传递给CoordinationLayout,然后CoordinationLayout#NestedScroll()方法将x,y传递给AppBarlayout消费
dispatchNestedPreScroll(canScrollHorizontally ? dx : 0,canScrollVertically ? dy : 0,mReusableIntPair, mScrollOffset, TYPE_TOUCH)//消费,最后自己消费剩余的滚动if (consumedX != 0 || consumedY != 0) {dispatchOnScrolled(consumedX, consumedY);}
以上就实现了联动滑动的效果。
问题来了,为什么在Vertical RecyclerView 再加一个Horizontal RecycleView的时候就会是Vertical RecyclerView来消费了呢,原因就是在onStartNestedScroll 通知到AppBarlayout#Behavior时,Horizontal 和Vertical 都通知了一遍,Horizontal时后面通知的,看一下通知后的代码:
//AppBarLayout.class
public boolean onStartNestedScroll(@NonNull CoordinatorLayout parent,@NonNull T child,@NonNull View directTargetChild,View target,int nestedScrollAxes,int type) {// Return true if we're nested scrolling vertically, and we either have lift on scroll enabled// or we can scroll the children.//这里判断了ViewCompat.SCROLL_AXIS_VERTICAL 如果不是VERTICAL的时候就不允许嵌套滑动了final boolean started =(nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0&& (child.isLiftOnScroll() || canScrollChildren(parent, child, directTargetChild));。。。。return started;}
然后Horizontal RecycleView在竖向滑动时是不支持的,所以事件由Vertical RecyclerView滑动,这时呢,嵌套滑动又被拒绝了,只能是
Vertical RecyclerView来响应滑动事件了。
解决方案:
recyclerView.isNestedScrollingEnabled = false