アプリの基礎

Jetpack Compose でタッチスクリーン以外の入力操作をスケーリングする

スピーカー

  • Alex Vanyo

    ソフトウェア エンジニア

  • Jeremy Walker

    Compose ソフトウェア エンジニア

概要

講演について

新しいフォーム ファクタがタッチスクリーンを超えて進化するにつれて、Compose の入力システムもその課題に対応できるように進化してきました。その階層型アーキテクチャは、高度なトラックパッド ジェスチャーから新しいハードウェア入力まで、あらゆるものをシームレスにサポートするための基盤を提供します。

明確なアーキテクチャの内訳と実践的なコード例を通じて、OS プラットフォーム レイヤから Compose ジェスチャー検出機能と UI コンポーネントに至るまで、入力イベントのライフサイクルを追跡します。スタック全体でイベントをトレースする方法、新しい入力タイプに主要なアーキテクチャ パターンを適用する方法、独自のアプリで複雑な操作を自信を持ってデバッグする方法を学びます。

概要

  • Jetpack Compose は、6 フェーズのパイプラインを介して、受信した Android ポインタ イベントを処理します。このパイプラインは、ヒットパス ツリーを維持し、UI ノード全体で 3 つのパスでイベントをディスパッチします。
  • Compose のトラックパッド入力が、タッチではなくマウス イベントにマッピングされるようになりました。Compose は、プラットフォームの疑似指イベントを専用のパンおよびスケール ポインタ イベントに変換します。
  • スマートグラスのトラックパッドは、画面座標のタップではなく、間接的な入力タイプを導入します。Primary Directional Motion Axis を使用して、水平方向のスワイプを垂直方向の次または前のナビゲーションにマッピングします。
  • テストのサポートが、performTrackpadInput などの新しいメソッドにより、タッチ以外にも拡張されました。これらの API は、アプリが非タッチデバイスを正しく処理することを確認するための、使い慣れたテスト サーフェスを提供します。

Gemini Notebook

トピック

  • jetpack-compose
  • pointer-input
  • トラックパッド
  • ジェスチャー
  • intelligent-eyewear
  • ユーザー補助

文字起こし

概要と入力の基本方針

  • 皆さん、こんにちは。

  • Android Developer Summit をお楽しみいただけているでしょうか。本日はご参加いただきありがとうございます。

  • 私は Compose チームのソフトウェア エンジニアの Alex Vanyo です。同じく Compose チームのソフトウェア エンジニアである Jeremy も参加しています。

  • 私たちは、Jetpack Compose の入力システムを強化する取り組みを進めてきました。本日は、タッチスクリーン以外の入力システムをサポートしやすくするために、タッチ入力以外の入力システムをどのように拡張したかについてお話しします。

  • 入力とは、大まかに言えば、ユーザーがアプリを操作する方法をキャプチャしたものです。

  • モバイルアプリの入力方法として最初に思い浮かぶのはタッチスクリーンかもしれませんが、入力方法はそれだけではありません。

  • その他の入力タイプには、物理キーボード、マウス、トラックパッド、タッチペン、コントローラ、ロータリー デバイスなどがあります。

  • また、スマートグラスのトラックパッド、XR のアイ トラッキングとハンド トラッキング、Wear の指と手首の動きなど、新しい入力タイプも登場しています。

  • さらに一般化すると、テキスト編集用のソフトウェア IME、マイク、カメラ、その他のデバイス センサーなど、他のタイプの入力もあります。

  • アプリは、さまざまな入力タイプで使用されることを想定する必要があります。

  • その理由の一つは、アプリケーションが動作するデバイスの種類がますます増え、それらのデバイスの適応性が高まっていることです。

  • デバイスのフォーム ファクタには、最も一般的な入力方法があるかもしれませんが、特定のデバイス フォーム ファクタが特定の入力タイプの使用を意味すると想定しないことが重要です。

  • Chromebook にはトラックパッドとキーボードが組み込まれていますが、これらの入力タイプは Chromebook 固有のものではありません。

  • スマートフォン、タブレット、テレビでも、物理キーボード、トラックパッド、マウスが使用されることが想定されます。一部のテレビにはタッチスクリーンも搭載されています。

  • 複数の入力タイプを想定してサポートする 2 つ目の大きな理由は、ユーザー補助です。

  • ユーザーが好みの入力デバイスを使用できるようにし、必要に応じて入力デバイスを切り替えられるようにすることは、アプリケーションのアクセシビリティとユーザビリティを向上させるために適切な方法です。

  • また、法域によっては、これらの入力タイプ(特にキーボードやマウスなど)に対して、特定の互換性のある動作をアプリに組み込むことが法的に義務付けられている場合があります。

  • つまり、フォーム ファクタに関係なく、さまざまな入力タイプのサポートを構築する必要があります。

  • では、Jetpack Compose がアプリでこうしたユーザーのニーズを満たすのにどのように役立つかを見てみましょう。

  • Compose の一般的な考え方では、これらの異なる入力タイプは、特別なことをしなくても、標準コンポーネントですぐに適切に動作するはずです。

  • そして、必要に応じて動作をカスタマイズして調整するために必要なツールを用意する必要があります。

  • そのため、Compose では、初期の入力タイプだけでなく、基盤となるプラットフォームでサポートされる入力タイプが増えるにつれて、それらのサポートを Compose コンポーネントに組み込み、Compose のバージョンを更新するだけでアプリの動作を改善できるようにすることを目指しています。

  • この講演では、Compose に最近追加された 2 つの例を見ていきます。

  • 偶然にも、どちらもトラックパッドのようなデバイスですが、ご覧のとおり、使い方は大きく異なります。

  • そこで、まずはノートパソコン型のトラックパッドに関する最近の取り組みからご紹介します。

  • トラックパッドは、ポインタベースの入力デバイスです。

  • 具体的には、トラックパッドを使用すると、2 次元の座標が得られます。

  • ただし、タッチスクリーンとは異なり、トラックパッド上の指の座標は UI の座標空間に直接配置されません。

  • 代わりに、ノートパソコンのようなトラックパッドでは、カーソルを制御するための変換が行われます。

  • この変換では、カーソルの加速やパーム リジェクションなど、複雑なプラットフォーム変換が多数行われます。

  • Compose 側では、通常のマウス入力のように動作する、カーソル位置に対応するポインタが取得されます。

  • Android プラットフォームからは、ホバー情報、クリック(セカンダリ クリックやターシャリ クリックを含む)が取得され、これらすべての情報が MotionEvent にラップされます。

  • Compose の最新バージョンで動作がどのように改善されたかを理解するために、まずは Compose がこれらのポインタイベントをどのように処理するのか、その概要を詳しく見ていきましょう。

ポインタ入力処理の 6 つのフェーズ

  • このプロセスを 6 つのフェーズに分けて、順番に説明していきます。

  • 最初のフェーズは、Compose に移動する MotionEvent です。

  • Compose は、内部的には単なるビューです。多くの処理を行う非常に複雑なビューですが、単なるビューであるため、ポインタ入力のために Android システム全体で動作する必要があります。

  • Compose は、通常のアクティビティ内の通常のウィンドウに存在するため、Android プラットフォームの MotionEvent を取得します。

  • そして、Compose はそれらを処理し、ポインタ入力に関して Compose 内で行いたいすべてのことを行う必要があります。

  • 最初のフェーズでは、これらの MotionEvent を、Compose のプラットフォームに依存しない内部表現(PointerInputEvent)に変換します。

  • これは内部型であるため、直接見ることはありません。

  • ここで何が起きているのかを詳しく確認したい場合は、MotionEventAdapter を参照してください。ここでは、その詳細については省略します。

  • これにより、Compose で MotionEvent をプラットフォームに依存しない形で表現できるようになりました。

  • 次に、入力は通常、時間の経過とともに発生することがわかります。

  • 入力イベントが次々と発生するため、時間の経過とともに何が起こっているかを実際に把握できるのは非常に便利です。

  • ジェスチャー、速度トラッキング、スロップ検出などは、各時点で何が起きているかを把握する必要があります。

  • この処理パイプラインの次のステージでは、この時間情報を追加し、連続するポインタイベントに基づいて前処理を行います。

  • コードでは、現在の PointerInputEvent を取得し、前の PointerInputEvent で発生した内容と組み合わせて、時間認識 InternalPointerEvent を作成します。

  • これも、名前からわかるように内部タイプですが、さまざまなポインタ ID をポインタ入力の変更にマッピングしたものと考えることができます。PointerInputChanges は公開タイプです。

  • これが最初の問題です。

  • 異なるポインタ ID について考える際に気づくかもしれないのは、時間のトラッキングを認識したことで、ポインタ ID がいつ現れていつ消えるかも追跡できるようになったということです。

  • 1 つだけではありません。

  • タッチスクリーンに複数の指を置いている場合、複数のポインタが同時に出入りする可能性があるため、それに対処する必要があります。

  • 詳細を知りたい場合は、PointerInputChangeEventProducer をご覧ください。このクラスがすべての処理を行っています。ここでは、このクラスの説明は省略します。

  • まだまだたくさんあります。

  • これで、Compose で入力を表現できるようになりました。これには時間情報が含まれています。

  • 次のステップは、そのイベントに対して何を行うかを実際に判断することです。

  • どこに送ればよいですか?

  • そして、ヒットパス テストにつながります。

  • MotionEvent が送信され、この ComposeView がウィンドウ全体、場合によっては画面全体を埋めている状況を考えてみましょう。

  • どこに送るかを決める必要があります。

  • Compose は単なるビューですが、Compose の内部には、さまざまなノード、ボタン、操作対象など、多数の要素があります。そのため、Compose では、特定の位置で発生したイベントの送信先を特定し、実際にそのイベントに関係するノードを特定する作業をすべて行う必要があります。

  • これらのレイアウト ノードには座標、境界、形状があるため、ヒットしたノードを特定する必要があります。

  • しかし、これも最も一般的な見方ではありません。

  • リスト内にボタンを配置するなど、操作可能な要素の中に別の操作可能な要素を配置することがよくあります。

  • 私たちは、そのようなことをよく行っています。

  • したがって、ここでの一般的なケースは、特定の入力イベントが、入力イベントの座標によってヒットされるすべてのインタラクティブなノードを通過する完全なヒットパスを作成することです。

  • この場合、ネスト階層を通過する 3 つの要素があります。

  • ヒットパスはありますが、それだけでは不十分です。

  • 複数のポインタイベント、複数のポインタがダウンしている可能性があると述べたことを覚えていますか?

  • 実際には、完全なヒットパス ツリーが必要です。

  • 複数のポインタがある場合、ヒットパスは 1 つだけではありません。

  • これが次のフェーズです。このヒットパス ツリーを作成して更新します。

  • これは、何が起こっているかを詳しく示したものです。

  • 左側には、視覚的に配置されたノードが表示されます。左下には、階層表現が表示されます。どのノードが他のノード内にあるか、どのノードが互いに祖先または子孫であるかがわかります。

  • ポインタがダウンすると、そのヒットパス(ツリー全体のフルパス)を追跡し、その特定のポインタのヒットパスを取得します。

  • このヒットパスは、2 つ目のポインタを取得するとツリーになります。

  • 他のパスと同じノードと交差する場合もありますが、他のノードと相互作用する場合もあります。

  • 右側は、現在維持しているヒットパス ツリーです。

  • ポインタは追加されたり削除されたりするため、このリストを継続的に管理して、現時点で進行中の各イベントに関心のあるノードの完全なリストを取得できるようにする必要があります。

  • それでは

  • これで、これらの入力イベントを管理するヒットパス ツリーが作成されたので、次のフェーズに進みます。

  • これで、これらのポインタ入力イベントをどこに送信するか、つまり、各イベントに関心があるノードがどこかが実際にわかるようになりました。

  • これがわかったので、座標変換を行うことができます。

  • ノードが独自のローカル座標空間内で入力イベントを取得すると、座標計算を自分で行う必要がなくなるため、非常に便利です。

  • それがわかれば、ポインタ入力イベントを取得し、対象のノードの座標と組み合わせることができます。そして、最終的に、すべてのユーザーとポインタ入力修飾子にディスパッチされる公開 PointerEvent が作成されます。

  • そして、フェーズ 6 の最後のステップが、実際にイベントを送信することです。

  • これは、イベントが発生したことを示すポインタ入力イベントであり、ノードに送信して、ノードが好きなように処理できるようにします。

  • この処理は 3 つの異なるパスで行われます。

  • 最初のパス、メインパス、最後のパスがあります。

  • 3 つの異なるパスがある理由は、ノードが相互に通信し、特定のポインター入力イベントの特定の部分に関心があることを伝え、ディスパッチの一部としてそれらの異なる部分を消費できるようにするためです。

  • 最初のパス(初期パス)は、祖先から子孫に移動します。

  • これが役立つ例としては、外側のレイアウト ノードがスクロール コンテナである場合が挙げられます。このノードは、スクロールが開始されたことを認識し、すべての子にポインタ イベントが到達しないようにします。

  • これにより、最初のパスでイベントを消費し、子にイベントが渡されないようにすることができます。

  • 2 回目のパスがメインパスです。

  • そして、これが子孫から祖先に移動する唯一のものです。

  • 名前が示すように、これは処理を行うためのメインのパスです。

  • たとえば、子孫がダウンイベントを消費してクリックの処理を開始する場合、クリック可能な要素がネストされていれば、親よりも先に処理を行うことができます。

  • 最後のパスである最終パスは、最初のパスと同じように、祖先から子孫へと進みます。

  • これにより、子が親にイベントの一部を消費したことを伝えることができます。

  • たとえば、子がクリックの処理を開始した後に親がスクロールを開始した場合、子は親がスクロールを開始したことを認識し、開始したクリックの処理をキャンセルできます。

  • 以上が 3 つのパスと 6 つのフェーズです。

  • これは、ポインタ入力システム上のすべての基盤となります。

  • ジェスチャー処理、スクロール可能、ドラッグ可能、クリック可能、カスタム ポインタ入力はすべて、この基盤の上に構築されています。

トラックパッドの更新とマルチフィンガー ジェスチャー

  • 以上を踏まえて、Compose でトラックパッドの動作のサポートを改善した方法について、具体的に説明します。

  • そのため、Compose 1.12 より前は、トラックパッドの入力イベントはタッチのポインタタイプとして報告されていました。これは、指がトラックパッドを物理的に操作してカーソルを制御するためです。

  • しかし、ユーザーは、この入力タイプが物理的なマウスのように動作することを期待しています。

  • そのため、ポインタの型を PointerType.Touch から PointerType.Mouse に変更しました。

  • この変更により、この情報を参照するすべてのコンポーネントで、マウスとトラックパッドの操作性が大幅に向上します。

  • たとえば、ツールチップの動作です。トラックパッドとマウスでツールチップの動作を同じにしたい場合があります。

  • トラックパッドに関する 2 つ目の大きな変更は、トラックパッドで行うマルチフィンガー ジェスチャーに関連しています。

  • 以前は、Compose は Android プラットフォームの巧妙ながらも少し不気味な「偽の指」というコンセプトを通過していました。

  • では、これがどのような意味を持つのかを見ていきましょう。

  • トラックパッドで 2 本の指を使ってパンすると、Android は、カーソルの位置に 1 本の指が置かれたタッチスクリーンをシミュレートします。

  • 次に、トラックパッド上で指を動かすと、タッチスクリーン上の疑似指が、目的の動作を模倣するために適切な距離と方向に移動し、その後、離れます。

  • つまり、トラックパッド上で 2 本の指を上に動かすと、タッチスクリーン上では 1 本の仮想の指が上に動くことになり、結果として下にスクロールされます。

  • ここで面白いのは、私のようにトラックパッドの逆スクロール機能を使用している場合、スクロールを逆方向にしたいということです。

  • これを実現するには、偽の指が移動する方向を反転させます。

  • そのため、トラックパッド上で 2 本の指を上に動かすと、タッチスクリーン上で 1 本の偽の指が下に動くように変換され、その結果、以前とは逆に上にスクロールされます。

  • スケーリングも同様の仕組みです。

  • トラックパッドで 2 本の指を使って拡大縮小すると、Android は 2 本のフェイクのタッチスクリーン指を生成します。この指は、ジェスチャーに応じて、下に移動してから、近づいたり離れたりして、ズームインまたはズームアウトの目的の動作を模倣します。

  • そのため、Compose 1.12(パン操作)と Compose 1.13(スケーリング操作)以降では、これらのフェイクフィンガーをインターセプトするようになりました。

  • プラットフォームから提供されたイベントをそのまま送信するのではなく、新しいポインター イベントタイプに再解釈します。パンの場合は PanStart、PanMove、PanEnd、スケーリングの場合は ScaleStart、ScaleChange、ScaleEnd です。

  • これは、先ほど説明した Android から Compose へのイベント変換フェーズで行います。

  • そのため、Fake Finger イベントを適切なタイプに変換し、このプラットフォーム定義のジェスチャーをカーソルに対応するヒットパスに送信します。

  • その重要性について説明します。

  • なぜこのようなことをするのでしょうか。

  • このボックスが、拡大したい画像だとします。

  • トラックパッドで 2 本の指を使用して拡大縮小する場合、2 本のフェイクフィンガーのいずれかが要素を逃す可能性がありました。

  • ヒットパスは画像と交差せず、境界の外側から始まります。

  • この場合、画像の拡大縮小ではなく、スクロールが発生する可能性があります。

  • また、画像の外部に別のスクロール可能な要素がある場合、それも一緒にスクロールを開始する可能性があります。

  • これはユーザーが望んでいたことではありません。

  • 同社は、スケーリングを望んでいました。

  • そこで、これらの偽の指を送信する代わりに、カーソル位置の下にある要素にのみ拡大縮小ジェスチャーを送信するようにしました。これにより、正しい動作が実現されます。

  • カーソルの位置や、拡大縮小する要素の端との距離に関係なく、正しく拡大縮小されるようになりました。

  • 皆さんにとって朗報なのは、これらの再解釈された入力イベントが、scrollable や transformable などのコンポーネントでデフォルトで使用されることです。これらの基本的なビルディング ブロックを使用している場合、トラックパッド入力の動作の再現性が高まります。

  • また、それらの上に構築されたものを使用している場合は、Compose のバージョンを最新バージョンに更新するだけで、改善された動作を利用できます。

  • カスタムのポインタ入力修飾子を作成している場合は、これらの新しい API を確認し、サポートしていることを確認して、さまざまな入力デバイスでユーザーに最適なエクスペリエンスを提供することをおすすめします。

  • もちろん、テスト用 API も追加されているため、アプリがこれらすべての異なる入力タイプを正しく処理していることを確認できます。

  • そのため、performTrackpadInput はマウスとタッチの既存のテスト API とよく似ています。

    onNodeWithContentDescription("Photo")
        .performTrackpadInput {
            click(center)
    
            pan(Offset(0f, 100f))
    
            scale(0.9f)
        }
  • これらすべてを使用することで、ユーザーがデバイスでどの入力デバイスを使用していても、アプリが正しく動作することを確認できます。

  • これで、ポインター イベントがタッチパッド上の指からアプリにどのように伝わるかについて、理解を深めていただけたかと思います。

  • それでは、トラックパッドのまったく異なる使用方法について、ジェレミーに説明してもらいましょう。

間接入力の概要

  • よろしくお願いいたします。

  • それでは

  • こんにちは。Jeremy Walker です。

  • 私は入力に関する作業を多く行っています。Compose の入力に関する新しい取り組みと、それが皆様にどのような影響を与えるかについてお話しできればと思います。

  • 特にこの間接入力タイプは、新しいフォーム ファクタ(スマートグラス)に基づく新しい入力方法として登場しました。

  • グラスから入力を取得する方法はいくつかあります。何かを見る、音声を使用する、ジェスチャーを使用するなどです。ここでは、タッチパッドについて詳しく説明します。

  • これらは、アレックスが話したのとは少し異なり、頭の横にあり、垂直で、非常に幅が広く、非常に短いものです。

  • そこで、Compose での動作を検討する際に、目の前にある小さなボタンにカーソルを移動してタップしようとすると、あまり良いエクスペリエンスにならないと考えました。

  • では、どのようにサポートすればよいでしょうか。

  • これは、Glasses UI の表示例です。

  • コンテンツを画角にシームレスに重ねるように独自に設計されています。

  • しかし、とてもシンプルです。シンプルに保つようにしています。

  • この例を見ると、これがリストであることがわかります。

  • 中央の特定の要素に焦点を当てています。

  • 本当にやりたいことは何なのか?

  • 選択したり、前後の項目に移動したりできます。

  • 画面上の位置は必ずしも必要ありません。

  • これは少し複雑ですが、基本的には、通話を終了する、ミュートを解除する、動画をオフにするという 3 つのオプションがあります。

  • しかし、実際には、それを選択するか、別のものに移動してそれを選択する必要があります。

  • そのため、画面上の位置(この場合は目)に直接マッピングする必要はないと考えています。

  • これが実際の画面です。

  • たとえば、ここでは要素間を移動しています。これは Glasses UI で、私たちが求めているものに合致しています。

  • タップすることもできます。

  • そこで、Android を見て、これをサポートするために何が使えるかを考えました。

  • そして、もし皆さんが、あ、すみません。

  • これは、ここでスキップしています。

  • 戻る

  • いいぞ

  • それでは

  • これらのデバイスのいずれかを使用して、Android デバイスとリンクしたことがある場合は、UI 要素を簡単に移動できることがわかります。

  • たとえば、リモコンを使用すると、Google TV とリンクしている場合、映画のグリッド全体が表示され、矢印ボタンを押して目的の映画に移動し、OK ボタンを押して選択できます。

  • ゲームメニューがあり、このコントローラがある場合は、D-pad またはジョイスティックを使用して、メニュー内の UI 要素に移動して選択できます。

  • キーボードも同様です。

  • これは、Compose で必要なものと一致しています。

  • ここでも、どのようなものかイメージできるように、いくつかの項目を示します。

  • もう少し遡ると、テレビとペアリングされたタッチパッド タイプがあり、ナビゲーション タッチと呼ばれるものが使用されていました。

  • この場合、x 座標と y 座標は画面にまったく関連付けられていません。

  • ジェスチャーの検出にのみ使用されます。

  • 右にスワイプすると、矢印を押して移動するような操作になります。

  • 左にスワイプすると、左、上、下に移動します。

  • これで、必要なものはほぼ揃いました。

  • x と y は、ドラッグやスワイプなどを把握するための速度トラッキングなどにのみ使用します。そのため、Compose を始めるために必要なものはほとんど揃っています。

  • テレビにマッピングされているため、正確ではありません。テレビのルートをたどらないように、後でテレビを混乱させないように、それらを分割する必要があります。これについては、後ほど説明します。

  • しかし、これは出発点のようなものです。

  • このタイプのタッチ ナビゲーションを使用できるようにするためです。

間接入力と新しいフォーム ファクタ向けの API 設計

  • うまくいきました。

  • API 設計はどのようになりますか?

  • Compose で最も一般的な入力はタッチです。

  • そのため、この点に関して多くの作業を行っています。

  • そのため、それとの関連性を把握する必要がありました。

  • PointerEvent は次のようになります。

  • ここでは、タイプがアップのダウン移動の種類で、この PointerInputChange は単なるリストです。指と考えることができます。

  • 3 本の指を折った場合は、リストに 3 つの項目があります。

  • そこで、このような外観にしたいと考えました。

  • これは UI モジュールではありません。UI モジュールは、ほとんどのデベロッパーが入力処理に使用するモジュールよりもはるかに下位のモジュールです。

  • これらは基盤レイヤの上のレイヤになります。

  • すべてのジェスチャー検出機能はここにあります。

  • デベロッパーは、このような細かい入力について気にする必要はありません。

  • ドラッグ、クリック、スワイプのいずれかが発生したかどうかを把握し、それに基づいてアクションを実行するだけです。

  • そのため、そうした処理を行うコードが多数用意されており、それらのコードは内部でこれらの機能を利用しています。

  • そこで、Foundation に存在するコードがこれらの新しい型を簡単にサポートできるようにするにはどうすればよいかを考え始めました。

  • そこで、ほぼ同じように見える新しいタイプを作成しました。

  • 間接呼び出しと呼ばれます。

  • 間接的とは、画面に直接触れないことを意味します。

  • つまり、これはタッチの観点から考えるのではなく、

  • ここでは、ジェスチャー処理と UI 要素間の移動のみを行っています。

  • タイプが同じであること。

  • 指のリストがあります。

  • この場合は IndirectPointerInputChange です。

  • それらを実践したところ、素晴らしい成果が得られました。

  • しかし、もう 1 つの項目として、Primary Directional Motion Axis(主要な方向性運動軸)というものがありました。

  • 少し複雑に聞こえるかもしれませんが、この機能がどのようにして生まれたのかを説明すると、理解していただけると思います。

  • では、どのようにして生まれたのでしょうか。

  • このタッチパッドは、頭の横にあり、幅が広く、短いものです。

  • この時点で、タッチ ナビゲーションというアイデアが生まれました。

  • 左右にスワイプできます。

  • これは便利です。

  • しかし、現在のトラックパッドの仕組みを考えてみてください。

  • 左右に移動する場合は、UI コンテンツとともにトラックパッドを左右に移動します。

  • 上下に移動すると、リストが上下にスクロールされます。

  • メガネをかけていると、これはそれほど悪くありません。

  • しかし、この方法では、あまり良いエクスペリエンスとは言えません。

  • そこで、最も自然な動きは、タップはもちろん、グラスの側面に沿ってスワイプすることだとわかりました。

  • そこで、ユーザーを対象にさまざまな調査を行ったところ、ユーザーは水平方向にスワイプしたときに、コンテンツが水平方向に移動するのではなく、垂直方向に移動することを期待していることがわかりました。

  • 頭の中で試してみるとわかると思いますが、ここでスワイプすると、目の前にリストがある場合、たとえば、リストを先に進むことも、リストを戻ることもできます。

  • つまり、2 次元の入力を 1 次元的に考えようとしています。

  • そこで、水平方向に移動すると垂直方向に移動するという、主要なモーション軸のアイデアが生まれました。

  • そのため、デバイス自体で指定できます。

  • これは通常のトラックパッドではない、と言えます。

  • 主な方向を水平方向にしたい。それが主な、そして唯一の軸にしたい。

  • それが、この機能が生まれた背景です。

  • 違いますね。

  • そのため、この点についてはかなり手応えを感じています。

  • Compose がすべてを処理しています。

  • そして、この優れた垂直方向の実装があります。

  • この点について考えてみてください。

  • そこで、水平方向の定義では、前進と後退のみをマッピングしました。

  • 上下左右はありません。

  • 前進と後退のみです。

  • そして、フォーカスに慣れている場合は、次と前をマッピングできます。

  • これで、次と前に移動できるようになりました。

  • これは縦向きの動画に最適です。

  • さらに、より快適に利用できます。

  • 水平方向はデフォルトで機能しますよね?

  • フォーカスされたアイテムが多数あるだけです。

  • 次のトラックまたは前のトラックに移動するだけで、どちらでも機能します。

  • これは Compose のケースで、Compose ですべてを定義して把握しましたが、デバイス自体が、形状が特殊であることや、この主軸だけが必要であることを認識しているものの、それを定義する方法がないため、デバイス自体がそれを伝えることができれば、非常に便利です。

  • そこで、このコンセプトをプラットフォームに持ち帰り、プラットフォームで定義して標準化し、デバイス自体が指定できるようにする必要がありました。そうすれば、Compose で完全にサポートできるようになります。

  • Compose で作業を進め、Compose で作業を戻し、それを持ち帰るというケースもあります。

  • そこで、まさにそのとおりにしました。

  • この新しい API を作成したことで、デベロッパーまたはデバイスが指定できるようになりました。

    InputDevice.ViewBehavior
    .getPrimaryDirectionalMotionAxis()
  • 先ほど、タップ入力(タップ ナビゲーション)について説明した際に、主にテレビ向けであると述べました。

  • Google は、それらの実装を壊したくありませんでした。

  • そこで、この新しいタイプのガラス入力を指定する別のフィールドを追加しました。

  • 違いますね。

  • では、最終的にこれは何を意味するのでしょうか。

  • これは、ジェスチャー検出機能として取得するものの、より具体的なバージョンです。

    // Glimmer Library
    Box(
        modifier =
            Modifier.fillMaxSize()
                .onIndirectPointerGesture(
                    enabled = true,
                    onSwipeForward = { /* onSwipeForward */ },
                    onSwipeBackward = { /* onSwipeBackward */ },
                    onClick = { /* onClick */ },
                )
                .focusTarget()
    ) {
        // App()
  • Foundation を使用している場合は、初期状態でジェスチャー検出機能が動作します。

  • こちらで対応いたしましたので、お客様側で必要なご対応はございません。

  • しかし、メガネ用のアプリを作成する場合は、Glimmer ライブラリを使用できます。特別なジェスチャー検出機能があり、これを使用するだけで、クリック、前方フレーム、後方フレームを取得できます。

間接ポインタ入力のテスト

  • 違いますね。

  • ここまで、プラットフォームと Compose での API の動作について説明しました。

  • テストについてはどうでしょうか。

  • タッチテストを行う場合、タッチは次のように表示されます。

    @Test
    clickTest {
    
    rule.onNodeWithTag(defaultTag)
    .performTouchInput
    { click(center) }
    
    }
  • performTouchInput と言ってから、ジェスチャーを行います。

  • さまざまなジェスチャーがあり、ジェスチャーを実現するために小さなイベントをすべて構築します。

  • 大まかな仕組みは次のとおりです。

  • Compose UI モジュールと UI テスト モジュールがあり、少なくとも入力に関しては MotionEvent を使用して通信します。

  • また、UI テスト モジュールは、すべて MotionEvent に基づくキューイング メカニズムも使用します。

  • なぜこのような話をするのかというと、

  • 間接入力イベントのような MotionEvent をプログラムで作成することはできないため、モジュールとキューイング システム間のすべての通信を書き直す必要がありました。

  • でも、やり遂げました。

  • これは、デベロッパーにとってどのような意味があるのでしょうか。

  • タップ入力テストに慣れている場合、これは間接的なテストです。

  • 名前の前に「indirect」を付けるだけです。

    @Test
    clickTest {
    
    rule.onNodeWithTag(defaultTag)
    // .performTouchInput
    .performIndirectPointerInput (
    IndirectPointerEventPrimaryDirectionalMotionAxis.X,
    inputDeviceSize,
    )
    { click(center) }
    }
  • ここでは、追加のパラメータをいくつか渡しています。

  • 最初のものは、水平方向を指定する場所です。

  • 入力サイズは、当然ながらタッチパッドのサイズになります。

  • これではあまり変わりません。

  • ソースコードを見ると、拡張関数を記述しただけで、このようになっています。

    @Test
    clickTest {
    
    rule.onNodeWithTag(defaultTag)
    // .performTouchInput
    .performIndirectPointerInput
    { click(center) }
    
    }
  • 違いますね。

  • このことから、フレームやスマートグラスのトラックパッドをサポートするために、どれだけの作業を行ったかがおわかりいただけると思います。

  • しかし、私たちはこれらのエクササイズを行っています。私は視線とハンド トラッキングについてもこれらのエクササイズを行っているので、今後さらに多くのことが期待できます。

  • 大まかに言うと、私のパートで覚えておいていただきたいことは何でしょうか?

まとめと重要なポイント

  • スマートフォンやメガネ、その他の入力デバイスでトラックパッドを使用する場合でも、私たちはそのことを考慮し、事前に対応しようとしています。

  • そのため、これらの Foundation ジェスチャー検出機能を使用している場合は、すぐに動作します。

  • これが 1 つ目です。

  • 2 つ目は、Compose が主要な UI ツールキットであることです。Google は、プラットフォームとツールキットの両方で、新しい入力タイプの探索とサポートを継続します。

  • そして、API をテストする際には、使い慣れたサーフェスとパラダイムを維持して、これらの新しいタイプの移行とテストをできるだけ簡単に行えるようにしたいと考えています。

  • お役に立てば幸いです。

コード

コード スニペット

「トラックパッドの更新とマルチフィンガー ジェスチャー」の 15:47 の Kotlin スニペット
onNodeWithContentDescription("Photo")
    .performTrackpadInput {
        click(center)

        pan(Offset(0f, 100f))

        scale(0.9f)
    }
「間接入力と新しいフォーム ファクタ向けの API 設計」の 25:08 の Kotlin スニペット
InputDevice.ViewBehavior
.getPrimaryDirectionalMotionAxis()
「間接入力と新しいフォーム ファクタ向けの API 設計」の 25:32 の Kotlin スニペット
// Glimmer Library
Box(
    modifier =
        Modifier.fillMaxSize()
            .onIndirectPointerGesture(
                enabled = true,
                onSwipeForward = { /* onSwipeForward */ },
                onSwipeBackward = { /* onSwipeBackward */ },
                onClick = { /* onClick */ },
            )
            .focusTarget()
) {
    // App()
「間接ポインタ入力のテスト」の 26:11 の Kotlin スニペット
@Test
clickTest {

rule.onNodeWithTag(defaultTag)
.performTouchInput
{ click(center) }

}
「間接ポインタ入力のテスト」の 27:08 の Kotlin スニペット
@Test
clickTest {

rule.onNodeWithTag(defaultTag)
// .performTouchInput
.performIndirectPointerInput (
IndirectPointerEventPrimaryDirectionalMotionAxis.X,
inputDeviceSize,
)
{ click(center) }
}
「間接ポインタ入力のテスト」の 27:23 の Kotlin スニペット
@Test
clickTest {

rule.onNodeWithTag(defaultTag)
// .performTouchInput
.performIndirectPointerInput
{ click(center) }

}