フォーカス ターゲットをグループ化する

focusGroup 修飾子を使用すると、複数のフォーカス ターゲットをグループ化できます。グループ化により、フォーカス階層の管理、論理 UI 領域(ツールバー、ダイアログ アクション、カードリストなど)の分離、フォーカスが領域に入ったときや領域から出たときのインターセプトが可能になります。

フォーカス グループのコンセプト: 開始、終了、デフォルトのトラバーサル

フォーカス グループは、子フォーカス ターゲットの周囲にまとまりのあるナビゲーション境界を確立します。この境界を越えてフォーカスが移動する仕組みを理解するには、次の 2 つの重要なコンセプトを理解する必要があります。

  • Enter: フォーカスがフォーカス グループ外の要素からグループ内の要素に移動したときに発生するイベント。
  • 終了: フォーカスがグループの最後の内部要素から離れ、外部のフォーカス ターゲットに移行したときに発生するイベント。
フォーカス グループの境界を越える Enter 遷移と Exit 遷移を示す図。
図 1. 開始イベントと終了イベントを含むフォーカス グループ。

デフォルトのフォーカス走査動作

デフォルトでは、フォーカス グループが外部からフォーカスを受け取ると、次のようになります。

  1. グループに入る: Compose は、内部の子フォーカス ターゲットを自動的に選択します。1 次元(Tab)ナビゲーションの場合、視覚的な表示順序で最初の子が選択されます。2 次元(矢印キーまたは D-pad)ナビゲーションの場合、入力方向に幾何学的に最も近い子を選択します。
  2. グループ内の移動: 移動キーを繰り返し押すと、グループで囲まれた兄弟フォーカス ターゲット間でフォーカスが移動します。
  3. グループの終了: ユーザーがグループ内の最後のフォーカス ターゲットを通過すると、次のキー操作でグループが終了し、グループ外の隣接するフォーカス ターゲットにフォーカスが移動します。

フォーカス エントリのリダイレクトをカスタマイズする

多くの UI 設計では、デフォルトのエントリ ターゲット(表示順序の最初の子など)は、ユーザーにとって最も論理的または便利な選択肢ではありません。

目的とユースケース

左側に [キャンセル] ボタン、右側にメインの [確認] ボタンがある確認ダイアログを考えてみましょう。ユーザーがフォーム フィールドからボタンバーに移動した場合:

  • デフォルトの動作: [キャンセル] は視覚的な表示順序で最初のボタンであるため、フォーカスは [キャンセル] に移動します。
  • 意図した動作: [Confirm] はメインの推奨される対応であるため、フォーカス順序の早い段階でボタングループに入るときに、フォーカスが [Confirm] に直接移動するようにします。必要に応じて、ユーザーが Left を押して [Cancel] を選択できるようにします。
ボタングループへのデフォルトのフォーカス エントリ。
図 2.キャンセル ボタンと確認 ボタン(デフォルトのフォーカス エントリ ターゲット)。

その他の一般的なユースケースは次のとおりです。

  • TabRow に入るときに、現在選択されているタブにフォーカスを直接移動する。
  • メディア再生バーに入ったときに、フォーカスを [再生 / 一時停止] コントロールに直接移動する。

onEnter と FocusRequester を使用した実装

フォーカス グループに入ったときにフォーカスをリダイレクトするには:

  1. 関連するフォーカス ターゲットを囲む親コンテナに focusGroup 修飾子を適用します。
  2. コンテナに focusProperties 修飾子を適用し、onEnter コールバックを構成します。
  3. onEnter コールバック(FocusEnterExitScope にスコープ設定)で、focusRequester を使用して、優先されるデフォルトの子に関連付けられた FocusRequester で requestFocus() を呼び出します。

val defaultChildRequester = remember { FocusRequester() }

Row(
    modifier = Modifier
        .focusGroup()
        .focusProperties {
            // Intercept entry into this group and redirect focus to the primary action
            onEnter = {
                defaultChildRequester.requestFocus()
            }
        }
) {
    Button(onClick = { /* Cancel action */ }) {
        Text("Cancel")
    }
    Button(
        onClick = { /* Confirm action */ },
        modifier = Modifier.focusRequester(defaultChildRequester)
    ) {
        Text("Confirm (Default)")
    }
}

onEnter と onExit の内部で cancelFocusChange() を呼び出して、境界を越えるフォーカス遷移を完全にブロックすることも、requestedFocusDirection を検査して、ユーザーが Tab、Down、Left のいずれを使用して移動したかに基づいてルーティングをカスタマイズすることもできます。

スクロール可能なコンテナはデフォルトでフォーカス グループになる

Jetpack Compose では、LazyRow、LazyColumn、LazyVerticalGrid などのスクロール可能なコンテナが自動的にフォーカス グループとして機能します。onEnter コールバックまたは onExit コールバックを設定するために、focusGroup 修飾子を明示的に適用する必要はありません。