React Native 中的指针事件
今天,我们将分享一个用于 React Native 的实验性跨平台指针 API。我们将讨论其动机、工作原理以及它给 React Native 用户带来的好处。其中包含启用说明,我们很高兴听到您的反馈!
距离我们分享 跨平台愿景,探讨构建超越移动端的优势以及如何为所有平台设定更高标准,已经过去一年多了。在此期间,我们加大了在 React Native for VR、Desktop 和 Web 上的投入。随着这些平台在硬件和交互方式上的差异,引发了一个问题:React Native 应该如何整体处理输入?
超越触控
传统上,桌面和 VR 主要依赖鼠标和键盘输入,而移动端则以触控为主。然而,随着触屏笔记本电脑的出现,以及在移动端支持键盘和笔进行交互的需求日益增长,这一叙事已经发生了演变。而 React Native 的触控事件系统却无法处理这些情况。
因此,非原生平台的开发者不得不 fork React Native 或创建自定义的原生组件和模块来支持悬停检测或左键点击等关键功能。这种分化导致了道具冗余,因为不同平台的事件处理程序功能相似但名称不同。这增加了框架的复杂性,并使跨平台代码共享变得繁琐。出于这些原因,团队决定提供一个跨平台的指针 API。
React Native 旨在提供强大且富有表现力的 API,以支持跨平台开发,同时保持独特的平台体验。设计这样的 API 具有挑战性,但幸运的是,在指针领域存在可以供 React Native 利用的先例。
借鉴 Web
Web 是一个在扩展到多个平台方面面临类似挑战的平台,同时也需要考虑面向未来的设计。万维网联盟 (W3C) 负责制定标准和提案,以构建一个在不同平台和浏览器之间可互操作的 Web。
对我们需求最相关的是,W3C 定义了一种抽象输入形式——指针——的行为。 Pointer Events 规范建立在鼠标事件的基础上,旨在为跨设备指针输入提供一套统一的事件和接口,同时在必要时允许设备特定的处理。
遵循 Pointer Events 规范为 React Native 用户带来了诸多好处。除了解决之前提到的问题外,它还提高了那些历史上无需考虑多输入类型交互的平台的性能。例如,为 Android 手机连接蓝牙鼠标,或者在 iPad M2 上使用 Apple Pencil 支持悬停。
遵守规范还为 Web 和 React Native 之间的知识共享提供了机会。对 Pointer Events Web 期望的教育可以双倍服务于 React Native 开发者。然而,我们也认识到 React Native 的需求与 Web 不同,我们对规范的采用是一种尽力而为的方法,并附有详细的文档说明,以便明确期望。还有与对齐某些 Web 标准相关的 工作,以减少 API 碎片化,尤其是在可访问性和性能 API 方面。
移植 Web 平台测试
虽然 Pointer Events 规范提供了 API 的接口和行为描述,但我们发现它不够具体,无法自信地进行更改并以规范作为验证。然而,Web 浏览器使用另一种机制来确保合规性和互操作性——Web Platform Tests!
Web Platform Tests 是针对浏览器命令式 DOM API 编写的,而 React Native 使用自己的视图原语,因此不支持这些 API。这意味着我们无法与浏览器共享测试代码,而是为 React Native 创建了一个类似的测试 API,从而更容易移植这些 Web Platform Tests。
我们实现了一个新的手动测试框架,现在我们正在使用 RNTester 来验证我们的实现。这些测试暂时命名为 RNTester Platform Tests,目前还比较基础。我们的实现提供了一个 API 来构建测试用例作为组件本身,这些组件会被渲染,并且结果仅通过 UI 报告。

随着我们进一步完善 Pointer Events 实现的完整性,这些测试将继续发挥作用。这些测试还将扩展到测试 Android 和 iOS 以外平台的 Pointer Events 实现。随着我们测试套件中测试数量的增加,我们将寻求自动化这些测试的运行,以便我们能更好地捕捉实现中的回归。
工作原理
我们的 Pointer Events 实现很大程度上依赖于现有的触控事件分发基础设施。在 Android 和 iOS 上,我们利用了相关的 MotionEvent 和 UITouch 事件。事件分发的通用流程如下图所示。

以 Android 为例,利用平台事件的通用方法是:
- 遍历
MotionEvent的所有指针,并进行深度优先搜索,以确定每个指针的目标 React 视图及其祖先路径。 - 将
MotionEvent的类别映射到相关的指针事件。MotionEvent与PointerEvent之间存在一对多的关系。在它们关系的图示中,虚线表示如果指向设备不支持悬停,则会触发事件。

- 使用
MotionEvent的平台详细信息和先前交互的缓存状态构建PointerEvent接口。(例如,button属性) - 从 Android 将指针事件分发到 React Native 的 核心事件队列,并利用 JSI 调用
react-native-renderer中的dispatchEvent方法,该方法会遍历 React 树以进行事件的冒泡和捕获阶段。
实现进展
关于我们当前实现 Pointer Events 规范的进展,我们专注于对最常见事件的稳健基础实现,这些事件处理诸如按下、悬停和移动等操作。
事件
| 已实现 | 进行中 | 尚未实现 |
|---|---|---|
| onPointerOver | onPointerCancel | onClick |
| onPointerEnter | onContextMenu | |
| onPointerDown | onGotPointerCapture | |
| onPointerMove | onLostPointerCapture | |
| onPointerUp | onPointerRawUpdate | |
| onPointerOut | ||
| onPointerLeave |
onPointerCancel 已连接到原生平台的“cancel”事件,但这并不一定与 Web 平台期望它们触发的时间相符。
事件属性
对于上面提到的每个事件,我们也实现了 PointerEvent 对象中大部分预期的属性——尽管在 React Native 中,这些属性通过 event.nativeEvent 属性暴露。您可以在 事件对象的 Flowtype 接口定义 中找到所有已实现属性的枚举。一个尚未完全实现的显著例外是 relatedTarget 属性,因为以这种临时方式暴露原生视图引用并非易事。
未来工作与探索
除了上述事件之外,还有一些与 Pointer Events 相关的 API。未来,我们计划将这些 API 作为此项工作的一部分来实现。这些 API 包括:
- Pointer Capture API
- 包括在元素引用上暴露的命令式 API,例如
setPointerCapture()、releasePointerCapture()和hasPointerCapture()。
- 包括在元素引用上暴露的命令式 API,例如
touch-action样式属性- Web 使用此 CSS 属性来声明性地协商浏览器与网站自身事件处理代码之间的手势。在 React Native 中,这可以用于协商 View 的指针事件处理程序和父 ScrollView 之间的事件处理。
click、contextmenu、auxclickclick是一个抽象的交互定义,可以通过可访问性范例或其他平台特有的交互触发。
原生 Pointer Events 实现的另一个好处是,它将使我们能够重新审视和改进目前仅限于触控事件、并在 JavaScript 中通过 Responder、Pressability 和 PanResponder API 处理的各种手势处理形式。
此外,我们还在继续探索为 React Native 主机组件(例如 add/removeEventListener)实现 EventTarget 接口,我们相信这将使更多用户层抽象能够处理指针交互。
试用
我们的 Pointer Events 实现仍处于实验阶段,但我们对社区对我们分享的内容的反馈很感兴趣。如果您有兴趣尝试此 API,您需要启用几个功能标志:
启用功能标志
覆盖下面的原生功能标志(如 RCTConstants 和 ReactFeatureFlags)在技术上是触及 React Native 的内部机制,因此这样做可能会很快破坏您的设置,因为我们正在努力逐步淘汰它们,以便更广泛地推出 Pointer Events。
Pointer Events 仅为 新架构 (Fabric) 实现,并且仅适用于 React Native 0.71+,而截至本文撰写时,0.71 仍是候选版本。
在您的入口 JavaScript 文件(默认 React Native 应用模板中的 index.js)中,您需要为 Pointer Events 启用 shouldEmitW3CPointerEvents 标志,并启用 shouldPressibilityUseW3CPointerEventsForHover 以在 Pressability 中使用 Pointer Events。
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);
// ...
}
请注意,为了确保 Pointer Event 实现能够区分 iOS 上的鼠标指针和触摸指针,您需要在 Xcode 项目的 info.plist 中添加 UIApplicationSupportsIndirectInputEvents。
Android 特定
与 iOS 类似,Android 也有一个您需要在应用程序初始化时启用的功能标志——通常是您的根 React activity 或 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'}}
/>;
欢迎反馈
目前 Pointer Events 已被我们的 VR 平台使用,并为 Oculus Store 提供支持,但我们也正在寻求社区对我们的方法和目前已有的实现的早期反馈。我们很乐意与您分享我们进一步的进展,如果您对此项工作有任何疑问或想法,请加入我们关于 Pointer Events 的专属讨论。

