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 定义了一种抽象输入形式的行为,称为指针。指针事件(Pointer Events)规范建立在鼠标事件之上,旨在为跨设备指针输入提供一套统一的事件和接口,同时在必要时仍然允许设备特定的处理。
遵循指针事件规范为 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 不支持这些 API,因为它使用自己的视图基元。这意味着我们无法与浏览器共享测试代码,而是为 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 中,这可用于协调视图的指针事件处理程序和父级 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 上区分鼠标和触摸指针,您需要将 UIApplicationSupportsIndirectInputEvents
添加到您的 Xcode 项目的 info.plist
中。
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 商店提供支持,但我们也期待社区就我们的方法和目前已有的实现提供早期反馈。我们很高兴与您分享我们的进一步进展,如果您对此工作有任何疑问或想法,请加入我们关于指针事件的专属讨论。