当前位置: 首页 > news >正文

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

相关文章:

  • 解惑Python:一文解决osgeo库安装失败问题
  • DeepSeek + Vue实战开发
  • Python字符模糊匹配指南 RapidFuzz | python小知识
  • RocketMQ 5.0安装部署
  • Ubuntu 安装 OpenCV (C++)
  • 请解释设备像素、CSS 像素、设备独立像素、DPR、PPI 之间的区别 ?
  • 将图片base64编码后,数据转成图片
  • Jetson Agx Orin平台preferred_stride调试记录--1924x720图像异常
  • SQL代码规范
  • 外贸跨境订货系统流程设计、功能列表及源码输出
  • 数据结构:单链表(Single Linked List)及其实现
  • 奥比中光3D机器视觉相机能连接halcon吗?
  • 基于海思soc的智能产品开发(图像处理的几种需求)
  • LeetCode每日精进:20.有效的括号
  • 【Go语言快速上手】第二部分:Go语言进阶之网络编程
  • Nuclei 使用手册
  • 视频孪生在机场的应用,赋能机场智慧化建设
  • 响应式网站开发需求以及解决方案
  • Vue 3 中可读可写的计算属性(Computed Properties)的使用场景
  • 快速搭建 OLLAMA + DeepSeek 模型并对接 Cherry Studio
  • thinkphp做网站有什么好处/百度客服24小时人工服务
  • 怎么做网站主/谷歌应用商店
  • 济南建网站/个人网页设计作品欣赏
  • 网站文字格式/南京今天重大新闻事件
  • 毕业查询结果网站怎么做/网络平台推广运营有哪些平台
  • 网站系统建设开票要开什么/国内重大新闻