Compose 嵌套滑动冲突的解决办法

news2024/12/28 21:53:41

前言

在最近我利用业余时间使用 Compose 写的 Gihub APP 中,它的首页结构是这样的:

1.png

采用了 Drawer 嵌套 Pager 的结构。

这就会出现一个问题,那就是 DrawerPager 都需要监听横向滑动手势,从而实现展开 Drawer 和 切换 Pager 的功能。

那么,如果我把他们嵌套在一起使用会发生什么呢?谁能最终拿到手势事件呢?

而在我这个 APP 中其中一个 Pager 页面中又额外嵌套了一个 webview 页面,这个页面也需要获取到横向滑动手势,如果此时我切换到这个页面又会发生什么呢?

实际发生的和我们希望的

在上述的场景中,实际会发生的情况是,如果我们手势滑动的位置是在中间的内容区域的话,触发的会是 Pager 的手势,从而发生页面的切换。

如果我们的手势滑动的区域是在中间内容区域之外,例如顶部菜单栏的话触发的就是 Drawer 的手势,从而发生展开 Drawer 的事件。

至于这个 webview ,显然,无论如何它都不会被触发。

那么,我们期望的处理流程是什么样的呢?

显然,我们期望的是无论滑动哪个地方的手势都应该优先触发 Pager 然后在到达第一页的时候向右滑动改为触发 Drawer 而向左滑动则依旧触发 Pager ,至于 webview 我们期望的是无论它是在哪个页面,只要我们的手指触摸到的是它的内容范围,则应该优先触发它的手势。

那么,怎么才能实现我们的目的呢?

其实在 Compose 1.2.0 版本中就已经提供了官方的嵌套滚动互操作的支持:Nested scrolling interop 。

仔细阅读这篇指南就会发现,虽然 Compose 官方提供了嵌套滚动互操作的 API 支持,大多数情况下只需要添加一个 Modifier.nestedScroll(nestedScrollInterop) 即可实现我们上述所说的需求。

然而,并非所有的组件都支持上述这个 API:

This issue is a result of the expectations built in scrollable composables. Scrollable composables have a “nested-scroll-by-default” rule, which means that any scrollable container must participate in the nested scroll chain, both as a parent via NestedScrollConnection, and as a child via NestedScrollDispatcher. The child would then drive a nested scroll for the parent when the child is at the bound. As an example, this rule allows Compose Pager and Compose LazyRow to work well together. However, when interoperability scrolling is being done with ViewPager2 or RecyclerView, since these don’t implement NestedScrollingParent3, the continuous scrolling from child to parent is not possible.

不幸的是,我这里所需要实现嵌套滚动的恰好是不受支持的组件。

所以,我们只能自己去实现了。

解决 Drawer 和 Pager 的滑动冲突

那么,我们就先从简单的开始,先去解决同为 Compose 组件的 Drawer 和 Pager 的冲突。

在开始之前,我们先明确一下我们需要解决的问题,那就是我们需要实现如果在第一页时,如果向右滑则触发 Drawer 展开侧栏;向左滑则触发 Pager 切换至下一页。如果不在第一页那么就只需要触发 Pager 切换页面。

为了解决这个问题,我首先想到的方法是如同原生 View 那样的,通过拦截触摸事件,然后在按照我们需求去重新分配触摸事件。

但是在我实际尝试过程中发现在 Compose 中想要拦截并重新分配触摸事件似乎不是那么的好实现。

所以我这里使用了一种折中的方法来实现。

由于相对于 Drawer 来说 Pager 是其子界面,所以这里我们选择给 Pager 添加一个 Modifier.draggable() 修饰,使用这个修饰我们可以获取到在 Pager 的单一方向的滑动手势,以及其滑动距离等数据:

HorizontalPager(
    // ……
    state = pagerState,
    modifier = Modifier.draggable(state = rememberDraggableState {offset ->
         // ……
         // 这里的 offset 即获取到的手势滑动的变化值
    },
        orientation = Orientation.Horizontal, // 这里表示只获取水平方向上的手势
        enabled = pagerState.currentPage == 0)
) { page ->
    // ……
}

在这里按照我们的需求,我们也给这个 Modifier.draggable 修饰加上了启用条件 enabled = pagerState.currentPage == 0 即只有当前处于第一页时才启用这个获取手势的修饰。

接下来,我们按照需求,判断手势的变化值来确定是需要展开 Drawer 还是切换 Pager ,其实判断方法也很简单,如果值为正则说明是向右滑动,则应该展开 Drawer ;如果值为负则说明是向左滑动,应该要切换 Pager 的页面:

HorizontalPager(
    // ……
    state = pagerState,
    modifier = Modifier.draggable(state = rememberDraggableState {offset ->
        if (drawerState.isClosed && !drawerState.isAnimationRunning) {
            if (offset >= 5f) {
            		// ……
            		// 在这里触发展开 Drawer
            }
            else if (offset < -5f && pagerState.canScrollForward && !pagerState.isScrollInProgress){
            		// ……
            		// 在这里触发切换页面
            }
        }
    },
        orientation = Orientation.Horizontal,
        enabled = pagerState.currentPage == 0)
) { page ->
    // ……
}

在这里我们为了避免误触,把判断的阈值设置为了 ± 5 个单位。

另外,为了避免在已经开始展开 Drawer 或切换页面时重复触发,我们首先要确保当前 Drawer 处于关闭状态,且没有处于状态变化中:

if (drawerState.isClosed && !drawerState.isAnimationRunning)

同理,当触发切换页面时也需要保证当前没有在切换过程中,且应当处于可以切换的状态:

if (offset < -5f && pagerState.canScrollForward && !pagerState.isScrollInProgress)

在这里我们分别用 drawerState.open()pagerState.animateScrollToPage(1) 来触发展开 Drawer 和切换页面。

其实这里如果想做的更“友好”一点或者说更“跟手”一点,那么我们应该是根据当前的手势滑动的值实时更新相应的布局变化值,直到达到某个阈值才认为状态变更完成,否则“弹回”未变更前。而不是像现在这样,不管滑了多少距离,直接二话不说就直接触发状态完全变化。

但是目前 DrawerPager 都没有提供相应的 API,所以只能这么粗暴的去实现了。

Draer 虽然有一个 drawerState.offset 参数,但是它是只读的)

至此,虽然不太完美,但是也实现了我们的需求,效果如下:

2.gif

解决 Pager 和 Webview 的冲突

上一节我们讲了同为 Compose 组件之间的嵌套滚动冲突的解决方法,接下来我们讲一讲 Compose 嵌套原生 View 的滑动冲突解决方法。

但是正如我在前言中所说, Compose 并不能很好的拦截并重新分配触摸事件。但是 VIew 是可以很容易的做到这一点的,因此我们的想法很简单,就是在 Webview 中拦截掉所有的触摸事件即可。

只是在这个 APP 中,webview 是被嵌套在多个有可能拦截触摸事件的 Compose 组件中的: webview -> LazyColumn -> SwipeRefresh -> Pager -> Drawer 。

好在,这几个组件都提供了禁用拦截触摸事件的参数:

LazyColumn 中有一个 userScrollEnabled 参数,当将这个参数设置为 false 时,LazyColumn 就不会再拦截触摸事件。

SwipeRefresh 中有一个 swipeEnabled 参数,当将这个参数设置为 false 时,SwipeRefresh 就不会再拦截触摸事件。

HorizontalPager 中有一个名为 userScrollEnabled 的参数,当将这个参数设置为 false 时,Pager 就不会再拦截触摸事件。

ModalNavigationDrawer 的中有一个叫 gesturesEnabled 的参数,将其设置为 false 时也不会再拦截触摸事件。

所以这里我们就从这几个参数下手,首先为 webview 设置触摸监听,如果 webview 接收到了触摸事件就回调给上述几个函数,设置其对应的参数为 false,确保其不会拦截 webview 的触摸事件,当 webview 失去触摸事件时就将其设置会 true,让他们继续相应对应组件的触摸事件。

为了实现这个目的,我们首先给封装的 webview 组件提供一个 onTouchEvent 回调:

@Composable
fun CustomWebView(
    // ……
    onTouchEvent: ((event: MotionEvent) -> Boolean)? = null
) {
    AndroidView(
        factory = { ctx ->
            WebView(ctx).apply {
                // ……

                if (onTouchEvent != null) {
                    setOnTouchListener { v, event ->
                        onTouchEvent(event)
                    }
                }
            }
        }
    )
}

在上述代码中,我们把触摸事件回调给了 onTouchEvent 函数。

因此我们在实际调用 CustomWebView 这样写:

CustomWebView(
    // ……
    onTouchEvent = {
        when (it.action) {
            MotionEvent.ACTION_DOWN -> {
                changeaScrollState(false)
                false
            }
            MotionEvent.ACTION_UP -> {
                changeaScrollState(true)
                false
            }
            else -> {
                false
            }
        }
    }
)

上述代码中,我们通过 changeaScrollState 函数设置我们一开始提到的几个 Compose 组件的触摸事件启用状态。

这里有一个地方需要注意,在 onTouchEvent 中记得一定要返回 false 表示只是获取这个触摸事件但是不消费,否则虽然触摸事件不会被其他 Compose 拦截消费了,但是却被我们自己消费了,webview 依然是接受不到这个触摸事件的。

最终实现效果如下:

3.gif

可以看到,现在 Webview 已经能够自由的滚动而不会受到几个父布局的影响了,并且当我们滑动的是 webview 以外的区域时,其他组件依旧能够正常滚动。

总结

以上就是我目前遇到的在 Compose 中的手势冲突的情况,以及我的解决方案。

完整的代码在这里: GithubAppByCompose

可以看到其实核心思路也是和使用 view 时一样,根据我们自己实际业务需求,重新分配不同的触摸事件给不同的 UI 。

不过我的处理方式实在无法称作优雅,所以各位大佬如果有更优雅的处理方式,希望能不吝赐教。

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

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

相关文章

C#TryCatch用法

前几天一个学员在学习C#与TryCatch用法时,也不知道TryCatch用法装可以用来做什么 。下面我们就详细讲讲C# 和封TryCatch用法相关知识。 C# 是一种通用、类型安全且面向对象的编程语言&#xff0c;由微软开发并在 .NET 平台上运行。TryCatch 是 C# 语言中的一个结构&#xff0c…

PYTHON在数据保存csv时文件内容乱码了怎么解决?

目录标题 前言问题1. 在打开 CSV 文件时指定编码方式2. 将数据转换成 Unicode 编码尾语 前言 嗨喽&#xff0c;大家好呀~这里是爱看美女的茜茜呐 问题 如果你的 Python 程序采集到的数据在保存成 CSV 格式的文件时出现了乱码。 那么可尝试以下解决方法&#xff1a; 1. 在打…

MySQL数据库——索引、事务、存储引擎

MySQL 索引 索引的概念 索引是一个排序的列表&#xff0c;在这个列表中存储着索引的值和包含这个值的数据所在行的物理地址&#xff08;类似于C语言的链表通过指针指向数据记录的内存地址&#xff09;。使用索引后可以不用扫描全表来定位某行的数据&#xff0c;而是先通过索引…

一个项目的测试计划模板该怎么写?【附案例】

目录 一、引言 二、测试任务 三、测试进度 四、测试资源 五、测试策略 六、测试完成标准 七、风险和约束 八、问题严重程度描述和响应时间规范 九、测试的主要角色和职责 软件测试是使用人工或者自动的手段来运行或者测定某个软件系统的过程&#xff0c;其目的在于检验…

Python进阶语法之异常处理

Python进阶语法之异常处理 在编写Python程序时&#xff0c;经常会遇到各种运行时错误&#xff0c;这些错误会导致程序终止并抛出异常。然而&#xff0c;有时我们希望程序能优雅地处理这些错误&#xff0c;而不是直接崩溃。在这种情况下&#xff0c;我们需要使用到Python的异常…

Vue中如何进行分布式存储与对象存储

Vue中如何进行分布式存储与对象存储 随着云计算和大数据时代的到来&#xff0c;分布式存储和对象存储越来越受到关注。在Vue中&#xff0c;我们可以使用不同的分布式存储和对象存储技术来存储和管理数据。本文将介绍Vue中如何进行分布式存储和对象存储。 什么是分布式存储&…

OpenGL 混合

1.简介 混合(Blending)通常是实现物体透明度的一种技术。透明就是说一个物体&#xff08;或者其中的一部分&#xff09;不是纯色(Solid Color)的&#xff0c;它的颜色是物体本身的颜色和它背后其它物体的颜色的不同强度结合。 2.丢弃片段 只想显示草纹理的某些部分&#xff0…

TDesign电商小程序模板解析01-自定义底部导航栏

目录 1 电商模板安装2 创建页面3 创建底部导航条总结 我们已经利用了两篇文章解读了一下微信小程序带的TDesign模板 TDesign小程序组件库01 TDesign小程序组件库02 入门一款前端组件库&#xff0c;如果挨个去看每个组件的用法未免比较枯燥&#xff0c;即使看懂了其实离实际开发…

从零开始 Spring Boot 44:Test

从零开始 Spring Boot 44&#xff1a;Test 图源&#xff1a;简书 (jianshu.com) 本篇文章我们讨论如何在 Spring 项目中编写测试用例。 当前使用的是 Spring 6.0&#xff0c;默认集成 JUnit 5。 依赖 Spring Boot 的测试功能需要以下依赖&#xff1a; <dependency><…

数据中心机柜PDU应该怎么选?

数据中心是国家确定的“新基建”七大领域之一。数据中心在国民经济和社会发展中所起的作用越来越重要&#xff0c;数据中心已经成为了各行各业的关键基础设施&#xff0c;为经济转型升级提供了重要支撑。在庞杂的数据中心&#xff0c;服务器和交换器担负着传输数据的重要责任&a…

【论文阅读-人机交互】通过用户参与来缓解人工智能决策中的知识失衡问题

Title: Mitigating knowledge imbalance in AI-advised decision-making through collaborative user involvement From: International Journal of Human - Computer Studies Link: https://doi.org/10.1016/j.ijhcs.2022.102977 目录 1 绪论2 方法2.1 假设2.2. 实验任务及研究…

【单片机】STM32F103C8T6 最小系统板原理图

STM32F103C8T6是一款基于ARM Cortex-M3内核的32位微控制器&#xff0c;由STMicroelectronics&#xff08;ST&#xff09;公司生产。它是STMicroelectronics的STM32系列微控制器中的一员&#xff0c;被广泛应用于嵌入式系统和电子设备中。 STM32F103C8T6单片机的主要特点和资源…

点云深度学习系列博客(七): 针对点云的数据增强技术

好长时间不更新博客了&#xff0c;入职以后突然就变忙了&#xff0c;确实有心无力。最近做一个点云数据增强的项目&#xff0c;搞了一个简单的前期调研&#xff0c;趁着最近几天不太忙&#xff0c;凑一篇博客出来&#xff0c;保一下博客专家资格... 一. 简介 我们在利用深度学…

TDesign电商小程序模板解析02-首页功能

目录 1 home.json2 goods-list组件3 goods-card组件总结 上一篇我们搭建了底部的导航条&#xff0c;这一篇来拆解一下首页的功能。首页有如下功能 可以进行搜索显示轮播图横向可拖动的页签图文卡片列表 1 home.json 因为是要使用组件库的组件搭建页面&#xff0c;自然是先需要…

Git第二章、多人协作

一、多人协作一 目前&#xff0c;我们所完成的工作如下&#xff1a; • 基本完成 Git 的所有本地库的相关操作&#xff0c;git基本操作&#xff0c;分支理解&#xff0c;版本回退&#xff0c;冲突解决等等 • 申请码云账号&#xff0c;将远端信息clone到本地&#xff0c;以及推…

简单认识Nginx网络服务

文章目录 一、简介1、概括2、Nginx和Apache的差异3、Nginx优于Apache的优点 二、编译安装nginx 服务1、在线安装nginx2、 nginx编译安装&#xff08;1&#xff09;、关闭防火墙&#xff0c;将安装nginx所需软件包传到/opt目录下&#xff08;2&#xff09;、#nginx的配置及运行需…

基于分时电价和蓄电池控制策略用电优化研究(matlab代码)

目录 1 主要内容 温控负荷模型 蓄电池模型 2 部分代码 3 程序结果 4 下载链接 点击直达&#xff01; 1 主要内容 该程序复现《基于需求侧家庭能量管理系统用电优化研究》中第三章模型&#xff0c;题目是《基于分时电价和蓄电池控制策略用电优化研究》&#xff0c;该部…

Spring Boot初阶篇笔记

SpringBoot笔记 SpringBoot官方文档 一、SpringBoot的常用注解 ConfigurationProperties、PropertySource、ImportResource的区别 1.ConfigurationProperties: ConfigurationProperties:告诉SpringBoot将本类中的所有属性与配置文件中的相关属性进行绑定; 如&#xff1a;C…

Linux权限维持方法论

Linux权限维持方法论 1.创建超级用户2.SUID后门权限维持3.Strace监听后门4.rookit后门 1.创建超级用户 例如&#xff1a; 创建一个用户名guest&#xff0c;密码123456的root用户 useradd -p openssl passwd -1 -salt salt 123456 guest -o -u 0 -g root -G root -s /bin/bas…

【C语言初阶】带你玩转C语言中的数组,并逐步实现冒泡排序,三子棋,扫雷

君兮_的个人主页 勤时当勉励 岁月不待人 C/C 游戏开发 数组的使用 前言一维数组1.一维数组的定义数组的分类 2.数组的初始化第一种越界情况 3.数组的使用数组的下标&#xff1a;第二种越界情况 4.数组在内存中的存储 二维数组1.二维数组的创建2.二维数组的初始化3.二维数组的…