跳至主内容

React Native 中的 Pointer Events

· 1 分钟阅读
Luna Wei
Luna Wei
Software Engineer @ Meta
Vincent Riemer
Vincent Riemer
Software Engineer @ Meta
非官方测试版翻译

本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →

今天我们将分享 React Native 的实验性跨平台指针 API。我们将探讨其设计动机、实现原理以及对开发者的价值。文中包含启用指南,期待听到您的反馈!

距离我们发布多平台愿景已过去一年多,该愿景阐述了超越移动端开发的优势及其为全平台设立的新标准。在此期间,我们加大了对 React Native 在 VR、桌面端和 Web 领域的投入。由于这些平台的硬件和交互方式存在差异,如何统一处理输入成为亟待解决的问题。

超越触控的边界

桌面和 VR 设备长期依赖鼠标键盘输入,而移动端则以触控为主。但随着触控笔记本的普及,以及移动端对键盘和触控笔交互的支持需求增长,React Native 原有的触摸事件系统已无法满足这些新场景。

因此,非移动端平台的开发者不得不分叉 React Native 代码库,或创建自定义原生组件来实现悬停检测、左键点击等关键功能。这种差异导致事件处理属性冗余——相同功能却需为不同平台分别实现。这不仅增加了框架复杂度,也使跨平台代码共享变得繁琐。正是这些痛点促使我们开发跨平台指针 API。

React Native 旨在提供强大而灵活的 API,使开发者能在保持各平台特色的同时构建多平台应用。设计这样的 API 颇具挑战,但幸运的是指针领域已有成熟方案可供借鉴。

借鉴 Web 标准

Web 平台同样面临支持多设备和未来验证设计的挑战。万维网联盟 (W3C) 负责制定标准提案,确保不同平台和浏览器间的互操作性。

最切合我们需求的是 W3C 定义的抽象输入形式——指针。Pointer Events 规范在鼠标事件基础上演进,旨在提供统一的跨设备指针事件接口,同时允许必要的设备特定处理。

遵循 Pointer Events 规范能为 React Native 用户带来多重价值:除解决前述问题外,还能增强历史未考虑多输入类型的平台能力。例如为 Android 手机连接蓝牙鼠标,或 Apple Pencil 在 iPad M2 上实现悬停功能。

符合规范还促进了 Web 与 React Native 间的知识共享。Web 领域的 Pointer Events 实践经验可直接赋能 React Native 开发者。当然,我们也认识到 React Native 的需求与 Web 存在差异,因此采取"尽力遵循+明确标注偏差"的策略,确保预期清晰。相关工作还包括减少无障碍和性能 API 的碎片化

移植 Web 平台测试

虽然 Pointer Events 规范提供了 API 接口和行为描述,但我们发现仅凭规范不足以可靠验证实现改动。不过浏览器另有确保合规性的机制——Web Platform Tests

这些测试基于浏览器的命令式 DOM API 编写,而 React Native 使用自有视图基元,无法直接共享测试代码。因此我们为 React Native 开发了类似的测试 API,方便移植 Web Platform Tests。

我们实现了新的手动测试框架,现通过 RNTester 验证功能实现。这些测试暂命名为 RNTester Platform Tests,目前仍较基础。该框架提供 API 将测试用例构建为可渲染组件,测试结果完全通过 UI 呈现。

GIF 展示了左右对比:左侧是 React Native (iOS) 运行的"指针事件可悬停指针属性测试",右侧是 Web 端原始实现

随着指针事件实现日趋完善,这些测试将持续发挥重要作用。它们还能扩展到 Android 和 iOS 之外的平台进行测试。当测试套件规模扩大后,我们将寻求自动化运行这些测试,以便更好地捕获实现中的回归问题。

工作原理

我们的指针事件实现大量复用现有触摸事件派发基础设施。在 Android 和 iOS 上,我们分别利用对应的 MotionEvent 和 UITouch 事件。事件派发的基本流程如下图所示。

Android 和 iOS UI 输入事件转译为指针事件的代码流程图。Android 端:输入处理函数 "onTouchEvent" 和 "onHoverEvent" 触发 "MotionEvents",经转译为指针事件后通过 JSI 派发至 React 渲染器。iOS 端:输入处理函数 "touchesBegan"、"touchesMoved"、"touchesEnded" 和 "hovering" 将 "UITouch" 和 "UIEvent" 转译为指针事件

以 Android 为例,利用平台事件的核心方法是:

  1. 遍历 MotionEvent 的所有指针,通过深度优先搜索确定每个指针的目标 React 视图及其祖先路径

  2. MotionEvent 类型映射到对应的指针事件。MotionEventPointerEvent 存在一对多关系。下图的对应关系中,虚线表示当指向设备不支持悬停时才触发的事件

Android MotionEvent 类型与触发指针事件的关系示意图。虚线表示指针设备不支持悬停时才触发的事件。"ACTION_DOWN" 和 "ACTION_POINTER_DOWN" 触发 pointerdown 事件,条件性触发 pointerenter、pointerover。"ACTION_MOVE" 和 "ACTION_HOVER_MOVE" 触发 pointerover、pointermove、pointerout、pointerup。"ACTION_UP" 和 "ACTION_POINTER_UP" 触发 pointerup 事件,条件性触发 pointerout、pointerleave

  1. 基于 MotionEvent 的平台细节及先前交互的缓存状态构建 PointerEvent 接口(例如 button 属性

  2. 将指针事件从 Android 派发至 React Native 的核心事件队列,通过 JSI 调用 react-native-renderer 中的 dispatchEvent 方法,该方法将在 React 树中遍历执行事件的冒泡和捕获阶段

实现进展

在实现指针事件规范的过程中,我们目前聚焦于构建核心事件的基础实现,包括按压、悬停和移动等最常见场景。

事件

ImplementedWork in ProgressYet to be Implemented
onPointerOveronPointerCancelonClick
onPointerEnteronContextMenu
onPointerDownonGotPointerCapture
onPointerMoveonLostPointerCapture
onPointerUponPointerRawUpdate
onPointerOut
onPointerLeave
信息

onPointerCancel 已关联至原生平台的"取消"事件,但这并不完全符合 Web 平台预期的触发时机。

事件属性

除上述事件外,我们还实现了 PointerEvent 对象所需的大部分属性——在 React Native 中这些属性通过 event.nativeEvent 暴露。所有已实现属性可在事件对象的 Flowtype 接口定义中查看。需要特别说明的是,relatedTarget 属性尚未完全实现,因为以这种特殊方式暴露原生视图引用并非易事。

未来工作与探索方向

除了上述事件外,还有一些与指针事件相关的其他 API。我们计划在未来工作中实现以下 API:

  • 指针捕获 API

    • 包含通过元素引用暴露的命令式 API:setPointerCapture()releasePointerCapture()hasPointerCapture()
  • touch-action 样式属性

    • Web 平台使用此 CSS 属性在浏览器与网站事件处理代码间声明式协调手势。在 React Native 中,可用于协调视图指针事件处理程序与父级 ScrollView 的事件处理
  • clickcontextmenuauxclick

    • click 是交互的抽象定义,可能通过无障碍范式或其他平台特性交互触发

原生指针事件实现的另一优势在于,让我们能重新审视并改进当前仅限触摸事件的各种手势处理方案——这些功能目前由 Responder、Pressability 和 PanResponder 等 JavaScript API 处理

此外,我们正在持续探索为 React Native 宿主组件(即原生视图元素)实现 EventTarget 接口(包含 add/removeEventListener 方法),这将使开发者能构建更灵活的指针交互处理抽象层

试用指南

我们的指针事件实现仍处于实验阶段,但期待社区就现有方案提供反馈。若您希望试用此 API,需启用以下功能标志:

启用功能标志

危险

覆盖以下原生功能标志(例如 RCTConstantsReactFeatureFlags)本质上是在侵入 React Native 的内部实现,这种做法可能导致您的开发环境随时崩溃。因为我们正在逐步淘汰这些内部标志,以便更广泛地推出 Pointer Events 功能。

备注

指针事件目前仅支持新架构(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 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'}}
/>;

欢迎反馈

当前指针事件已应用于我们的 VR 平台并支持 Oculus Store 运行,同时我们期待社区就实现方案和设计思路提供早期反馈。我们将持续分享进展,若您对此工作有任何疑问或建议,请加入指针事件专项讨论参与交流