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

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

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

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

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

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

デフォルトでは、フォーカス グループが外部からフォーカスを受け取ると:

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

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

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

意図とユースケース

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

  • デフォルトの動作: 視覚的な順序で最初のボタンである [キャンセル] にフォーカスが移動します。
  • 意図した動作: [確認] が推奨される対応であるため、フォーカス順序の早い段階でボタン グループに入ったときに、フォーカスが [確認] に直接移動するようにしつつ、必要に応じてユーザーが 左を押して [キャンセル] を選択できるようにします。
ボタン グループへのデフォルトのフォーカス エントリ。
図 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 修飾子を明示的に適用する必要はありません。