React Native 中的指针事件
今天,我们分享一个 React Native 的实验性跨平台指针 API。我们将介绍其动机、工作原理以及对 React Native 用户的益处。其中包含如何启用的说明,我们很高兴听到您的反馈!
自从我们分享了我们的多平台愿景以来,已经过去一年多了,我们阐述了在移动端之外构建的优势,以及它如何为所有平台设定更高的标准。在此期间,我们增加了对 React Native 在 VR、桌面和 Web 领域的投资。由于这些平台在硬件和交互方面的差异,引发了一个问题,即 React Native 应该如何全面地处理输入。
超越触摸
桌面和 VR 历史上依赖鼠标和键盘输入,而移动设备主要依赖触摸。但随着触摸屏笔记本电脑的出现,以及在移动设备上通过键盘和笔支持交互的需求不断增长,这种说法已经发生了变化。所有这些都是 React Native 触摸事件系统无法处理的。
因此,树外平台的用户 fork 了 React Native 和/或创建自定义原生组件和模块,以支持悬停检测或左键单击等关键功能。这种分歧导致了 prop 冗余,事件处理程序的功能相似,但用于不同的平台。这增加了框架的复杂性,并使跨平台代码共享变得繁琐。由于这些原因,团队有动力提供跨平台指针 API。
React Native 旨在提供强大且富有表现力的 API,以便为多个平台构建应用,同时保持各平台的特性体验。设计这样的 API 具有挑战性,但值得庆幸的是,在指针领域已经有先例,React Native 可以借鉴。
借鉴 Web
Web 是一个平台,在扩展到多个平台方面面临类似的挑战,同时还要考虑面向未来的设计。万维网联盟 (W3C) 的任务是制定标准和提案,以构建一个在不同平台和浏览器之间可互操作的 Web。
与我们的需求最相关的是,W3C 为一种抽象的输入形式定义了行为,称为指针。《指针事件》规范建立在鼠标事件的基础上,旨在为跨设备指针输入提供一组统一的事件和接口,同时在必要时仍然允许设备特定的处理。
遵循《指针事件》规范为 React Native 用户带来了许多好处。除了解决前面提到的问题外,它还提高了那些历史上不必考虑多输入类型交互的平台的能力。例如,将蓝牙鼠标连接到您的 Android 手机,或者 Apple Pencil 在 iPad M2 上支持悬停。
符合规范也为 Web 和 React Native 之间的知识共享提供了机会。关于指针事件的 Web 期望的教育可以双重地服务于 React Native 开发人员。但是,我们也认识到 React Native 的要求与 web 不同,我们对规范的态度是尽力而为,并充分记录偏差,以便明确期望。还有相关工作是将某些 Web 标准与减少 API 碎片化对齐,包括无障碍和性能 API。
移植 Web 平台测试
虽然《指针事件》规范提供了 API 的接口和行为描述,但我们发现它不够具体,无法让我们自信地进行更改并以规范作为验证。然而,Web 浏览器使用另一种机制来确保合规性和互操作性——Web 平台测试!
Web 平台测试旨在针对浏览器的命令式 DOM API 进行工作——React Native 不支持这一点,因为它使用自己的视图原语。这意味着我们无法与浏览器代码共享测试,而是为 React Native 提供类似的测试 API,这使得移植这些 Web 平台测试变得更容易。
我们实现了一个新的手动测试框架,我们现在使用它通过 RNTester 验证我们的实现。这些测试暂定名为 RNTester 平台测试,并且仍然相当基础。我们的实现提供了一个 API,可以将测试用例构建为组件本身,这些组件被渲染,并且结果仅通过 UI 报告。
当我们进一步完善指针事件的实现时,这些测试将继续有所帮助。这些测试还将扩展到测试 Android 和 iOS 以外平台上的指针事件实现。随着我们套件中测试数量的增加,我们将寻求自动化运行这些测试,以便我们更好地发现实现中的回归。
工作原理
我们的大部分指针事件实现都建立在用于调度触摸事件的现有基础设施之上。在 Android 和 iOS 上,我们利用相关的 MotionEvent 和 UITouch 事件。事件调度的总体流程如下图所示。
以 Android 为例,利用平台事件的通用方法是
- 遍历
MotionEvent
的所有指针,并进行深度优先搜索,以确定每个指针的目标 React 视图及其祖先路径。 - 将
MotionEvent
的类别映射到相关的指针事件。MotionEvent
和PointerEvent
之间存在一对多的关系。在它们关系的图中,虚线表示如果指点设备不支持悬停则触发的事件。
- 使用来自
MotionEvent
的平台详细信息和先前交互的缓存状态构建PointerEvent
接口。(例如button
属性) - 将指针事件从 Android 调度到 React Native 的 核心事件队列,并利用 JSI 调用
dispatchEvent
方法,该方法位于react-native-renderer
中,它会遍历 React 树以进行事件的冒泡和捕获阶段。
实现进度
在实现《指针事件》规范的当前进展方面,我们专注于最常见事件的可靠基线实现,这些事件处理诸如按下、悬停和移动等操作。
事件
已实现 | 正在进行中 | 尚未实现 |
---|---|---|
onPointerOver | onPointerCancel | onClick |
onPointerEnter | onContextMenu | |
onPointerDown | onGotPointerCapture | |
onPointerMove | onLostPointerCapture | |
onPointerUp | onPointerRawUpdate | |
onPointerOut | ||
onPointerLeave |
onPointerCancel 已连接到原生平台的“cancel”事件,但这不一定与 web 平台期望它们触发的时间相对应。
事件属性
对于上面提到的每个事件,我们还实现了 PointerEvent 对象中预期的大多数属性——尽管在 React Native 中,这些属性通过 event.nativeEvent
属性公开。您可以在事件对象的 Flowtype 接口定义中找到所有已实现属性的枚举。一个值得注意的未完全实现的例外是 relatedTarget
属性,因为以这种临时方式公开原生视图引用并非易事。
未来工作和探索
除了上述事件之外,还有一些与指针事件相关的其他 API。未来,我们计划将这些 API 作为这项工作的一部分来实现。这些 API 包括
- 指针捕获 API
- 包括在元素引用上公开的命令式 API,包括
setPointerCapture()
、releasePointerCapture()
和hasPointerCapture()
。
- 包括在元素引用上公开的命令式 API,包括
touch-action
样式属性- web 使用此 CSS 属性以声明方式协商浏览器和网站自身事件处理代码之间的手势。在 React Native 中,这可以用于协商 View 的指针事件处理程序和父 ScrollView 之间的事件处理。
click
,contextmenu
,auxclick
click
是交互的抽象定义,可以通过无障碍范例或其他特性平台交互触发。
原生指针事件实现的另一个好处是,它将允许我们重新审视和改进目前仅限于触摸事件的各种手势处理形式,这些手势处理形式目前由 Responder、Pressability 和 PanResponder API 在 JavaScript 中处理。
此外,我们正在继续探索为 React Native 主机组件(即 add
/removeEventListener
)包含 EventTarget
接口的实现,我们相信这将为处理指针交互提供更多用户层面的抽象。
尝试使用
我们的指针事件实现仍处于实验阶段,但我们有兴趣获得社区对我们分享内容的反馈。如果您有兴趣尝试此 API,您需要启用一些功能标志
启用功能标志
指针事件仅为 新架构 (Fabric) 实现,并且仅适用于 React Native 0.71+,在撰写本文时,这是一个候选版本。
在您的入口 JavaScript 文件(默认 React Native 应用模板中的 index.js)中,您需要为指针事件启用 shouldEmitW3CPointerEvents
标志,并启用 shouldPressibilityUseW3CPointerEventsForHover
以在 Pressability 中使用指针事件。
import ReactNativeFeatureFlags from 'react-native/Libraries/ReactNative/ReactNativeFeatureFlags';
// enable the JS-side of the w3c PointerEvent implementation
ReactNativeFeatureFlags.shouldEmitW3CPointerEvents = () => true;
// enable hover events in Pressibility to be backed by the PointerEvent implementation
ReactNativeFeatureFlags.shouldPressibilityUseW3CPointerEventsForHover =
() => true;
iOS 特有
为了确保指针事件从原生 iOS 渲染器发送,您需要在原生应用的初始化代码(通常是 AppDelegate.mm
)中翻转一个原生功能标志。
#import <React/RCTConstants.h>
// ...
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
RCTSetDispatchW3CPointerEvents(YES);
// ...
}
请注意,为了确保指针事件实现可以区分 iOS 上的鼠标和触摸指针,您需要在 Xcode 项目的 info.plist
中添加 UIApplicationSupportsIndirectInputEvents
。
Android 特有
与 iOS 类似,Android 也有一个功能标志,您需要在应用的初始化中启用它——通常是根 React 活动或 surface 的 onCreate
。
import com.facebook.react.config.ReactFeatureFlags;
//... somewhere in initialization
@Override
public void onCreate() {
ReactFeatureFlags.dispatchPointerEvents = true;
}
JavaScript
function onPointerOver(event) {
console.log(
'Over blue box offset: ',
event.nativeEvent.offsetX,
event.nativeEvent.offsetY,
);
}
// ... in some component
<View
onPointerOver={onPointerOver}
style={{height: 100, width: 100, backgroundColor: 'blue'}}
/>;
欢迎反馈
如今,指针事件已在我们的 VR 平台中使用,并为 Oculus Store 提供支持,但我们也期待早期社区对我们的方法以及我们目前实现的反馈。我们很高兴与您分享我们进一步的进展,如果您对此工作有任何疑问或想法,请加入我们在指针事件上的专门讨论。