React Native 中的指针事件
今天,我们分享一个用于 React Native 的实验性跨平台指针 API。我们将介绍其动机、工作原理以及对 React Native 用户的益处。这里有关于如何启用的说明,我们很高兴听到您的反馈!
自从我们分享了 我们的多平台愿景 以来,已经过去一年多了,我们讨论了构建移动平台之外的优势,以及它如何为所有平台设定更高的标准。在此期间,我们增加了对 VR、桌面和 Web 的 React Native 投资。由于这些平台在硬件和交互方面的差异,引发了一个问题:React Native 应该如何整体处理输入。
超越触摸
桌面和 VR 历来依赖鼠标和键盘输入,而移动设备主要依赖触摸。但随着触摸屏笔记本电脑的出现以及移动设备上通过键盘和笔进行交互的需求不断增长,这种说法已经发生了变化。React Native 触摸事件系统无法处理所有这些情况。
因此,非树内平台的用户会 Fork 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 调用
dispatchEvent
方法在react-native-renderer
中,它遍历 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
标志,并为在 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);
// ...
}
请注意,为了确保指针事件实现可以区分 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 Store 提供支持,但我们也期待社区对我们的方法以及我们迄今为止的实现提供早期反馈。我们很高兴与您分享我们的进一步进展,如果您对此项工作有疑问或想法,请加入我们在 关于指针事件的专门讨论。