[控场AI]
· 8 分钟阅读· 4,423 字

Jetpack Compose如何将输入交互扩展至触屏之外

Jetpack Compose如何将输入交互扩展至触屏之外

Compose扩展指针输入体系,统一触控板、智能眼镜等多类输入并保持开箱即用。

本文基于Android开发者峰会分享,梳理Compose在多输入形态下的演进。文章首先指出触屏只是输入方式之一,物理键盘、鼠标、触控板、手写笔、手柄乃至智能眼镜和XR设备的手势追踪都需被认真对待,且无障碍要求使输入兼容成为基本义务。随后详解指针事件的六阶段管线:Motion Event转换、时间维度合并、命中路径测试、坐标变换以及initial/main/final三趟分发,这是所有手势的基础。重点改进包括:Compose 1.12起将触控板指针类型从touch改为mouse;拦截系统“假手指”并重解释为pan/scale事件,使缩放正确作用于光标下元素;为智能眼镜引入indirect输入类型和primary directional motion axis,把横向滑动映射为列表前进/后退。这些改进对使用Foundation手势检测器的开发者开箱即用,并配套新增测试API。

为什么输入类型不再只是触屏

在Android开发者峰会上,Compose团队软件工程师Alex Van Nachenberg与Jeremy Walker分享了一个常被忽视的事实:触屏只是移动应用输入方式的一种。物理键盘、鼠标、触控板、手写笔、手柄、旋钮设备早已存在,而智能眼镜上的触控板、XR中的眼动与手势追踪、Wear设备上的手指与手腕手势,正在成为新的输入形态。

两位工程师强调,不能因为某类设备通常搭配某种输入方式,就假设该设备只使用这一种输入。Google Book有内置触控板和键盘,但这不意味着这些输入是它独有的——手机、平板和电视同样可能接上物理键盘、触控板或鼠标,部分电视甚至配有触摸屏。另一个关键理由是无障碍:让用户使用自己选择的输入设备并随时切换,是应用可访问性的基本要求,在某些司法管辖区,键盘和鼠标的兼容行为甚至可能是法律强制要求。

Compose的设计哲学是:标准组件应开箱即用地正确处理各类输入,开发者无需做特殊处理;同时提供足够工具,让有定制需求的场景能够调整行为。随着底层平台支持更多输入类型,Compose组件会持续吸纳这些支持——开发者只需升级Compose版本,应用行为就会自动改善。

指针事件的六阶段处理管线

要理解Compose如何改进触控板支持,需要先看清指针事件从系统到应用的完整旅程。Alex将其拆解为六个阶段。

第一阶段:Motion Event进入Compose。 Compose本质上是一个复杂的View,运行在普通窗口和Activity中,因此它首先接收到的是Android平台的Motion Event。Compose将这些事件翻译成内部平台无关的表示——Pointer Input Event。

第二阶段:加入时间维度。 输入往往随时间连续发生,手势、速度追踪、slop检测都需要理解每个时间点的状态。Compose将当前事件与前一事件结合,生成带时间信息的内部指针事件,其核心是一个从指针ID到PointerInputChange的映射。PointerInputChange是公开类型,而多指触控意味着指针会不断出现和消失,这套机制需要持续跟踪。

指针输入变化事件生产器

第三阶段:命中路径测试。 知道了事件位置,还需要确定把它发给谁。Compose中的布局节点有坐标、边界和形状,且可交互元素常嵌套在另一些可交互元素中——按钮放在列表里就是典型场景。因此一个输入事件会生成一条穿过所有被命中节点的完整命中路径。多指同时按下时,多条命中路径还会组成一棵命中路径树,需要随时间维护。

第四阶段:坐标变换。 节点应当在自己的局部坐标系中接收输入,避免开发者手动做坐标运算。Compose将指针事件与目标节点的坐标结合,生成最终分发的公开Pointer Event。

第五阶段与第六阶段:三趟分发。 事件分发分为initial、main、final三趟。initial从祖先到后代,适合滚动容器在开始滚动时拦截事件、阻止子节点继续接收;main是唯一从后代到祖先的一趟,是处理点击等主要逻辑的地方,嵌套可点击元素时子节点先获得机会;final再次从祖先到后代,让子节点得知父节点消费了事件的一部分,从而取消自己已开始的点击。

这套六阶段管线是所有手势处理的基础——clickable、scrollable、draggable以及自定义指针输入,全部构建于其上。

Compose的指针输入管线之所以要有一个平台无关的内部表示,根源在于它需要同时运行在多种宿主环境上。虽然当前主要宿主是Android View系统,但Compose的设计目标并不局限于单一平台。将Android的Motion Event翻译为Pointer Input Event,实质上是把带有平台特性的原始事件抽象为一组与具体窗口系统解耦的数据结构,后续的命中测试、坐标变换和分发都基于这套抽象进行。这样一来,同一套手势逻辑不必关心事件来自触摸屏、鼠标还是触控板,只需处理统一的指针标识、位置和状态。这也是为什么新增输入类型(如indirect)能够以较低成本接入现有管线——它复用了同一套事件表示和分发机制,只在语义层做区分。

笔记本触控板:从“假手指”到真实指针类型

Alex重点介绍了两项触控板改进。在Compose 1.12之前,触控板事件被上报为touch类型,因为用户确实是用手指在触控板上操作。但用户的预期是它更像鼠标。Compose将指针类型从touch改为mouse,使任何依赖该信息的组件获得更一致的鼠标与触控板体验——工具提示就是典型例子。

触控板与鼠标的工具提示行为应保持一致

更复杂的是多指手势。此前Compose直接透传Android平台的“假手指”机制:双指平移时,系统在光标位置生成一根假触摸屏手指,按相应距离和方向移动后抬起。双指上滑被翻译成假手指上移,进而触发列表向下滚动;如果用户开启了反向滚动,系统只需翻转假手指的方向。缩放同理,系统生成两根假手指相互靠近或远离。

问题在于,假手指可能落在目标元素边界之外。当用户双指缩放一张图片时,其中一根假手指的命中路径可能不经过图片,结果变成滚动而非缩放,甚至带动外层可滚动容器一起滚动。从Compose 1.12(平移)和1.13(缩放)起,Compose在事件转换阶段拦截这些假手指,将其重新解释为pan start/pan move/pan end和scale start/scale change/scale end,并把平台定义的手势直接发送到光标位置对应的命中路径。这样无论光标离元素边缘多近,缩放都能正确作用于光标下的元素。

这些改进默认作用于scrollable和transformable等基础组件,使用它们的上层组件升级Compose即可受益。编写自定义指针输入修饰符的开发者被鼓励适配新API。Compose同时新增了performTrackpadInput测试API,形态与既有的鼠标、触摸测试API相似,便于验证应用在不同输入设备下的行为。

“假手指”机制是Android平台为兼容旧应用而采用的一种折中方案:由于早期大量应用只认识触摸事件,系统在收到触控板的多指手势时,会伪造出触摸屏上的手指运动,让这些应用无需修改就能获得基本的滚动和缩放能力。但这种转换丢失了原始手势的语义——应用无法区分“用户真的用手指触摸屏幕”和“用户在触控板上做双指滑动”。当需要做更精细的判断时,比如判断缩放是否应该作用于光标下的图片,假手指的落点就可能偏离用户意图。Compose 1.12和1.13的改进本质上是绕过这层兼容转换,在事件进入Compose管线后、命中测试之前,把假手指序列还原为具有明确语义的pan和scale手势,再基于光标位置而非假手指位置做命中测试,从而让手势目标与用户视觉焦点保持一致。

智能眼镜:用一维手势导航二维界面

Jeremy Walker接棒,讲述了一种截然不同的触控板——智能眼镜侧面的触控区域。它竖直、很宽、极短,用户视野前方是Glass UI,内容简洁地叠加在视野中,焦点落在中央元素上,用户需要的是选中、前进或后退,而非精确的屏幕坐标。

在眼镜界面中选中或移动到另一个元素

Android生态中已有可借鉴的方案:电视遥控器的方向键、游戏手柄的十字键与摇杆、键盘导航,都是在一组UI元素间移动并选中;与电视配对的触控板使用navigation touch,X/Y坐标与屏幕完全解耦,仅用于手势检测——右滑相当于按右方向键。

Compose的API设计选择了贴近既有指针事件的形态:新增indirect类型,与touch结构相同,包含指针按下/移动的列表,但语义上明确表示“这不是对屏幕的直接触摸”,而是元素间的导航手势。

随后团队发现还需要一个概念:primary directional motion axis。眼镜触控板宽而短,用户研究显示,人们最自然的动作是沿眼镜侧面横向滑动,且预期横向滑动驱动内容的垂直移动——向前翻或向后翻列表。于是设备可以声明自己的主轴为水平方向,Compose将水平滑动映射为forward/back,再对应到焦点系统的next/previous。这在垂直和水平列表上都能开箱即用。

横向滑动驱动垂直导航的直观理解

由于设备本身最清楚自己的形状和轴向需求,团队将这一概念回溯到平台层,新增API让设备声明该属性,同时保留原有面向电视的touch navigation实现不被破坏,并新增字段区分眼镜输入类型。对开发者而言,使用Foundation手势检测器即可自动获得支持;专为眼镜开发时可使用Glimmer库中的专用手势检测器,直接获得click、forward和back。测试方面,由于无法通过程序构造indirect类型的Motion Event,团队重写了UI测试模块间的通信与队列机制,最终提供的performIndirectInput API与触摸测试高度相似,额外参数用于指定轴向和触控板尺寸。

indirect指针类型与普通touch类型在数据结构上几乎一致,都包含指针按下、移动、抬起的序列,但语义约定不同:touch表示手指直接接触屏幕某点,坐标即目标位置;indirect则表示输入发生在与屏幕解耦的表面上,坐标只用于识别手势方向与幅度,不直接对应屏幕上的像素位置。这一区分让Compose可以在不破坏现有触摸逻辑的前提下,为导航类输入提供独立的处理路径。配合primary directional motion axis,设备可以声明其触控面的主导轴向,框架据此把一维滑动映射为焦点系统中的next/previous,而不是要求应用自己解释原始坐标。这种“设备声明能力、框架做映射”的分工,使得同一套手势检测器能同时服务电视遥控器、手柄和智能眼镜等多种形态。

对开发者的意义

两位工程师的总结指向三点:使用Foundation手势检测器时,新输入类型开箱即用;Compose会继续在平台与工具包两侧探索和支持新输入,眼动与手势追踪的相关工作已在推进;测试API保持熟悉的接口形态,降低迁移成本。对应用团队来说,最实际的行动是升级Compose版本,并检查自定义指针输入代码是否适配了新的指针类型与手势API。

#Jetpack Compose#指针输入#触控板支持#智能眼镜#输入交互#Android开发#手势检测
分享:

相关推荐