Android嵌套滑动造成的滑动冲突原理分析
嵌套滑动造成的滑动冲突原理分析
场景复现:
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 就是AppBarLayout
final 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.class
public 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 dispatch
continue;
}
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