降Compose十八掌之『鱼跃于渊』| Gesture Handling

公众号「稀有猿诉」        原文链接 降Compose十八掌之『鱼跃于渊』| Gesture Handling

UI是用户界面,一个最为基础的功能就是与用户进行交互,要具有可交互性。要想有可交互性就需要处理用户输入事件。手势是最为常见的一种用户输入,今天就来专门学习一下如何处理Jetpack Compose中最为常见的手势。

banner

输入事件与手势概述

在开始学习之前有必要先澄清一些概念,以免混淆。与View系统不太一样的是,触摸事件在Jetpack Compose中称之为触点事件(Pointer event),对应的主体称之为触点(Pointer),一连串的触点事件就形成了手势(Gesture)。之所以叫触点,是因为并不总是由触摸屏幕触发事件,也可以是手写笔,(外接)鼠标或者(外接)触摸板,这些都是触控类的输入主体,它的最主要的特点是发生在屏幕上的一个坐标点。其具体的类型称之为触点类型(Pointer type)。

事件处理最主要的是也就是要识别各种不同的触点手势,然后做出响应,以让UI具体可交互性。

点击事件(Tap and Press)

点击事件是最为常见,也是最为基础的一种手势了,可以简单的看成按下事件(pointer down)和抬起事件(pointer up)组成,但其实也会有移动(pointer move),只不过移动的位移特别小而已,这里我们不过多的纠结。点击事件分为单击,双击和长按,幸运的是在Compose中都有封装好的回调函数可以直接使用,我们一一来看一下。

单击(Tap/Click)

单击是最为常见的事件处理了,在之前的教程已经见过了,通过Modifier的扩展函数Modifier.clickable就可以为任意一个Composable设置单击事件处理函数。

双击(Double tap/Double click)和长按(LongPress/Long click)

对于双击和长按,并不像clickable那样常用,因此需要用到另外一个扩展函数Modifier.combinedClickable,这个函数可以设置多个点击事件处理函数,单击双击和长按都可以通过它来设置:

    Box(modifier = Modifier.size(100.dp).background(Color.Yellow).combinedClickable(onClick = { gotoDetail() },onClickLabel = "Go to details",onLongClick = { showContextMneu() },onLongClickLabel = "Open context menu",onDoubleClick = { shareContent() }))

滚动(Scroll)

滚动手势是指朝着某一固定的方向慢速的滑动,多用于查看屏幕之外的内容。像集合性布局设计的目的就是为了显示大量的同一类型的数据集合,天生就支持滚动。对于滚动手势需要处理的就是常规布局支持滚动,以及滚动的嵌套。

非集合性布局支持滚动

对于常规的非集合性布局(Box,Row和Column)正常情况下是不可滚动的,是没有办法查看超出其尺寸大小范围的内容的。想让这几个布局可滚动也不难,用Modifier的扩展函数verticalScroll和horizontalScroll就可以让不可滚动布局(Box,Row和Column)支持垂直方向滚动和水平方向滚动:

@Composable
private fun ScrollBoxes() {Column(modifier = Modifier.background(Color.LightGray).size(100.dp).verticalScroll(rememberScrollState())) {repeat(10) {Text("Item $it", modifier = Modifier.padding(2.dp))}}
}

gestures-simplescroll.gif

大部分情况下,如果只是想让布局可滚动就不需要处理ScrollState,但如果想要获取滚位置,或者改变滚动位置,比如说页面进入时(Initial composition)自动滚动到某一们位置,可以通过修改SrollState来实现:

@Composable
private fun ScrollBoxesSmooth() {// 进入页面时就自动的平滑的滚动val state = rememberScrollState()LaunchedEffect(Unit) { state.animateScrollTo(100) }Column(modifier = Modifier.background(Color.LightGray).size(100.dp).padding(horizontal = 8.dp).verticalScroll(state)) {// ...}
}

滚动手势处理

对于任意的Composable来文章,都可以通过Modifier的扩展函数scrollable来监听并处理滚动手势。需要注意的是,scrollable仅会告诉你有滚动手势发生和当前的滚动距离,但并不会直接修改布局,需要开发者去使用滑动距离进行布局的修改:

@Composable
private fun ScrollableSample() {// actual composable statevar offset by remember { mutableStateOf(0f) }Box(Modifier.size(150.dp).scrollable(orientation = Orientation.Vertical,// Scrollable state: describes how to consume// scrolling delta and update offsetstate = rememberScrollableState { delta ->offset += deltadelta}).background(Color.LightGray),contentAlignment = Alignment.Center) {Text(offset.toString())}
}

handle_scrollable

如果让滚动对布局产生影响,可以用计算得到offset去改变布局的offset属性offset(y = offset.dp)就可以了。

滚动嵌套

手势处理最大的一个麻烦就是手势的嵌套,而又以滚动的嵌套最为麻烦,最为典型的就是同一方向的列表中套着列表,开发者必须手动处理滑动冲突。滚动冲突处理的策略并不难,优先由子View消费滚动事件,当子View还可以滚动时,就把事件消费掉;如果子View已到达边界,无法滚动时,视为事件未消费,把事件再传递给父View,由父View消费,这时父View会进行滚动;当然如果滑动事件没有发生在子View上面,那肯定 是父View滚动。

策略虽然简单,但有魔鬼细节,传统的View必须要在onTouch和onInterceptTouch里面写上大坨大坨的逻辑,还要定义很多个全局变量。幸运的是,针对 于同方向的可滚动布局嵌套,Jetpack Compose已经帮我们处理了。对于使用verticalScroll,horizontalScroll,scrollable,集合性布局(LazyRow,LazyColumn和LazyGrid)和TextField实现的同方向滚动嵌套,不用再特殊处理,Compose已经按照前面说的策略处理好了,这就是自动嵌套滚动机制(Automatic nested scrolling)。来看一个例子:

@Composable
private fun AutomaticNestedScroll() {val gradient = Brush.verticalGradient(0f to Color.Yellow, 1000f to Color.Red)Column(modifier = Modifier.fillMaxWidth().height(400.dp).background(Color.LightGray).verticalScroll(rememberScrollState()).padding(32.dp),horizontalAlignment = Alignment.CenterHorizontally,verticalArrangement = Arrangement.spacedBy(16.dp)) {repeat(6) {Box(modifier = Modifier.height(128.dp).verticalScroll(rememberScrollState())) {Text("$it 滑动试试!",modifier = Modifier.align(Alignment.Center).border(12.dp, Color.DarkGray).background(brush = gradient).padding(24.dp).height(150.dp))}}}
}

ascroll.gif

这个例子中外层Column支持垂直滚动,里面的每个Box也支持垂直滚动,当里面的Box自己消费滚动时,外层 是不会动的,而当里面的Box无法滚动时(overscrolled)事件就到了外层的Column,即Column会滚动。

注意: 滚动嵌套并不是一个好的交互设计,尽管有技术手段解决,但用起来仍旧是怪怪的,操作起来也并不方便,误操作的可能性很大。不同方向的滚动嵌套在一起是比较好的方案,比如横向的Tab页代表不同的分类,竖向的内容页是一个分类中的具体内容,内容是竖向的,内容中仍旧可以有一些横向滑动的扩展内容,如图片库,tag标签等。

拖拽(Drag)

拖拽是指按住屏幕慢速移动,被点击到的UI元素应该跟随手势移动并停留在触点离开屏幕的地方。通过扩展函数Modifier.draggable可以处理单一方向的拖拽手势。在draggable中我们可以用状态记录移动的距离,然后把距离应用到Composable的offset以生成拖拽后的效果:

@Composable
private fun DraggableText() {var offsetX by remember { mutableStateOf(0f) }Text(modifier = Modifier.offset { IntOffset(offsetX.roundToInt(), 0) }.background(Color.LightGray).padding(8.dp).draggable(orientation = Orientation.Horizontal,state = rememberDraggableState { delta ->offsetX += delta}),text = "降Compose十八掌!",style = MaterialTheme.typography.headlineLarge,color = MaterialTheme.colorScheme.primary)
}

drag.gif

滑动(Swipe/Fling)

滑动与拖拽的区别在于滑动是有速度的,滑动手势在触点离开屏幕后并不会立即停止,而且是会继续朝原方向减速直到速度变为0才停,最为常见的交互方式就是滑动删除(swipe-to-dismiss),以及像列表的Fling手势。

使用Modifier的扩展函数anchoredDraggable来处理滑动事件,定义一些锚点(DraggableAnchors),视为一个手势操作中的不同状态,比如像滑动开关,就是开和关,像滑动删除就是正常和已删除,再用一个AnchoredDraggableState来追踪滑动的状态,这里面可以定义初始锚点,锚点值,和终止状态的阈值(positionalThreshold超过一定位置就认为到达终点锚点,velocityThreshold速度小于这个时就认为到达终点锚点),以及手势过程中的动画(animationSpec)。然后,再把AnchoredDraggableState中的滑动距离offset设置到Composable中即可。

说的挺复杂,其实很直观,看一个例子就明了:

enum class SwipeableSwitchState {SWITCH_ON, SWITCH_OFF
}@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun SwipeableSample() {val width = 128.dpval squareSize = 64.dpval density = LocalDensity.currentval sizePx = with(density) { squareSize.toPx() }val anchors = DraggableAnchors {SwipeableSwitchState.SWITCH_ON at sizePxSwipeableSwitchState.SWITCH_OFF at 0f}val swipeableState = remember {AnchoredDraggableState(initialValue = SwipeableSwitchState.SWITCH_OFF,anchors = anchors,positionalThreshold = { d: Float -> d * 0.4f },velocityThreshold = { with(density) { 100.dp.toPx() } },animationSpec = tween())}Box(modifier = Modifier.width(width).anchoredDraggable(state = swipeableState,orientation = Orientation.Horizontal,startDragImmediately = false).background(Color.LightGray)) {Box(Modifier.offset {IntOffset(if (swipeableState.offset.isNaN()) 0 else swipeableState.offset.roundToInt(),0)}.size(squareSize).background(Color.DarkGray))}
}

swipe.gif

这个例子展示了一个滑动开关的手势处理,滑动距离超过整体长度0.4时,或者速度小于100时就认为到达另一锚点状态。可以明显的看出与拖拽的区别,滑动后手可以离开,但手势仍在继续直到达到终点锚点。

注意: 在Compose 1.6版本以前有另外一个扩展函数swipeable来处理滑动手势,但在1.6版本时已废弃,被anchoredDraggable取代,并且有一个替换的教程。

未完待续

事件处理对于UI来说是极其重要的,本篇重点讲述了Jetpack Compose中的最为基础和最为常见的事件处理方式,足以满足绝大多数应用场景。事件处理也是极其复杂的,对于交互极其复杂的页面来说,还需要进一步的了解更为底层的事件处理方法,以达到复杂交互的目的,将在后面的文章中继续深入探讨事件处理。

References

  • Tap and press
  • Drag, swipe, and fling
  • Scroll
  • How to Implement Swipe-to-Action using AnchoredDraggable in Jetpack Compose
  • Exploring Jetpack Compose Anchored Draggable Modifier
  • Jetpack Compose: Anchored Draggable Item in MotionLayout Part 1
  • Jetpack Compose: Anchored Draggable Item in MotionLayout Part 2

subscription

欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!

保护原创,请勿转载!

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SAPUI5基础知识18 - 自定义CSS和主题色

1. 背景 在上一篇博客中,我们通过使用SAPUI5提供的CSS类实现元素间距的调整。在本篇博客中,让我们看一下如何实现自定义的CSS样式。 2. 背景知识 2.1 CSS基础语法 CSS,全称为级联样式表(Cascading Style Sheets)&a…

6.6 使用dashboard商城搜索导入模板

本节重点介绍 : 模板商城中搜索模板导入模板修改模板 大盘模板商城地址 免费的 地址 https://grafana.com/grafana/dashboards 搜索模板技巧 详情 导入dashboard 两种导入模式 url导入id导入json文件导入 导入 node_exporter模板 https://grafana.com/grafana/dashboa…

“葫芦娃”竟上了SCI论文!当童年碰上科研,你还会觉得科研枯燥吗?

本周投稿推荐 SCI • 能源电力类,1.5-2.0(25天来稿即录) • 1区计算机类,3.5-4.0(1个月录用) • CCF推荐,1区-Top(3天初审) EI • 各领域沾边均可(2天录…

[CISCN2019 华东南赛区]Web11

进来先做信息收集,右上角显示当前ip,然后有api的调用地址和请求包的格式以及最重要的是最下面的smarty模版,一看到这个就得想到smarty模版注入 测试了一下两个api都无法访问 直接切到数据包看看能不能通过XFF来修改右上角ip 成功修改&#x…

Flink笔记整理(四)

Flink笔记整理(四) 文章目录 Flink笔记整理(四)六、Flink中的时间和窗口6.1 窗口(Window)窗口的概念窗口的分类窗口API概览窗口分配器窗口函数(Window Functions) 6.2 时间语义&…

Windows电脑如何启动RTSP服务实现本地摄像头数据共享

技术背景 提起Windows共享本地摄像头,好多人想到的是通过ffmepg或vlc串流到服务器,实际上,用轻量级RTSP服务更简单,本文就介绍下,如何用大牛直播SDK的Windows轻量级RTSP服务,采集摄像头,生成本…

React Router-v6.25.1

以下例子是根据vitereactts构建的,使用路由前先安装好这些环境!!!! 1、路由的简单使用 首先要创建一个浏览器路由器并配置我们的第一个路由。这将为我们的 Web 应用启用客户端路由。 该main.jsx文件是入口点。打开它…

什么是大型语言模型 (LLM)

本章探讨下,人工智能如何彻底改变我们理解和与语言互动的方式 大型语言模型 (LLM) 代表了人工智能的突破,它采用具有广泛参数的神经网络技术进行高级语言处理。 本文探讨了 LLM 的演变、架构、应用和挑战,重点关注其在自然语言处理 (NLP) 领…

Unity XR Interaction Toolkit设置或监听手柄按键事件(三)

提示:文章有错误的地方,还望诸位大神不吝指教! 文章目录 前言一、XRI Default Input Actions1.导入官方案例2.设置控制器绑定,如手柄、主/辅助按钮、操纵杆等1.要设置控制器绑定,如左右手 手柄、主/辅助按钮、操纵杆等…

UART编程框架详解

1. UART介绍 UART:通用异步收发传输器(Universal Asynchronous Receiver/Transmitter),简称串口。 调试:移植u-boot、内核时,主要使用串口查看打印信息 外接各种模块 1.1 硬件知识_UART硬件介绍 UART的全称是Unive…

微信小程序教程001:小程序简介

文章目录 学习目标小程序简介1、小程序和普通网页开发的区别2、注册小程序账号3、获取小程序的AppID4、安装开发者工具4.1 了解开发者工具4.2 下载开发工具 5、设置开发者工具外观 学习目标 如何创建小程序项目小程序项目的基本组成结构小程序页面由几部分组成小程序常见的组件…

小模型狂飙!6家巨头争相发布小模型,Andrej Karpathy:大语言模型的尺寸竞争正在倒退...

过去一周,可谓是小模型战场最疯狂的一周,商业巨头改变赛道,向大模型say byebye~。 OpenAI、Apple、Mistral等“百花齐放”,纷纷带着自家性能优越的轻量化小模型入场。 小模型(SLM),是相对于大语言模型(LLM…

sql注入详解【从数据库架构分析】

文章目录 简介数据库的架构sql注入概念正常语句正常回显页面在页面中使用sql语句 跨库查询sql文件读写影响条件复现读写的路径的问题 sql注入请求分类sql注入请求类型sql注入请求方式:sql注入数据请求格式 数据库的增删改查数据库查询数据库添加数据库删除数据库修改…

拓扑排序+dp(消除主观臆断)

这题一开始写错的原因就是搞错了&#xff0c;处于西边的节点的编号不一定小&#xff0c;不能直接dp&#xff0c;要先进行拓扑排序 写到一般我才发现&#xff0c;其实可以一边dp&#xff0c;一边进行dp #define _CRT_SECURE_NO_WARNINGS #include<bits/stdc.h> using name…

GPT-4o mini 震撼登场:开发者的新机遇与挑战

GPT-4o mini 震撼登场&#xff1a;开发者的新机遇与挑战 一、引言二、GPT-4o mini 模型的卓越性能三、极具竞争力的价格优势四、开发者的探索与实践五、提升开发效率和创新能力的策略六、面临的挑战与应对措施七、未来展望八、总结 在科技的浪潮中&#xff0c;OpenAI 最新推出的…

论文快过(图像配准|Coarse_LoFTR_TRT)|适用于移动端的LoFTR算法的改进分析 1060显卡上45fps

项目地址&#xff1a;https://github.com/Kolkir/Coarse_LoFTR_TRT 创建时间&#xff1a;2022年 相关训练数据&#xff1a;BlendedMVS LoFTR [19]是一种有效的深度学习方法&#xff0c;可以在图像对上寻找合适的局部特征匹配。本文报道了该方法在低计算性能和有限内存条件下的…

改进智能优化算法中的一个常见错误

声明&#xff1a;文章是从本人公众号中复制而来&#xff0c;因此&#xff0c;想最新最快了解各类智能优化算法及其改进的朋友&#xff0c;可关注我的公众号&#xff1a;强盛机器学习&#xff0c;不定期会有很多免费代码分享~ ​昨天看到网上有一个流传很广的改进鲸鱼优化算法M…

vue3 使用Mock

官网: http://mockjs.com/ 安装 npm install mockjs -Dsteps1: main.js 文件引入 import /api/mock.jssteps2: src/api/mock.js import Mock from mockjs import homeApi from ./mockData/home /*** 1.拦截的路径:mock拦截了正常NetWork/网络请求,数据正常响应* 2.方法* …

货架管理a

路由->vue的el标签->Api->call方法里calljs的api接口->数据声明const xxxData-> 编辑按钮:点击跳出页面并把这一行的数据给到表单formDataba2 保存按钮:formDataba2改过的数据->xxApi发送->查询Api 跳转仓库:把tableData.value数据清空->callXxxAp…

传输层协议——TCP

TCP协议 TCP全称为“传输控制协议”&#xff0c;要对数据的传输进行一个详细的控制。 特点 面向连接的可靠性字节流 TCP的协议段格式 源/目的端口&#xff1a;表示数据从哪个进程来&#xff0c;到哪个进程4位首部长度&#xff1a;表示该TCP头部有多少字节&#xff08;注意它…