フォーカス動作を変更する

画面上の要素のデフォルト フォーカス動作をオーバーライドする必要がある場合があります。たとえば、コンポーザブルをグループ化する、特定のコンポーザブルにフォーカスできない、1 つのコンポーザブルに明示的にフォーカスをリクエストする、フォーカスを取得または解放する、開始時または終了時にフォーカスをリダイレクトする場合などが考えられます。このセクションでは、デフォルトが不要な場合にフォーカスの動作を変更する方法について説明します。

フォーカス グループで一貫したナビゲーションを提供する

Jetpack Compose は、タブ付きナビゲーションの次のアイテムをすぐに推測しないことがあります。特に、タブやリストなどの複雑な親の Composables が関係する場合に顕著です。

フォーカス検索は通常、Composables の宣言順序に従いますが、階層内の Composables の 1 つが完全に表示されていない水平スクロール可能である場合など、これが不可能な場合もあります。以下に例を示します。

Jetpack Compose は、以下に示すように、一方向ナビゲーションで期待されるパスで続行するのではなく、画面の開始位置に最も近い次のアイテムをフォーカスすることを決定することがあります。

上部に水平ナビゲーションとアイテムのリストを表示するアプリのアニメーション。
図 1. 上部の水平ナビゲーションとその下にアイテムのリストを表示するアプリのアニメーション

この例では、フォーカスが [Chocolates] タブから次の最初の画像に移動してから [Pastries] タブに戻る意図がなかったことがわかります。代わりに、最後のタブまでタブでフォーカスし、それから内部のコンテンツに集中するようにしました。

上部に水平ナビゲーションとアイテムのリストを表示するアプリのアニメーション。
図 2. 上部の水平ナビゲーションとその下にアイテムのリストを表示するアプリのアニメーション

前の例のタブ行のように、コンポーザブルのグループが順番にフォーカスを取得する必要がある場合は、focusGroup() 修飾子を持つ親で Composable をラップする必要があります。

LazyVerticalGrid(columns = GridCells.Fixed(4)) {
    item(span = { GridItemSpan(maxLineSpan) }) {
        Row(modifier = Modifier.focusGroup()) {
            FilterChipA()
            FilterChipB()
            FilterChipC()
        }
    }
    items(chocolates) {
        SweetsCard(sweets = it)
    }
}

双方向ナビゲーションは、指定された方向に最も近いコンポーザブルを探します。別のグループの要素が、現在のグループ内の完全に表示されていないアイテムよりも近くにある場合、ナビゲーションは最も近いコンポーザブルを選択します。この動作を回避するには、focusGroup() 修飾子を適用します。

FocusGroup は、フォーカスの観点からグループ全体が単一のエンティティであるかのように見えますが、グループ自体がフォーカスを取得することはありません。代わりに、最も近い子がフォーカスを取得します。これにより、ナビゲーションは、グループから離れる前に、完全に表示されていないアイテムに移動することを認識します。

この場合、SweetsCards がユーザーに完全に表示され、一部の FilterChip が非表示になっている場合でも、FilterChip の 3 つのインスタンスが SweetsCard アイテムの前にフォーカスされます。これは、focusGroup 修飾子がフォーカス マネージャーに対して、ナビゲーションが簡単で、かつ UI との一貫性を高めるために、アイテムがフォーカスされる順序を調整するように指示するためです。

focusGroup 修飾子がない場合、FilterChipC が表示されていない場合、フォーカス ナビゲーションはそれを最後に選択します。ただし、このような修飾子を追加すると、検出可能になるだけでなく、ユーザーの期待どおりに FilterChipB の直後にフォーカスが取得されます。

コンポーザブルをフォーカス可能にする

一部のコンポーザブルは、ボタンや clickable 修飾子がアタッチされたコンポーザブルなど、設計上フォーカス可能です。コンポーザブルにフォーカス可能な動作を明示的に追加する場合は、focusable 修飾子を使用します。

var color by remember { mutableStateOf(Green) }
Box(
    Modifier
        .background(color)
        .onFocusChanged { color = if (it.isFocused) Blue else Green }
        .focusable()
) {
    Text("Focusable 1")
}

コンポーザブルをフォーカス不可にする

一部の要素がフォーカスに参加すべきでない場合もあります。このようなまれなケースでは、canFocus property を利用して Composable をフォーカス可能から除外できます。

var checked by remember { mutableStateOf(false) }

Switch(
    checked = checked,
    onCheckedChange = { checked = it },
    // Prevent component from being focused
    modifier = Modifier
        .focusProperties { canFocus = false }
)

FocusRequester でキーボード フォーカスをリクエストする

ユーザー操作に対するレスポンスとして、明示的にフォーカスをリクエストしたい場合があります。たとえば、フォームへの入力を再開するかどうかをユーザーに尋ね、ユーザーが「はい」を押した場合、そのフォームの最初のフィールドにフォーカスし直すことができます。

まず、キーボード フォーカスの移動先のコンポーザブルに FocusRequester オブジェクトを関連付けます。次のコード スニペットでは、Modifier.focusRequester という修飾子を設定して、FocusRequester オブジェクトを TextField に関連付けます。

val focusRequester = remember { FocusRequester() }
var text by remember { mutableStateOf("") }

TextField(
    value = text,
    onValueChange = { text = it },
    modifier = Modifier.focusRequester(focusRequester)
)

FocusRequester の requestFocus メソッドを呼び出して、実際のフォーカス リクエストを送信できます。このメソッドは、Composable コンテキストの外部で呼び出す必要があります(そうしないと、再コンポーズのたびに再実行されます)。次のスニペットは、ボタンがクリックされたときにキーボード フォーカスを移動するようシステムにリクエストする方法を示しています。

val focusRequester = remember { FocusRequester() }
var text by remember { mutableStateOf("") }

TextField(
    value = text,
    onValueChange = { text = it },
    modifier = Modifier.focusRequester(focusRequester)
)

Button(onClick = { focusRequester.requestFocus() }) {
    Text("Request focus on TextField")
}

フォーカスをキャプチャして解放

フォーカスを利用して、たとえば有効なメールアドレスや電話番号を取得するなど、アプリがタスクを実行するために必要な適切なデータをユーザーに提供するようユーザーに案内できます。エラー状態は、何が行われているかをユーザーに知らせますが、修正されるまでフォーカスを維持するために、誤った情報を含むフィールドが必要になることもあります。

フォーカスをキャプチャするには、次の例のように captureFocus() メソッドを呼び出し、その後、代わりに freeFocus() メソッドを使用して解放します。

val textField = FocusRequester()

TextField(
    value = text,
    onValueChange = {
        text = it

        if (it.length > 3) {
            textField.captureFocus()
        } else {
            textField.freeFocus()
        }
    },
    modifier = Modifier.focusRequester(textField)
)

フォーカス修飾子の優先順位

Modifiers は、子が 1 つしかない要素と見なすことができます。キューに置くと、左側(または上)にある各 Modifier が、右側(または下)に続く Modifier をラップします。つまり、2 番目の Modifier は最初のもの内に含まれるため、2 つの focusProperties を宣言する場合、次のものは最上位に含まれるため、最上位にある 1 つのみが機能します。

コンセプトをより明確にするには、次のコードをご覧ください。

Modifier
    .focusProperties { right = item1 }
    .focusProperties { right = item2 }
    .focusable()

この場合、右フォーカスとして item2 を示す focusProperties は、前のフォーカスに含まれているため使用されません。したがって、item1 が使用されます。

この方法を利用すると、親は FocusRequester.Default を使用して動作をデフォルトにリセットすることもできます。

Modifier
    .focusProperties { right = Default }
    .focusProperties { right = item1 }
    .focusProperties { right = item2 }
    .focusable()

親は同じ修飾子チェーンの一部である必要はありません。親コンポーザブルは、子コンポーザブルのフォーカス プロパティを上書きできます。たとえば、ボタンをフォーカス不可にする次の FancyButton について考えてみましょう。

@Composable
fun FancyButton(modifier: Modifier = Modifier) {
    Row(modifier.focusProperties { canFocus = false }) {
        Text("Click me")
        Button(onClick = { }) { Text("OK") }
    }
}

ユーザーは、canFocustrue に設定することで、このボタンを再びフォーカス可能にすることができます。

FancyButton(Modifier.focusProperties { canFocus = true })

他の Modifier と同様に、フォーカス関連オブジェクトは宣言する順序によって動作が異なります。たとえば、次のようなコードは Box をフォーカス可能にしますが、FocusRequester はフォーカス可能な後に宣言されるため、このフォーカス可能に関連付けられていません。

Box(
    Modifier
        .focusable()
        .focusRequester(Default)
        .onFocusChanged {}
)

focusRequester は、階層内でその下にある最初のフォーカス可能な子に関連付けられているため、この focusRequester は最初のフォーカス可能な子を指していることに留意してください。利用できない場合は、何も指定されません。 ただし、Box は(focusable() 修飾子により)フォーカス可能であるため、双方向ナビゲーションを使用して移動できます。

別の例として、onFocusChanged() 修飾子は focusable() 修飾子または focusTarget() 修飾子の後に表示される最初のフォーカス可能な要素を参照するため、次のいずれかを使用できます。

Box(
    Modifier
        .onFocusChanged {}
        .focusRequester(Default)
        .focusable()
)
Box(
    Modifier
        .focusRequester(Default)
        .onFocusChanged {}
        .focusable()
)

開始時または終了時にフォーカスをリダイレクトする

場合によっては、次のアニメーションに示すように、非常に特殊なナビゲーションを指定する必要があります。

2 列のボタンが横に並んで配置され、1 つの列からもう 1 つの列へフォーカスが移動している画面のアニメーション。
図 3. 2 列のボタンが並んで配置され、一方の列から他方の列へのフォーカスがアニメーション化されている画面のアニメーション

作成方法の説明に入る前に、フォーカス検索のデフォルトの動作を理解しておくことが重要です。何も変更しない場合、フォーカス検索が Clickable 3 項目に到達した後、D-pad(または同等の矢印キー)で DOWN を押すと、フォーカスが Column の下に表示されているものに移動し、グループが終了して右側のものは無視されます。使用可能なフォーカス可能なアイテムがない場合、フォーカスはどこにも移動せず、Clickable 3 に留まります。

この動作を変更して目的のナビゲーションを提供するには、focusProperties 修飾子を利用します。これにより、フォーカス検索が Composable に出入りするときの動作を管理できます。

val otherComposable = remember { FocusRequester() }

Modifier.focusProperties {
    exit = { focusDirection ->
        when (focusDirection) {
            Right -> Cancel
            Down -> otherComposable
            else -> Default
        }
    }
}

階層の特定の部分に出入りするときにフォーカスを特定の Composable に向けることができます。たとえば、UI に 2 つの列があり、最初の列が処理されるたびにフォーカスが 2 番目の列に切り替えられるようにする場合などです。

2 列のボタンが横に並んで配置され、1 つの列からもう 1 つの列へフォーカスが移動している画面のアニメーション。
図 4. 2 列のボタンが並んで配置され、一方の列から他方の列へのフォーカスがアニメーション化されている画面のアニメーション

この GIF では、フォーカスが Column 1 の Clickable 3 Composable に達すると、フォーカスされている次の項目は別の ColumnClickable 4 になります。この動作を実現するには、focusProperties 修飾子内で focusDirectionenter および exit の値を組み合わせる必要があります。どちらもフォーカスの方向をパラメータとして受け取り、FocusRequester を返すラムダを必要とします。このラムダは、次の 3 つの方法で動作します。FocusRequester.Cancel を返すとフォーカスの継続が停止しますが、FocusRequester.Default はその動作を変更しません。代わりに、別の Composable にアタッチされた FocusRequester を指定すると、その特定の Composable にフォーカスがジャンプします。

フォーカスの進行方向を変更

フォーカスを次のアイテムまたは正確な方向に進めるには、onPreviewKey 修飾子を使用し、moveFocus 修飾子でフォーカスを進めるように LocalFocusManager を暗黙的に指定します。

次の例は、フォーカス メカニズムのデフォルトの動作を示しています。tab キーの押下が検出されると、フォーカスはフォーカス リスト内の次の要素に進みます。これは通常、構成する必要があるものではありませんが、デフォルトの動作を変更できるようにするには、システムの内部動作を理解することが重要です。

val focusManager = LocalFocusManager.current
var text by remember { mutableStateOf("") }

TextField(
    value = text,
    onValueChange = { text = it },
    modifier = Modifier.onPreviewKeyEvent {
        when {
            KeyEventType.KeyUp == it.type && Key.Tab == it.key -> {
                focusManager.moveFocus(FocusDirection.Next)
                true
            }

            else -> false
        }
    }
)

このサンプルでは、focusManager.moveFocus() 関数が、指定されたアイテムに、または関数パラメータで暗黙的に指定された方向にフォーカスを進めます。