React Native 中的指针事件
今天,我们将分享一个用于 React Native 的实验性跨平台指针 API。我们将讨论其动机、工作原理以及它给 React Native 用户带来的好处。其中包含启用说明,我们很高兴听到您的反馈!
自从我们分享我们的多平台愿景以来,已经一年多了,该愿景阐述了在移动端之外构建的优势以及它如何为所有平台设定更高的标准。在此期间,我们加大了对 React Native 在 VR、桌面和 Web 领域的投入。由于这些平台的硬件和交互方式存在差异,这引发了 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 不同,我们对规范的方法是尽力而为,并有充分记录的偏差,以便期望清晰。还有相关的工作将某些 Web 标准与 减少 API 碎片化 在可访问性和性能 API 中对齐。
移植 Web 平台测试
虽然指针事件规范提供了 API 的接口和行为描述,但我们发现它不够具体,无法让我们自信地进行更改并指向规范作为验证。然而,Web 浏览器使用另一种机制来确保合规性和互操作性——Web 平台测试!
Web 平台测试是针对浏览器的命令式 DOM API 编写的——React Native 不支持这些 API,因为它使用自己的视图基元。这意味着我们无法与浏览器共享测试代码,而是为 React Native 提供了一个类似的测试 API,从而更容易移植这些 Web 平台测试。
我们实现了一个新的手动测试框架,现在我们正在使用它通过 RNTester 验证我们的实现。这些测试暂定命名为 RNTester 平台测试,并且仍然相当基础。我们的实现提供了一个 API,可以将测试用例本身构造为组件,这些组件被渲染,并且结果仅通过 UI 报告。
随着我们进一步完善 Pointer Events 的实现,这些测试将继续提供帮助。这些测试还将扩展到 Android 和 iOS 之外的平台上测试 Pointer Events 的实现。随着我们测试套件中测试数量的增加,我们将寻求自动化这些测试的运行,以便我们能够更好地发现实现中的回归。
工作原理
我们的指针事件实现很大程度上建立在现有分发触控事件的基础设施之上。在 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 主机组件中包含 `EventTarget` 接口的实现(即 `add`/`removeEventListener`),我们相信这将使更多用户空间抽象能够处理指针交互。
试用
我们的指针事件实现仍处于实验阶段,但我们有兴趣听取社区对我们所分享内容的反馈。如果您有兴趣尝试此 API,则需要启用几个功能标志。
启用功能标志
指针事件仅针对新架构(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);
// ...
}
请注意,为了确保 Pointer Event 实现能够区分 iOS 上的鼠标和触控指针,您需要将 UIApplicationSupportsIndirectInputEvents
添加到您的 Xcode 项目的 info.plist
中。
安卓特有
与 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 商店提供支持,但我们也期待社区对我们的方法和迄今为止的实现提供早期反馈。我们很高兴与您分享我们的进一步进展,如果您对此工作有疑问或想法,欢迎加入我们关于指针事件的专题讨论。