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

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は独自のビュープリミティブを使用しているため、サポートされていません。これは、ブラウザとテストコードを共有できないことを意味します。その代わりに、React Native用の類似のテストAPIがあり、これらのWebプラットフォームテストを簡単に移植できるようになっています。

私たちは新しい手動テストフレームワークを実装し、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のカテゴリを関連するポインターイベントにマッピングします。MotionEventとPointerEventの間には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インターフェース定義で見つけることができます。完全に実装されていない notable な例外はrelatedTargetプロパティです。というのも、ネイティブビュー参照をこのようなアドホックな方法で公開するのは容易ではないからです。

今後の作業と探求

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

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

ネイティブのポインターイベント実装のもう1つの利点は、現在タッチイベントのみに限定され、Responder、Pressability、PanResponder APIによってJavaScriptで処理されているさまざまな形式のジェスチャー処理を見直して改善できることです。

さらに、React Nativeホストコンポーネント(つまりadd/removeEventListener)のEventTargetインターフェースの実装を含めることを引き続き検討しています。これにより、ポインターインタラクションを処理するためのより多くのユーザーランド抽象化が可能になると考えています。

試してみる

ポインターイベントの実装はまだ実験段階ですが、共有した内容についてコミュニティからのフィードバックに興味があります。このAPIを試してみたい場合は、いくつかの機能フラグを有効にする必要があります。

機能フラグを有効にする

危険

以下のネイティブ機能フラグ(RCTConstantsReactFeatureFlagsなど)をオーバーライドすることは、厳密にはReact Nativeの内部に踏み込むことであり、ポインターイベントをより広く展開するために段階的に廃止する作業を行っているため、すぐにセットアップが壊れる可能性があります。

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