Compose での片手操作ジェスチャー


Wear OS 7(API レベル 37)以降では、片手ジェスチャー フレームワークと Compose for Wear OS の一部である API を使用して、ユーザーがタッチフリーでアプリを操作できるようになります。

このフレームワークは、当初は Google Pixel Watch デバイス(Google Pixel Watch 3 以降)でサポートされていましたが、すべての OEM で利用できるようになりました。この API を採用することで、ハードウェア サポートの拡大に伴い、アプリのジェスチャー サポートがエコシステム全体に自動的に拡張されます。

UI を煩雑にすることなく、利用可能なジェスチャーをユーザーが発見できるように、Wear OS フレームワークにはアニメーション化されたジェスチャー インジケーターが用意されています。これらの視覚的なヒントは、ジェスチャーを実行できる場所をハイライト表示します。システムは、ユーザーの設定に応じて、表示の頻度とミュートの頻度を自動的に管理します。

サポートされているジェスチャーと操作

Wear OS ジェスチャー フレームワークは、次の 2 種類のジェスチャーをサポートしています。

  • メイン アクション(ダブルピンチ): 電話への応答やメディア再生の切り替えなど、画面上のメイン アクションにマッピングされます。
  • 閉じる操作(手首を回す): 戻るナビゲーション、ダイアログを閉じる、プロンプトをキャンセルする、にマッピングされます。

Compose でジェスチャーを構成する

片手ジェスチャー API を使用すると UI を強化できますが、一部のハードウェアと OEM はこれらのジェスチャーをサポートしていないことに注意してください。API が、アプリがこれらのサポートされていないデバイスのいずれかで実行されていることを検出すると、ライブラリは標準のタッチ操作に影響を与えることなく、自動的に no-op を実行します。

標準の Compose の動作と同様に、修飾子を使用して UI 要素で片手ジェスチャーを有効にします。アプリのジェスチャーは、実行するアクション(メインまたは閉じる)と、システムレベルのユーザー設定(ヒントの表示頻度や頻度のミュートなど)を調整する gestureId に応じて構成します。この構成は、OneHandedGestureConfiguration オブジェクトを作成することで表現します。作成には rememberOneHandedGestureConfiguration 関数を使用することをおすすめします。OneHandedGestureConfiguration では、ジェスチャーの優先度を指定することもできます。

rememberOneHandedGestureConfiguration 関数は、アプリの状態を公開することなく、再コンポジション全体でユーザー インタラクションの履歴を追跡します。アプリで構成を作成したら、インタラクティブなコンポーザブルの Modifier.oneHandedGesture に構成を渡す必要があります。

ユーザーが利用可能なジェスチャーを見つけられるように、ライブラリには OneHandedGestureClickIndicator メソッドが用意されています。このメソッドは、基盤となるコンテンツを置き換えて、ジェスチャー操作が利用可能であることをユーザーに示すラッパーとして機能します。

インタラクティブな構成要素

ボタンなどのインタラクティブなコントロールでジェスチャーを有効にするには、OneHandedGestureAction.Primary を指定する構成を作成し、oneHandedGesture 修飾子を適用します。コントロールと修飾子の両方に同じ MutableInteractionSource を渡して、ジェスチャー イベントがコントロールに視覚的な押下フィードバックを出力するようにします。

ジェスチャー インジケーターを有効にするには、OneHandedGestureClickIndicatorState のインスタンスを作成して記憶します。次に、視覚的なフィードバックをトリガーするには、oneHandedGesture 修飾子によって提供される onGestureAvailable コールバック内で showIndicator を呼び出します。これにより、インジケーション イベントが発生したことがシステムに通知されます。呼び出されると、コンポーネントは通常のコンテンツをジェスチャー アニメーションに一時的に置き換えます。

var isPlaying by remember { mutableStateOf(false) }
val onClick = { isPlaying = !isPlaying }

val gestureConfig = rememberOneHandedGestureConfiguration(
    action = OneHandedGestureAction.Primary
)
val indicatorState = remember { OneHandedGestureClickIndicatorState() }
val coroutineScope = rememberCoroutineScope()
val interactionSource = remember { MutableInteractionSource() }

Button(
    onClick = onClick,
    interactionSource = interactionSource,
    modifier = Modifier
        .fillMaxWidth()
        .oneHandedGesture(
            gestureConfiguration = gestureConfig,
            interactionSource = interactionSource,
            onGestureLabel = if (isPlaying) "pause" else "play",
            onGestureAvailable = { coroutineScope.launch { indicatorState.showIndicator() } },
            onGesture = onClick
        )
) {
    OneHandedGestureClickIndicator(
        gestureConfiguration = gestureConfig,
        state = indicatorState
    ) {
        Text(if (isPlaying) "Pause" else "Play", modifier = Modifier.fillMaxWidth())
    }
}

スクロール可能なコンテナ

スクロール可能な画面またはリストの場合は、OneHandedGestureAction.Primary を指定する構成を作成し、oneHandedGesture 修飾子をコンテナに適用して、scrollDown などのスクロール ヘルパーを呼び出します。

スクロール操作の視覚的なフィードバックを提供するには、OneHandedGestureScrollIndicator を使用します。このコンポーネントは、スクロール位置を示す標準のスクロール インジケーターとして機能しますが、スクロール ジェスチャーが利用可能であることをユーザーに示すこともできます。このインジケーターは通常、ScreenScaffold の scrollIndicator スロットに渡され、TransformingLazyColumn などのスクロール可能なコンテナの状態と結合されます。また、OneHandedGestureScrollIndicatorState を監視して、視覚的な遷移を管理します。

視覚的なフィードバックをトリガーするには、この状態で showIndicator を呼び出します。通常は、oneHandedGesture 修飾子の onGestureAvailable コールバック内で呼び出します。トリガーされると、インジケーターは標準の視覚状態をジェスチャー アニメーション シーケンスに一時的に置き換えて、ユーザーに警告します。

val scrollState = rememberTransformingLazyColumnState()
val gestureConfig = rememberOneHandedGestureConfiguration(
    action = OneHandedGestureAction.Primary,
    priority = OneHandedGesturePriority.Scrollable
)
val indicatorState = remember(gestureConfig) { OneHandedGestureScrollIndicatorState() }
val coroutineScope = rememberCoroutineScope()

ScreenScaffold(
    scrollState = scrollState,
    scrollIndicator = {
        OneHandedGestureScrollIndicator(
            gestureConfiguration = gestureConfig,
            indicatorState = indicatorState,
            scrollState = scrollState,
            modifier = Modifier.align(Alignment.CenterEnd)
        )
    }
) { contentPadding ->
    TransformingLazyColumn(
        state = scrollState,
        contentPadding = contentPadding,
        modifier = Modifier
            .fillMaxSize()
            .oneHandedGesture(
                gestureConfiguration = gestureConfig,
                onGestureLabel = "scroll",
                onGestureAvailable = {
                    coroutineScope.launch { indicatorState.showIndicator() }
                },
                onGesture = { OneHandedGestureDefaults.scrollDown(scrollState) }
            )
    ) {
        items(10) { index ->
            Text("Item $index", modifier = Modifier.padding(8.dp))
        }
    }
}

複数のジェスチャーを組み合わせる

OneHandedGestureConfiguration オブジェクトに gesturePriority を追加することで、同じプライマリ アクションでスクロール ジェスチャーとクリック ジェスチャーの両方を構成できます。

  • OneHandedGesturePriority.Clickable(最高): Button や Card などのインタラクティブなコントロールに割り当てて、画面に表示されたときにジェスチャーをキャプチャできるようにします。
  • OneHandedGesturePriority.Scrollable(中): スクロール可能またはページング可能なコンテナに割り当てます。これにより、クリック可能な子に譲りますが、クリック可能なコントロールが表示されていない場合はスクロールします。
  • OneHandedGesturePriority.Unspecified(最低): 割り当てられていない優先度。これは、priority が設定されていないジェスチャーのデフォルト値です。

内部ボタンに priority = OneHandedGesturePriority.Clickable を明示的に設定し、その親リストに priority = OneHandedGesturePriority.Scrollable を明示的に設定することで、システムはこのジェスチャー優先動作を表示できます。ユーザーが片手操作でメイン アクションをトリガーすると、まずボタンが表示されるまでリストが下にスクロールされ、次にボタンのクリック アクションがキャプチャされます。

ADB を使用してジェスチャーをテスト、デバッグする

Android Debug Bridge(adb)と IWearGestureService システム サービスを使用すると、実際に手首を動かすことなく、実機またはエミュレータで片手ジェスチャーをテストできます。

ジェスチャー シミュレーションを有効にする

ADB を使用してジェスチャーをシミュレートする前に、デバイスの設定と制約のオーバーライドを構成します。

  1. Wear OS デバイスに Wear OS 7(API レベル 37)以降が搭載されていることを確認します。

    adb shell getprop ro.build.version.sdk
    
  2. 手首に装着していない、または充電器に置かれている実機でテストする場合は、ジェスチャー フレームワークがアクティブなままになるように、オフボディ制約をオーバーライドします。

    adb shell cmd IWearGestureService override-constraints offbody-state
    

ADB を使用してジェスチャー イベントをトリガーする

ダブルピンチ ジェスチャー(Google Pixel Watch の Primary アクション)をシミュレートするには、次の ADB シェルコマンドを実行します。

adb shell cmd IWearGestureService gesture DoublePinch

手首を返すジェスチャー(Google Pixel Watch の Dismiss アクション)をシミュレートするには、次の ADB シェルコマンドを実行します。

adb shell cmd IWearGestureService gesture WristTurn

ジェスチャー ヒントのトラッキングをリセット

システムはユーザー操作の履歴を追跡し、グローバルなケイデンス設定([常に] や [毎日] など)に基づいて、フローティング ジェスチャー ヒントを表示します。アプリのジェスチャー インジケーターをデバッグするときは、このトラッキング履歴をリセットして、パッケージのヒントが再び表示されるようにします。

  • userdebug ビルドまたはエミュレータの場合:

    adb shell cmd IWearGestureService hint clear <your_package_name>
    
  • 製品版(user)ビルドの場合:

    root アクセス権のない商用デバイスでは、hint clear はシステム権限によってブロックされます。アプリのローカルデータを消去して、ヒントの検出をリセットします。

    adb shell pm clear <your_package_name>
    

デフォルトの制約を復元する

テストが完了したときに、すべてのデバッグ制約のオーバーライドをリセットするには:

adb shell cmd IWearGestureService override-constraints reset

ジェスチャー インジェクションのトラブルシューティング

アプリがシミュレートされたジェスチャーを受信しない場合:

  1. スマートウォッチの画面が復帰してオンになっていることを確認します。画面がオフまたは常に画面表示モードの場合、ジェスチャー フレームワークはジェスチャーをアプリにディスパッチしません。ADB を使用してディスプレイを復帰させるには、次のコマンドを実行します。

    adb shell input keyevent KEYCODE_WAKEUP
    
  2. アプリがアクティブなジェスチャー サブスクライバーとして登録され、現在ウィンドウ フォーカスを保持しているかどうかを確認します。

    adb shell cmd IWearGestureService get-active-gestures -readable
    

    アプリのジェスチャー画面がフォアグラウンドにあり、画面が起動している場合、このコマンドは [DoublePinch] または [DoublePinch, WristTurn] を返します。空のリスト([])が返された場合は、ウィンドウにフォーカスがあるかどうか、またはオフボディ制約によってアクティベーションがブロックされているかどうかを確認します。

  3. 内部ジェスチャー サービスのステータスとアクティブなサブスクライバー トークンを検査します。

    adb shell dumpsys IWearGestureService
    

参考情報

片手ジェスチャーを使用するタイミングと場所に関する設計ガイダンスについては、片手ジェスチャーをご覧ください。