React Native 中的指针事件
今天,我们将分享一个用于 React Native 的实验性跨平台指针 API。我们将讨论其动机、工作原理以及它给 React Native 用户带来的好处。其中包含启用说明,我们很高兴听到您的反馈!
距离我们分享我们的多平台愿景已逾一年,其中讨论了超越移动端进行开发的优势以及它如何为所有平台设定更高的标准。在此期间,我们增加了对 VR、桌面和 Web 平台上的 React Native 的投入。由于这些平台在硬件和交互方面存在差异,这引发了 React Native 应如何全面处理输入的问题。
超越触摸
桌面和 VR 历来依赖鼠标和键盘输入,而移动端则主要依赖触摸。随着触摸屏笔记本电脑的出现以及在移动设备上支持通过键盘和笔进行交互的需求不断增长,这种叙述已经发生了演变。所有这些都是 React Native 触摸事件系统无法处理的。
因此,树外平台的用户会分叉 React Native 和/或创建自定义原生组件和模块,以支持诸如悬停检测或左键单击等关键功能。这种分歧导致属性冗余,事件处理程序的功能类似但适用于不同的平台。它增加了框架的复杂性,并使平台间的代码共享变得繁琐。出于这些原因,团队致力于提供一个跨平台指针 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 不同,我们对规范的方法是尽力而为,并有充分记录的偏差,以便期望清晰。在可访问性和性能 API 中,存在将某些 Web 标准对齐以减少 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 调用
react-native-renderer
中的dispatchEvent
方法,该方法遍历 React 树以进行事件的冒泡和捕获阶段。
实现进度
就我们目前实现指针事件规范的进展而言,我们专注于最常见事件的坚实基线实现,这些事件处理诸如按压、悬停和移动等操作。
事件
已实现 | 正在进行中 | 待实现 |
---|---|---|
onPointerOver | onPointerCancel | onClick |
onPointerEnter | onContextMenu | |
onPointerDown | onGotPointerCapture | |
onPointerMove | onLostPointerCapture | |
onPointerUp | onPointerRawUpdate | |
onPointerOut | ||
onPointerLeave |
onPointerCancel 已连接到原生平台的“取消”事件,但这不一定与 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,您需要启用几个功能标志
启用功能标志
覆盖下面的原生功能标志(例如 RCTConstants
和 ReactFeatureFlags
)在技术上是触及 React Native 的内部,因此这样做可能会很快破坏您的设置,因为我们正在努力逐步淘汰它们,以便我们更广泛地推出指针事件。
指针事件仅针对新架构 (Fabric) 实现,并且仅适用于 React Native 0.71+,撰写本文时此版本为候选版本。
在您的入口 JavaScript 文件(默认 React Native 应用模板中的 index.js)中,您需要为指针事件启用 shouldEmitW3CPointerEvents
标志,并为在 Pressability
中使用指针事件启用 shouldPressibilityUseW3CPointerEventsForHover
。
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 上能够区分鼠标和触摸指针,您需要将 UIApplicationSupportsIndirectInputEvents
添加到您的 Xcode 项目的 info.plist
中。
Android 特定
与 iOS 类似,Android 也有一个功能标志,您需要在应用的初始化中启用它——通常是您的根 React 活动或界面的 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 提供支持,但我们也期待社区对我们的方法和目前实现的早期反馈。我们很高兴与您分享我们未来的进展,如果您对此工作有任何问题或想法,请加入我们关于指针事件的专门讨论。