一篇文章搞定《Android嵌套滑动》

前言

大家需要保证上一篇《一篇文章搞定事件分发》都看完了。或者对事件分发都心知肚明了。
不然解决起嵌套滑动,很容易理解不了。如果了解了事件分发看起来会事半功倍。

说起嵌套滑动,那不得不说起一些电商的首页。
多层的列表滑动、左右滑动列表、上下滑动列表、吸顶Table。充斥着嵌套滑动。
所以本文也会模拟电商的首页去讲解我们的嵌套滑动

嵌套滑动冲突种类

产生原因

首先滑动冲突产生的原因是什么?
大家可以想象一下,事件的U型图。是U型的进行处理我们的事件。
那么当ViewGroup1中嵌套了ViewGroup2。那么事件会先到ViewGroup1,之后ViewGroup1就会到ViewGroup2进行消费。
那这个时候如果ViewGroup2把事件消费了,ViewGroup1岂不是不能消费到了?
对应函数就是onTouchEvent收不到了。那ViewGroup1的MotionEvent事件不都就收不到了是不。

那就导致事件的失效,比如我想往下滑动我的ScrollView1。但是我命中了子ViewGroup的ScrollView2。
那么这时候时间被ScrollView2消费了。这时候滑动的就变成了ScrollView2而不是ScrollView1。
下面看看都有哪些情况和处理方式:

1、外部与内部滑动方向不一致

也就是说可以左右进行滑动,也可以上下进行滑动。
可以是RecyclerView+ListView。也可以是ScrollView+RecyclerView的形式。我的意思是多种形式的组合。
当然你可以用ViewPage + RecyclerView的形式。并且你会发现哎呦喂,没有滑动冲突啊,因为RecyclerView帮我们做了。这里就不赘述RecyclerView的原理了,到头来都是利用事件分发原理去做的。
在这里插入图片描述

2、外部与内部滑动方向一致

也就是说两次的滑动View都是上下滑动、或者左右滑动的
可以是ScorllView+ScorllView。也可以是RecyclerView+ScorllView。我的意思是多种组合。
发现了吗?不管怎么组合都是ViewGroup的组合。
在这里插入图片描述

3、多种情况下的嵌套(电商首页)

直接我给你来个电商图。(后面会用MVVM去写一个首页楼层框架,开源给大家)
可以看到整体结构为:
1、顶部的ToolBar
2、多类型Item的List楼层
3、其中最后一个Item是:左右滑动的可吸顶Tab加上下滑动的瀑布流List的Page
在这里插入图片描述
很多电商的首页都是这中间结构的。我还发现其他App有一些带有评论的页面也有这种结构。
下面让我们逐个去讲解一下这三类的嵌套滑动冲突处理

解决嵌套滑动的方法

主要分为三种方式去解决此类的问题,下面也将会采用这三类方式去讲解
我先说一下拦截的思路,举例放到后面解决问题的场景中吧。要不重复代码过多。

1、外部拦截法

控制父View的onInterceptTouchEvent()方法,决定在什么时候拦截。
拦截时机:先判断手势的走向,然后根据子View的需求场景进行拦截。
这了解了事件分发的就一定知道onInterceptTouchEvent拦截了。不清楚的先看事件分发吧!!

2、内部拦截法

由子滑动View调用requestDisallowInterceptTouchEvent()决定父View是否可拦截。这个是拦截在dispatchTouchEvent这个阶段的

在源码中,requestDisallowInterceptTouchEvent()方法定义在ViewParent接口中,子View可以通过getParent()方法获取到它的父View,并调用该方法来控制父View是否拦截事件。在ViewGroup中的dispatchTouchEvent()方法中会先检查是否可以拦截事件,如果子View调用了requestDisallowInterceptTouchEvent(true)方法,则父View不会拦截该事件。

3、现有API框架

NestedScrolling机制滑动控件:根据接口实现,动态分配事件
比如嵌套滑动组件 NestedScrollingParent 和 NestedScrollingChild、CoordinatorLayout(也是通过NestedScrolling接口来实现的)
列举一下在View中使用了这些接口的组件:
实现 NestedScrollingParent 接口的 View 有:NestedScrollView、CoordinatorLayout、MotionLayout 等
实现 NestedScrollingChild 接口的 View 有:NestedScrollView、RecyclerView 等
NestedScrollView 是唯一同时实现两个接口的 View,这意味着它可以用作中介来实现多级嵌套滑动,后面会说到。

外部与内部滑动方向不一致

1、ViewPage和RecyclerView嵌套

这个单拿出来简单说一下,大家在开发时使用ViewPage和RecyclerView不同方向时发现他没有什么滑动冲突啊。
这是因为它的内部给我们处理了:
在ViewPager和RecyclerView嵌套使用时,它们在内部有一个默认的处理机制,以避免横向滑动冲突。具体来说,ViewPager会处理水平方向的滑动事件,而RecyclerView会处理垂直方向的滑动事件。
这种处理机制的实现依靠了ViewPager控件和RecyclerView控件内部的触摸事件拦截机制。在ViewPager中,重写onInterceptTouchEvent()方法,根据滑动方向,判断是否拦截触摸事件。在RecyclerView中,则重写dispatchTouchEvent()方法,在垂直或水平方向上优先处理滑动事件。
当然是上层看起来是继承NestedScrollingChild去处理的。

2、ScrollView嵌套RecyclerView

这个大家可以去试一下,大家会发现哎呀,也没有嵌套滑动的冲突啊
这个是因为什么?
这是因为RecyclerView实现了NestedScrollingChild接口,以便和父View协同处理滑动事件,从而避免了滑动冲突的问题。
注意:自从RecyclerView 22.2.0版本以后,它才默认实现了滑动冲突的处理。
AndroidX是在"com.android.support:recyclerview-v7"库升级到28.0.0版本之后推出的,所以AndroidX中的RecyclerView也是处理过的了。

3、ScrollView嵌套HorizontalScrollView

都做了处理,那找个没被处理过的给大家演示吧!!
小知识ScrollView没有横向的,需要用HorizontalScrollView
这里用ScrollView作为上下滑动父控件、HorizontalScrollView作为左右滑动子控件来给大家演示:
先看一下图做下铺垫,有的同学就喜欢图不喜欢文字。
ps:最外层为一个ScrollView上下滑动,中途有一个HorizontalScrollView左右滑动
一开始想用自己搭建布局给大家看,但是粘贴XML代码整篇文章又太乱了,我后面放到gitee上大家自取。
在这里插入图片描述
先说一下思路昂:首先按照上面布局搭建的话,就会发生中间的HorizontalScrollView不能进行左右滑动,也就是说被ScrollView拦截了。哎呦喂,滑动嵌套终于发生了。
步骤一:先要获取用户的滑动动作
那怎么先获取滑动动作呢:来个简单的数学,嘻嘻
我们可以得到滑动过程中的两个点的坐标。一般情况下根据水平和竖直方向滑动的距离差就可以判断方向。
也可以通过两个点的夹角去判断。
在这里插入图片描述
假设起点为(2 ,2)终点为(5,4)
这样我们通过(5-2)-(4-2)= 1可知横向的移动大于竖向的移动。那么我们就可以判断出来是横向移动
步骤二:去拦截不该移动的ScrollView
这里使用外部拦截和内部拦截分别去处理

外部拦截法

上面简述了,外部拦截要控制父View的onInterceptTouchEvent()方法
利用上面判断的方法判断滑动方向,之后通过onInterceptTouchEvent的返回值来决定是否拦截。
了解事件分发的兄弟都知道,返回true是拦截,false和super是传递给下层。
直接上代码:(代码中也有部分说明)

class MyScrollerView : ScrollView {
    
    
    //构造函数省略了
    private var mLastMoveX = 0
    private var mLastMoveY = 0

    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
    
    
        var intercept = false
        val moveX = ev.x.toInt()
        val moveY = ev.y.toInt()

        when (ev.action) {
    
    
            MotionEvent.ACTION_DOWN -> {
    
    
                intercept = false;
                //调用ViewPager的onInterceptTouchEvent方法初始化mActivePointerId
                super.onInterceptTouchEvent(ev)
            }

            MotionEvent.ACTION_MOVE -> {
    
    
                //横坐标的增量
                val deltaX = moveX - mLastMoveX
                //纵坐标的增量
                val deltaY = moveY - mLastMoveY
                //我的的内部是横向的,所以当横向移动距离大的时候
                //那么我们外层的ScrollView就不要进行拦截。反之进行拦截
                intercept = abs(deltaX) <= abs(deltaY)
                Log.d("MyScrollerView", "intercept=$intercept")
            }

            MotionEvent.ACTION_UP -> {
    
    
                intercept = false
            }
        }

        mLastMoveX = moveX
        mLastMoveY = moveY
        return intercept
    }
}

内部拦截法

上面说了要用子滑动View调用requestDisallowInterceptTouchEvent()决定父View是否可拦截。 当然这个事情是发生在dispatchTouchEvent这个阶段的。
下面我们来用代码实现一下:

class MyHorizontalScrollView : HorizontalScrollView {
    
    
   	//构造函数省略了
    private var mLastMoveX = 0
    private var mLastMoveY = 0

    override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
    
    
        val moveX = ev.x.toInt()
        val moveY = ev.y.toInt()

        when (ev.action) {
    
    
            MotionEvent.ACTION_DOWN -> {
    
    
                parent.requestDisallowInterceptTouchEvent(true)
            }

            MotionEvent.ACTION_MOVE -> {
    
    
                //横坐标的增量
                val deltaX = moveX - mLastMoveX
                //纵坐标的增量
                val deltaY = moveY - mLastMoveY
                //我的的内部是横向的,所以当横向移动距离大的时候, 就去通知父view不要拦截
                if (abs(deltaX)  > abs(deltaY)){
    
    
                    parent.requestDisallowInterceptTouchEvent(false)
                }
            }
        }

        mLastMoveX = moveX
        mLastMoveY = moveY
        return super.dispatchTouchEvent(ev)
    }
}

使用API解决拦截

直接把外层的ScrollView换成NestedScrollView

<androidx.core.widget.NestedScrollView
    android:id="@+id/scrollView_out"
    .......>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        .......>

        <ImageView
            android:layout_width="match_parent"
            android:layout_height="300dp"
            android:background="@color/white"/>

        <HorizontalScrollView
            android:id="@+id/ScrollView_2"

之后再测试一下,发现哎呦喂!!!还真奏效呢。这是因为什么呢? 简单说一下
NestedScrolling是Android5.0推出的嵌套滑动机制。他有NestedScrollingChild、NestedScrollingParent两个接口和NestedScrollingChildHelper、NestedScrollingParentHelper两个辅助类来帮助控件实现嵌套滑动,CoordinatorLayout便是基于这个机制实现各种神奇的滑动效果。
具体的可以看这篇文章NestedScrolling嵌套滑动机制之基础篇

外部与内部滑动方向相同

ViewPage和RecyclerView

其实网上很多博文对同方向的滑动冲突有点错误的引导:
比如很多用ViewPage和RecyclerView来举例。大家可以试一试。
这种是没有问题的,它本身就会传递到子View去处理的。

他其实在业务场景中你ViewPage中有RecyclerView肯定是想内部的RecyclerView进行滑动的。所以这部分你处理个毛。
所以对于ViewPage和RecyclerView这种。你可以去尝试一下你会发现当子RecyclerView滑动到末尾的时候。
再次滑动就会触发ViewPage的滑动。

所以可能会碰到的需求场景是,即便子RecyclerView滑到末尾也不要触发ViewPage的滑动。这时候我们才需要去处理。
场景一:ViewPage和RecyclerView滑动到末尾后不触发ViewPage滑动
解决方案:大家估计想到了,没错就是重写内部的RecyclerView。只要是在RecyclerView就不让上层处理。(关于正式需求上的优化,后面自己在基础上加就行了。比如:滑动速度和距离上的问题)

class MyRecyclerView : RecyclerView {
    
    
    //一些构造函数省略了
    //.....
    override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
    
    
        if (ev.action == MotionEvent.ACTION_DOWN || ev.action == MotionEvent.ACTION_MOVE) {
    
    
            parent.requestDisallowInterceptTouchEvent(true)
        } else {
    
    
            parent.requestDisallowInterceptTouchEvent(false)
        }
        return super.dispatchTouchEvent(ev)
    }
}

ScrollView嵌套ScrollView

场景二:当你使用ScrollView嵌套ScrollView,或者HorizontalScrollView嵌套HorizontalScrollView发生的。内部的ScrollView不能滑动:

原因:两个ScrollView嵌套时,滑动距离达到滑动手势判定阈值(mTouchSlop)的这个MOVE事件,会先经过父View 的onInterceptTouchEvent()方法,父View直接把事件拦截,子 View 的onTouchEvent()方法里虽然也会在判定滑动距离足够后调用requestDisallowInterceptTouchEvent(true)。

解决:大家肯定想到了,直接把外层的ScrollView进行自定义,重写onInterceptTouchEvent。让他传递给内部的ScrollView就OK了。确实如此!!!

override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
    
    
    if(ev.action == MotionEvent.ACTION_MOVE || ev.action == MotionEvent.ACTION_DOWN){
    
    
        super.onInterceptTouchEvent(ev);
        return false
    }
    return true
}

多种情况嵌套(电商首页)

电商首页这个实例我就不在这说了,不然写的也很匆忙,文章也很长。
我在下一篇文章直接把CoordinatorLayout完整解析一下。
之后全篇把这个电商首页作为例子去写一下。

总结

嵌套滑动的冲突其实,经过API的版本迭代Google已经利用NestedScrolling 机制帮我处理了很多了。
有些比较特殊的场景需要我们去处理,但是万变不离其中,都是通过事件分发的机制去解决的。
所以大家要把事件分发搞清楚,再来理解嵌套滑动,去解决嵌套滑动的问题。
我也是看了好多次之后才对嵌套滑动有了一点理解。
希望读者们多多提意见!!!

猜你喜欢

转载自blog.csdn.net/weixin_45112340/article/details/131003331
今日推荐