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

news2024/11/22 5:43:42

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

  • 前言
  • 嵌套滑动冲突种类
    • 产生原因
    • 1、外部与内部滑动方向不一致
    • 2、外部与内部滑动方向一致
    • 3、多种情况下的嵌套(电商首页)
  • 解决嵌套滑动的方法
    • 1、外部拦截法
    • 2、内部拦截法
    • 3、现有API框架
  • 外部与内部滑动方向不一致
    • 1、ViewPage和RecyclerView嵌套
    • 2、ScrollView嵌套RecyclerView
    • 3、ScrollView嵌套HorizontalScrollView
      • 外部拦截法
      • 内部拦截法
      • 使用API解决拦截
  • 外部与内部滑动方向相同
    • ViewPage和RecyclerView
    • ScrollView嵌套ScrollView
  • 多种情况嵌套(电商首页)
  • 总结

前言

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

说起嵌套滑动,那不得不说起一些电商的首页。
多层的列表滑动、左右滑动列表、上下滑动列表、吸顶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 机制帮我处理了很多了。
有些比较特殊的场景需要我们去处理,但是万变不离其中,都是通过事件分发的机制去解决的。
所以大家要把事件分发搞清楚,再来理解嵌套滑动,去解决嵌套滑动的问题。
我也是看了好多次之后才对嵌套滑动有了一点理解。
希望读者们多多提意见!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/600723.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MySQL第三章、表的增删查改

目录 一、CRUD 二、新增&#xff08;Create&#xff09; 2.1单行数据 全列插入 ​2.2多行数据 指定列插入 ​编辑 三、查询&#xff08;Retrieve&#xff09; ​3.1全列查询 3.2指定列查询 3.3 查询字段为表达式 3.4 别名 3.5 去重&#xff1a;DISTINCT 3.6 排序…

docker-compose安装 rocketmq server、dashboard

目录 目录结构 nameserver安装 broker安装 控制台安装 测试效果 rocket分为3个服务&#xff1a;nameserver、broker、dashboard 这边我计划分开安装&#xff1a; 安装版本为4.5.0 目录结构 规划的结构 命令 mkdir -p /apps/rocketmq/namesrv/{config,data,logs} mkdir…

自动化测试-终章

自动化测试-终章 前沿 如果想做不需要人去点击使用程序做到真正的自动化测试思想,以下是我的思想,需要跟着我的思路来,我们做一个可以测试所有页面的增删改查功能是否好使 思想一 我使用的是Java 做自动化测试,我们现做一个简单的自动化 pom 需要引入 selenium-java 然后需…

Qt中的窗口类及其特点

目录 常用的窗口类 窗口的显示内嵌窗口 QWidget内嵌窗口演示 QWidget不内嵌窗口演示 QDialog类型的窗口特点 QMainWindows窗口的特点 总结 常用的窗口类 常用的窗口类有 3 个 在创建 Qt 窗口的时候&#xff0c;需要让自己的窗口类继承上述三个窗口类的其中一个QWidget 所有…

增量数据抽取技术

写在前面 本文隶属于专栏《大数据从 0 到 1》&#xff0c;该专栏为笔者原创&#xff0c;引用请注明来源&#xff0c;不足和错误之处请在评论区帮忙指出&#xff0c;谢谢&#xff01; 本专栏目录结构和文献引用请见《大数据从 0 到 1》 正文 在数据仓库中要实现增量抽取&#x…

STL-string-1

stoi int stoi (const string& str, size_t* idx 0, int base 10);int stoi (const wstring& str, size_t* idx 0, int base 10); Convert string to integer 解析str&#xff0c;将其内容解释为指定基数的整数&#xff0c;该整数作为int值返回。 如果idx不是空…

QLoRa:在消费级GPU上微调大型语言模型

大多数大型语言模型(LLM)都无法在消费者硬件上进行微调。例如&#xff0c;650亿个参数模型需要超过780 Gb的GPU内存。这相当于10个A100 80gb的gpu。就算我们使用云服务器&#xff0c;花费的开销也不是所有人都能够承担的。 而QLoRa (Dettmers et al.&#xff0c; 2023)&#x…

公司裁员不给赔偿怎么办?

阅读本文大概需要 1.61 分钟。 最近在星球回答球友问题的时候&#xff0c;发现不少人都提到裁员这个话题。 有球友说他们公司在裁员&#xff0c;但不想给赔偿。 领导给他的方案是把年假调休休了&#xff0c;然后再给三周找工作时间&#xff0c;这三周不用打卡&#xff0c;三周后…

茅塞顿开的C#代码——通用型科学计算器

计算器是经常遇到的编程作业。 一般都是实现加、减、乘、除四则运算的普通计算器。 这里介绍用几十行C#代码实现的复杂的《科学计算器》&#xff0c;可以计算各种函数。 不知道其他语言实现同样的功能需要编写多少行代码&#xff1f;20000行&#xff1f; using System; usin…

SpringBoot接口如何正确地接收时间参数

唠嗑部分 在做Java开发时&#xff0c;肯定会碰到传递时间参数的情况吧&#xff0c;比如用户的出生日期、活动的开始&#xff0c;结束日期等等&#xff0c;这些参数往往是由前端传递过来的&#xff0c;那么在SpringBoot项目中&#xff0c;该如何正确的接收日期参数呢&#xff0…

如果不小心上了电信黑名单,应该怎么妥善处理呢?

有些小伙伴们在处理不用的手机卡时&#xff0c;可能会粗心大意&#xff0c;认为不用了就用不了呗&#xff0c;存在欠费停机的情况下也没有及时的去补交欠费&#xff0c;然后销户&#xff0c;导致了自己不小心上了电信黑名单&#xff0c;那遇到这种情况&#xff0c;应该怎么妥善…

论文解读 | 利用图形卷积核在距离图像中实现高效的3D目标检测

原创 | 文 BFT机器人 01 摘要 该论文提出了一种基于范围图像的高效3D物体检测方法&#xff0c;通过利用图卷积核来提取每个像素周围的局部几何信息。 作者设计了一种新颖的2D卷积网络架构&#xff0c;并提出了四种替代内积核心的卷积核&#xff0c;以注入所需的三维信息。该方法…

GPT最常用的应用场景有哪些?

生成式预训练转换器&#xff08;GPT&#xff09;是一种深度学习模型&#xff0c;它能够根据给定的提示生成类似人类的文本&#xff0c;彻底改变了自然语言处理&#xff08;NLP&#xff09;领域。 聊天机器人和虚拟助手 GPT最受欢迎的应用程序之一是开发聊天机器人和虚拟助手。凭…

【Python 自然语言处理(NLP)】零基础也能轻松掌握的学习路线与参考资料

Python 自然语言处理&#xff08;NLP&#xff09;是目前人工智能&#xff08;AI&#xff09;发展中的重要领域。随着科技的不断进步&#xff0c;NLP已经被应用于文本自动摘要、机器翻译、语音识别、情感分析、问答系统等各项实际任务中。 要学习 Python 自然语言处理&#xff…

“河南省数字化转型与信息技术应用创新专家研讨会-政府数字化转型推动信创产业发展”专场活动成功召开

由《中国信息化》杂志社主办的“2023河南省数字化转型与信息技术应用创新专家研讨会——政府数字化转型推动信创产业发展”专场活动于5月27日&#xff0c;在郑州成功举办。本次活动由深圳竹云科技股份有限公司协办&#xff0c;由河南省测绘学会、中国信息主管平台支持。中国交通…

Windows 安装部署 MinIo

1、下载地址 安装包下载地址&#xff1a;https://min.io/download#/windows 2、安装目录 下载的是一个可执行文件 minio.exe 将其放到一个方便寻找的目录&#xff0c;我这里放在 D:\develop\minio 同时新建一个 data 文件夹&#xff0c;用来存储上传的文件 3、启动 MinIo 服…

Godot引擎 4.0 文档 - 第一个 3D 游戏

本文为Google Translate英译中结果&#xff0c;DrGraph在此基础上加了一些校正。英文原版页面&#xff1a; Your first 3D game — Godot Engine (stable) documentation in English 你的第一个 3D 游戏 在这个循序渐进的教程系列中&#xff0c;您将使用 Godot 创建您的第一…

Java程序设计入门教程 -- 二维数组

二维数组创建 定义数组 声明二维数组 Java中二维数组的声明格式&#xff1a; 数据类型名[ ][ ] 数组名; 或 数据类型名 数组名[ ][ ]; 分配数组内存 常用格式&#xff1a; new 数组名[ M][N ] ; //M&#xff0c;N为数组行号和列号 分配数组内存 1&#xff09;规…

chatgpt赋能python:Python的几次方符号介绍

Python的几次方符号介绍 当我们在使用Python编程时&#xff0c;经常需要进行数学计算。其中最常见的计算就是幂运算。Python使用幂运算符号来表示一个数的几次方。这个符号既可以用在数字之间&#xff0c;也可以用在变量之间。在本文中&#xff0c;我们将介绍Python中的几次方…

堆基础1_小白垃圾笔记

小白垃圾笔记&#xff0c;不建议阅读。 目录 1.什么是堆&#xff1f; 2.堆从哪里来&#xff1f; 3.堆管理器是什么 4.堆申请的实现方式 1.brk&#xff1a;brk仅仅主线程申请小空间的时候用&#xff0c;子线程不可调用brk。 2.mmap&#xff1a;主线程申请大的内存的时候和…