React Native 中的指针事件
今天,我们分享了 React Native 的一个实验性跨平台指针 API。我们将介绍其动机、工作原理以及它对 React Native 用户的好处。其中包含了如何启用该 API 的说明,我们期待听到您的反馈!
在我们分享了关于构建超越移动设备的优势以及它如何为所有平台设定更高标准的多平台愿景已经过去一年多了。在此期间,我们增加了对 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 不同,我们对规范的处理方式是尽力而为并对偏差进行详细说明,以便明确预期。有相关工作将某些 Web 标准与减少 API 分裂相关联,从而在可访问性和性能 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 已连接到原生平台的“cancel”事件,但这并不一定对应于 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,则需要启用几个功能标志
启用功能标志
指针事件仅适用于新架构 (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 活动或表面的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 商店正在使用指针事件,但我们也希望获得社区对我们的方法和迄今为止实现的反馈。我们很高兴与您分享我们的进一步进展,如果您对这项工作有任何疑问或想法,请加入我们关于指针事件的专门讨论。