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

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

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

本日、React Native向けの実験的なクロスプラットフォームポインターAPIを共有します。動機、仕組み、React Nativeユーザーへの利点について説明します。有効化する方法に関する手順があり、皆様からのフィードバックをお待ちしております!

モバイルを超えた構築の勝利と、それがすべてのプラットフォームに対してどのように高い基準を設定するかについて、多プラットフォームビジョンを共有してから1年以上が経過しました。この間、VR、デスクトップ、Web向けのReact Nativeへの投資を増やしてきました。これらのプラットフォームでのハードウェアとインタラクションの違いから、React Nativeが入力にどのように包括的に対処する必要があるかという疑問が生じました。

タッチを超えて

デスクトップとVRは、従来、マウスとキーボードの入力に依存していましたが、モバイルは主にタッチです。その状況は、タッチスクリーンのノートパソコンや、モバイルでのキーボードとペンによるインタラクションをサポートする必要性の高まりとともに進化してきました。React Nativeのタッチイベントシステムは、これらのすべてに対応できるように装備されていません。

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

React Nativeは、多くのプラットフォームを構築するための堅牢で表現力豊かなAPIを提供することを目指しながら、プラットフォーム固有のエクスペリエンスを維持しています。そのようなAPIを設計することは困難ですが、幸いなことに、React Nativeが活用できるポインタースペースには先行事例があります。

Webを参照する

Webは、将来を見据えた設計を考慮しながら、多くのプラットフォームに拡張するという同様の課題を抱えるプラットフォームです。World Wide Web Consortium(W3C)は、さまざまなプラットフォームとブラウザー間で相互運用可能なWebを構築するための標準と提案を設定する役割を担っています。

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

ポインターイベント仕様に従うと、React Nativeユーザーに多くの利点がもたらされます。前述の問題に対処することに加えて、これまでマルチ入力タイプのインタラクションを考慮する必要がなかったプラットフォームの機能が向上します。AndroidフォンにBluetoothマウスを接続したり、iPad M2でApple Pencilがホバーをサポートしたりすることを考えてください。

仕様に準拠することで、WebとReact Native間の知識共有の機会も得られます。ポインターイベントに関するWebの期待値の教育は、React Native開発者にも役立ちます。ただし、React Nativeの要件がWebとは異なることも認識しており、仕様へのアプローチは、期待値が明確になるように十分に文書化された逸脱を伴う最善の努力です。アクセシビリティとパフォーマンスAPIのAPIの断片化を削減するために、特定のWeb標準を調整する関連作業があります。

Webプラットフォームテストの移植

ポインターイベント仕様は、APIのインターフェースと動作の説明を提供しますが、変更を自信を持って行い、検証として仕様を参照するには十分な具体的さがないことがわかりました。ただし、Webブラウザーは、準拠と相互運用性を確保するために別のメカニズムを使用しています。それはWebプラットフォームテストです!

Webプラットフォームテストは、ブラウザーの命令型DOM APIに対して動作するように記述されており、React Nativeは独自のビュープリミティブを使用するため、サポートされていません。これは、テストをブラウザーとコード共有できないことを意味し、代わりにWebプラットフォームテストの移植を容易にするReact Native用の類似のテストAPIを使用する必要があります。

RNTesterを通じて実装を検証するために現在使用している新しい手動テストフレームワークを実装しました。これらのテストは、仮にRNTesterプラットフォームテストと名付けられており、まだかなり基本的なものです。私たちの実装では、コンポーネント自体としてテストケースを構築するための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 はネイティブプラットフォームの「キャンセル」イベントにフックされていますが、これは必ずしも Web プラットフォームがそれらが発火することを期待するタイミングに対応しているわけではありません。

イベントプロパティ

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

今後の作業と調査

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

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

ネイティブポインターイベントの実装のもう 1 つの利点は、現在タッチイベントのみに制限され、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 でのポインターイベントの実装がマウスとタッチポインターを区別できるようにするには、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 を動かしていますが、当社のアプローチとこれまでの実装の両方について、コミュニティからの初期のフィードバックも求めています。今後の進捗状況を皆様と共有できることを楽しみにしています。この作業に関する質問や意見がある場合は、ポインターイベントに関する専用のディスカッションに参加してください。