メインコンテンツへスキップ

React Nativeにおけるポインターイベント

·10分で読めます
Luna Wei
Luna Wei
ソフトウェアエンジニア @ Meta
Vincent Riemer
Vincent Riemer
ソフトウェアエンジニア @ Meta

本日、React Native向けの実験的なクロスプラットフォームポインターAPIを公開します。この記事では、その動機、仕組み、そしてReact Nativeユーザーにもたらすメリットについて説明します。有効化する方法についての説明もあり、皆さまからのフィードバックをお待ちしています!

私たちが多くのプラットフォームのためのビジョンを共有してから1年以上が経ちました。このビジョンでは、モバイルを超えた開発の利点と、それがすべてのプラットフォームにとってより高い基準を設けることになる方法について述べました。この間、私たちはVR、デスクトップ、ウェブ向けのReact Nativeへの投資を増やしてきました。これらのプラットフォームにおけるハードウェアとインタラクションの違いから、React Nativeがどのように入力を包括的に扱うべきかという疑問が生じました。

タッチを超えて

デスクトップとVRは歴史的にマウスとキーボード入力に依存してきましたが、モバイルは主にタッチです。しかし、タッチスクリーン付きのラップトップや、モバイルでのキーボードやペンによるインタラクションのサポートに対する需要の高まりによって、その状況は変化しました。これらすべてをReact Nativeのタッチイベントシステムは扱うようには設計されていません。

その結果、アウトオブツリープラットフォームのユーザーは、ホバー検出や左クリックといった重要な機能をサポートするために、React Nativeをフォークしたり、カスタムのネイティブコンポーネントやモジュールを作成したりしています。この乖離は、異なるプラットフォーム向けに同様の目的を果たすイベントハンドラーを持つプロパティの冗長性につながります。これによりフレームワークが複雑になり、プラットフォーム間のコード共有が面倒になります。これらの理由から、チームはクロスプラットフォームなポインターAPIを提供する意欲を持ちました。

React Nativeは、特徴的なプラットフォーム体験を維持しつつ、多くのプラットフォーム向けに開発するための堅牢で表現力豊かなAPIを提供することを目指しています。このようなAPIの設計は困難ですが、幸いなことにポインターの分野にはReact Nativeが活用できる先行技術があります。

Webに目を向ける

ウェブは、将来を見据えた設計を考慮しつつ、多くのプラットフォームへのスケーリングという同様の課題を持つプラットフォームです。World Wide Webコンソーシアム(W3C)は、異なるプラットフォームやブラウザ間で相互運用可能なウェブを構築するための標準や提案を設定する役割を担っています。

私たちのニーズに最も関連するのは、W3Cがポインターと呼ばれる抽象的な入力形式の動作を定義していることです。ポインターイベント仕様はマウスイベントを基に構築されており、必要に応じてデバイス固有の処理を許容しつつ、クロスデバイスのポインター入力に対して単一のイベントとインターフェースのセットを提供することを目指しています。

ポインターイベント仕様に従うことは、React Nativeユーザーに多くの利点をもたらします。前述の問題を解決するだけでなく、これまで複数種類の入力インタラクションを考慮する必要がなかったプラットフォームの能力を引き上げます。例えば、AndroidスマートフォンにBluetoothマウスを接続したり、iPad M2でApple Pencilがホバーをサポートしたりすることなどです。

仕様に準拠することは、WebとReact Nativeの間で知識を共有する機会も提供します。ポインターイベントに関するWebの知見を教育することは、React Native開発者にとっても二重の利益となります。しかし、React Nativeの要件はWebとは異なることも認識しており、仕様へのアプローチはベストエフォートであり、期待が明確になるように逸脱点は十分に文書化します。アクセシビリティやパフォーマンスAPIにおけるAPIの断片化を減らすために、特定のWeb標準を合わせる関連作業もあります。

Web Platform Testsの移植

ポインターイベント仕様はAPIのインターフェースと動作を記述していますが、私たちが自信を持って変更を加え、検証として仕様を指し示すには、それだけでは十分に具体的ではないことがわかりました。しかし、ウェブブラウザは準拠性と相互運用性を確保するために別のメカニズム、すなわちWeb Platform Testsを使用しています!

Web Platform Testsは、ブラウザの命令的なDOM APIに対して動作するように書かれていますが、これは独自のビュープリミティブを使用するReact Nativeではサポートされていません。つまり、ブラウザとテストをコード共有することはできず、代わりにReact Native用のアナログなテストAPIを用意することで、それらのWeb Platform Testsを移植しやすくしています。

私たちは新しい手動テストフレームワークを実装し、現在RNTesterを通じて実装を検証するために使用しています。これらのテストは暫定的にRNTester Platform Testsと名付けられており、まだかなり基本的なものです。私たちの実装は、テストケース自体をコンポーネントとして構築するAPIを提供し、それらがレンダリングされ、結果はUIを通じてのみ報告されます。

GIF showing a side by side comparison of the "Pointer Events hoverable pointer attributes test" running in React Native (iOS) on the left, and Web (the original implementation) on the right.

これらのテストは、ポインターイベント実装の完全性をさらに高めていく上で引き続き役立ちます。また、これらのテストはAndroidやiOS以外のプラットフォームでのポインターイベント実装をテストするためにもスケールします。テストスイート内のテスト数が増えるにつれて、これらのテストの実行を自動化し、実装の回帰をより良く捉えられるようにすることを目指します。

仕組み

私たちのポインターイベント実装の多くは、タッチイベントをディスパッチするための既存のインフラストラクチャを基に構築されています。AndroidとiOSでは、関連するMotionEventとUITouchイベントを活用しています。イベントディスパッチの一般的な流れを以下に示します。

Diagram of code flow for interpreting Android and iOS UI input events into Pointer Events. On Android, input handlers "onTouchEvent" and "onHoverEvent" fire "MotionEvents" that are interpreted into Pointer Events and through JSI are dispatched to the React renderer. iOS takes a similar path with input handlers "touchesBegan", "touchesMoved", "touchesEnded", and "hovering" interpreting "UITouch" and "UIEvent" into Pointer Events.

Androidを例に、プラットフォームイベントを活用する一般的なアプローチは以下の通りです。

  1. MotionEventのすべてのポインターを反復処理し、深さ優先探索を行って各ポインターのターゲットとなるReactビューとその祖先パスを決定します。
  2. MotionEventのカテゴリを関連するポインターイベントにマッピングします。MotionEventPointerEventの間には1対多の関係があります。関係図の点線は、ポインティングデバイスがホバーをサポートしていない場合に発火するイベントを示します。

A diagram illustrating the relationship of types of Android MotionEvents into Pointer Events fired. Some pointer events are conditionally fired if pointing device does not support hover. "ACTION_DOWN" and "ACTION_POINTER_DOWN" fire pointerdown and conditionally fire pointerenter, pointerover. "ACTION_MOVE" and "ACTION_HOVER_MOVE" fire pointerover, pointermove, pointerout, pointerup. "ACTION_UP" and "ACTION_POINTER_UP" fire pointerup and conditionally fire pointerout, pointerleave.

  1. MotionEventからのプラットフォーム詳細と、以前のインタラクションのキャッシュされた状態を使用してPointerEventインターフェースを構築します。(例:buttonプロパティ
  2. ポインターイベントをAndroidからReact Nativeのコアイベントキューにディスパッチし、JSIを活用してreact-native-rendererdispatchEventメソッドを呼び出します。これにより、イベントのバブルフェーズとキャプチャフェーズのためにReactツリーを反復処理します。

実装の進捗

ポインターイベント仕様の実装に関する現在の進捗状況については、押す、ホバーする、動かすといった最も一般的なイベントの堅実なベースライン実装に焦点を当ててきました。

イベント

実装済み作業中未実装
onPointerOveronPointerCancelonClick
onPointerEnteronContextMenu
onPointerDownonGotPointerCapture
onPointerMoveonLostPointerCapture
onPointerUponPointerRawUpdate
onPointerOut
onPointerLeave
情報

onPointerCancelはネイティブプラットフォームの「キャンセル」イベントに接続されていますが、これは必ずしもウェブプラットフォームが期待するタイミングで発火するとは限りません。

イベントプロパティ

上記の各イベントについて、PointerEventオブジェクトに期待されるプロパティの大部分も実装しました。ただし、React Nativeではこれらはevent.nativeEventプロパティを通じて公開されます。実装されたすべてのプロパティの列挙は、イベントオブジェクトのFlowtypeインターフェース定義で確認できます。完全に実装されていない notable な例外としてrelatedTargetプロパティがあります。これは、ネイティブビューの参照をこのアドホックな方法で公開することが簡単ではないためです。

今後の作業と探求

上記のイベントに加えて、ポインターイベントに関連する他のAPIもいくつかあります。将来的には、これらのAPIをこの取り組みの一環として実装する予定です。これらのAPIには以下が含まれます。

  • ポインターキャプチャAPI
    • setPointerCapture()releasePointerCapture()hasPointerCapture()など、要素の参照に公開される命令的APIを含みます。
  • touch-action スタイルプロパティ
    • Webでは、このCSSプロパティを使用して、ブラウザとウェブサイト独自のイベント処理コードとの間でジェスチャーを宣言的に調整します。React Nativeでは、Viewのポインターイベントハンドラーと親のScrollViewとの間のイベント処理を調整するために使用できる可能性があります。
  • click, contextmenu, auxclick
    • clickは、アクセシビリティパラダイムやその他の特徴的なプラットフォームインタラクションを通じてトリガーされる可能性のある、インタラクションの抽象的な定義です。

ネイティブなポインターイベント実装のもう一つの利点は、現在タッチイベントに限定され、JavaScriptでResponder、Pressability、PanResponder APIによって処理されている様々な形式のジェスチャーハンドリングを再検討し、改善できることです。

さらに、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);

// ...
}

ポインターイベントの実装がiOS上でマウスとタッチのポインターを区別できるようにするためには、Xcodeプロジェクトのinfo.plistUIApplicationSupportsIndirectInputEventsを追加する必要があることに注意してください。

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を動かしていますが、私たちのアプローチとこれまでの実装の両方について、コミュニティからの早期のフィードバックも求めています。今後の進捗を皆さんと共有できることを楽しみにしています。この作業に関する質問や考えがある場合は、ポインターイベントに関する専用のディスカッションにご参加ください。