Fokusverhalten ändern

Manchmal muss das Standardfokusverhalten der Elemente auf Ihrem Bildschirm überschrieben werden. So können Sie beispielsweise zusammensetzbare Funktionen gruppieren, den Fokus auf eine bestimmte zusammensetzbare Funktion verhindern, den Fokus explizit auf eine zusammensetzbare Funktion anfordern, den Fokus erfassen oder freigeben oder den Fokus weiterleiten beim Ein- oder Beenden. In diesem Abschnitt wird beschrieben, wie Sie das Fokusverhalten ändern können, wenn die Standardeinstellungen nicht Ihren Anforderungen entsprechen.

Sorgen Sie für eine kohärente Navigation mit Fokusgruppen

Manchmal errät Jetpack Compose das richtige nächste Element für die Navigation mit Tabs nicht sofort, insbesondere wenn komplexe übergeordnete Composables wie Tabs und Listen ins Spiel kommen.

Die fokussierte Suche folgt normalerweise der Deklarationsreihenfolge von Composables. Dies ist jedoch in manchen Fällen unmöglich, z. B. wenn eine der Composables in der Hierarchie horizontal scrollbar ist und nicht vollständig sichtbar ist. Das wird im Beispiel unten gezeigt.

Jetpack Compose kann den Fokus auf das nächste Element richten, das am Anfang des Bildschirms liegt, wie unten gezeigt, anstatt den Pfad fortzusetzen, den Sie für die unidirektionale Navigation erwarten:

Animation einer App mit einer horizontalen Navigation oben und einer Liste von Elementen darunter.
Abbildung 1: Animation einer App mit einer horizontalen Navigation oben und einer Liste der darunterliegenden Elemente

In diesem Beispiel ist klar, dass die Entwickler nicht beabsichtigt hatten, vom Tab Schokolade zum ersten Bild unten und dann wieder zum Tab Süßwaren zu springen. Der Fokus sollte stattdessen auf den Tabs bis zum letzten Tab und dann auf den inneren Inhalt liegen:

Animation einer App mit einer horizontalen Navigation oben und einer Liste von Elementen darunter.
Abbildung 2: Animation einer App mit einer horizontalen Navigation oben und einer Liste der darunterliegenden Elemente

In Situationen, in denen es wichtig ist, dass eine Gruppe von zusammensetzbaren Funktionen nacheinander hervorgehoben wird, wie in der Tabzeile aus dem vorherigen Beispiel, müssen Sie die Composable in ein übergeordnetes Element umschließen, das den focusGroup()-Modifikator enthält:

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

Bei der bidirektionalen Navigation wird nach der nächstgelegenen zusammensetzbaren Funktion für die angegebene Richtung gesucht. Wenn ein Element aus einer anderen Gruppe näher liegt als ein nicht vollständig sichtbares Element in der aktuellen Gruppe, wird das nächstgelegene Element ausgewählt. Um dies zu vermeiden, können Sie den focusGroup()-Modifikator anwenden.

FocusGroup lässt eine ganze Gruppe im Hinblick auf den Fokus wie eine einzelne Einheit erscheinen. Die Gruppe selbst wird jedoch nicht im Fokus. Stattdessen wird das nächstgelegene untergeordnete Element in den Fokus rücken. Auf diese Weise weiß die Navigation, das nicht vollständig sichtbare Element aufzurufen, bevor die Gruppe verlassen wird.

In diesem Fall werden die drei Instanzen von FilterChip vor den SweetsCard-Elementen hervorgehoben, auch wenn die SweetsCards für den Nutzer vollständig sichtbar sind und einige FilterChip möglicherweise ausgeblendet sind. Das liegt daran, dass der focusGroup-Modifikator den Fokusmanager anweist, die Reihenfolge anzupassen, in der die Elemente fokussiert werden, um die Navigation einfacher und kohärenter zu gestalten.

Wenn FilterChipC ohne den Modifikator focusGroup nicht sichtbar wäre, wird sie von der Fokusnavigation zuletzt aufgerufen. Wenn Sie einen solchen Modifikator hinzufügen, wird er jedoch nicht nur sichtbar, sondern wird auch direkt nach FilterChipB hervorgehoben, wie es Nutzer erwarten würden.

Eine zusammensetzbare fokussierbare Funktion erstellen

Einige zusammensetzbare Funktionen sind fokussierbar, z. B. eine Schaltfläche oder eine zusammensetzbare Funktion, an die der clickable-Modifikator angehängt ist. Wenn Sie einer zusammensetzbaren Funktion ausdrücklich ein fokussierbares Verhalten hinzufügen möchten, verwenden Sie den Modifikator focusable:

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

Eine zusammensetzbare Funktion nicht fokussierbar machen

Es kann Situationen geben, in denen einige Ihrer Elemente nicht im Fokus stehen sollten. In diesen seltenen Fällen kannst du mit canFocus property verhindern, dass ein Composable fokussierbar ist.

var checked by remember { mutableStateOf(false) }

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

Tastaturfokus mit FocusRequester anfordern

In einigen Fällen kann es erforderlich sein, den Fokus als Antwort auf eine Nutzerinteraktion explizit anzufordern. Sie können beispielsweise einen Nutzer fragen, ob er ein Formular noch einmal ausfüllen möchte. Wenn er auf „Ja“ klickt, wird das erste Feld des Formulars neu fokussiert.

Verknüpfen Sie zuerst ein FocusRequester-Objekt mit der zusammensetzbaren Funktion, auf die Sie den Tastaturfokus verschieben möchten. Im folgenden Code-Snippet wird ein FocusRequester-Objekt mit einem TextField verknüpft, indem ein Modifizierer namens Modifier.focusRequester festgelegt wird:

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

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

Sie können die Methode requestFocus von FocusRequester aufrufen, um tatsächliche Fokusanfragen zu senden. Sie sollten diese Methode außerhalb eines Composable-Kontexts aufrufen. Andernfalls wird sie bei jeder Neuzusammensetzung noch einmal ausgeführt. Das folgende Snippet zeigt, wie Sie das System auffordern, den Tastaturfokus zu verschieben, wenn auf die Schaltfläche geklickt wird:

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")
}

Fokus erfassen und loslassen

Sie können Fokus nutzen, um Ihre Nutzer zu motivieren, die richtigen Daten bereitzustellen, die Ihre App für ihre Aufgabe benötigt, z. B. eine gültige E-Mail-Adresse oder Telefonnummer. Obwohl Fehlerstatus Ihre Nutzer über das Problem informieren, benötigen Sie möglicherweise das Feld mit den fehlerhaften Informationen, um konzentriert zu bleiben, bis es behoben ist.

Um den Fokus zu erfassen, können Sie die Methode captureFocus() aufrufen und anschließend wie im folgenden Beispiel mit der Methode freeFocus() freigeben:

val textField = FocusRequester()

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

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

Vorrang von Fokusmodifikatoren

Modifiers können als Elemente angesehen werden, die nur ein untergeordnetes Element haben. Wenn Sie sie in die Warteschlange stellen, umschließt jedes Modifier links (oder oben) das Modifier, das rechts (oder darunter) folgt. Das bedeutet, dass die zweite Modifier im ersten enthalten ist. Wenn also zwei focusProperties deklariert werden, funktioniert nur die oberste, da die folgenden in der obersten enthalten sind.

Im folgenden Code wird das Konzept näher erläutert:

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

In diesem Fall wird die focusProperties, die item2 als rechten Fokus angibt, nicht verwendet, da sie im vorherigen enthalten ist. Daher wird item1 verwendet.

Mit diesem Ansatz kann ein übergeordnetes Element das Verhalten auch mithilfe von FocusRequester.Default auf die Standardeinstellungen zurücksetzen:

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

Das übergeordnete Element muss nicht Teil derselben Modifikatorkette sein. Eine übergeordnete zusammensetzbare Funktion kann die Fokuseigenschaft einer untergeordneten zusammensetzbaren Funktion überschreiben. Beispielsweise ist die Schaltfläche durch den FancyButton nicht fokussierbar:

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

Ein Nutzer kann diese Schaltfläche wieder fokussierbar machen, indem er canFocus auf true setzt:

FancyButton(Modifier.focusProperties { canFocus = true })

Wie alle Modifier verhalten sich auch fokussierte Elemente je nach ihrer deklarierten Reihenfolge unterschiedlich. Beispielsweise macht Code wie der folgende das Box fokussierbar, aber FocusRequester ist diesem fokussierbaren Element nicht zugeordnet, da es nach dem fokussierbaren Element deklariert wird.

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

Ein focusRequester ist mit dem ersten fokussierbaren Element darunter verknüpft, das in der Hierarchie darunter liegt. Daher verweist dieses focusRequester auf das erste fokussierbare untergeordnete Element. Falls keiner vorhanden ist, verweist er auf nichts. Da die Box jedoch fokussierbar ist (dank des focusable()-Modifikators), können Sie sie über die bidirektionale Navigation aufrufen.

Als weiteres Beispiel würde eine der folgenden Methoden funktionieren, da sich der Modifizierer onFocusChanged() auf das erste fokussierbare Element bezieht, das nach dem focusable()- oder focusTarget()-Modifikator angezeigt wird.

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

Fokus beim Betreten oder Verlassen weiterleiten

Manchmal müssen Sie eine ganz bestimmte Art von Navigation bereitstellen, wie in der folgenden Animation gezeigt:

Animation eines Bildschirms mit zwei Spalten mit Schaltflächen, die nebeneinander angeordnet sind und den Fokus von einer Spalte zur anderen animiert.
Abbildung 3: Animation eines Bildschirms mit zwei Spalten mit Schaltflächen, die nebeneinander angeordnet sind und den Fokus von einer Spalte zur anderen animiert.

Bevor wir damit beginnen, wie dies erstellt wird, müssen Sie das Standardverhalten der fokussierten Suche verstehen. Sobald die fokussierte Suche das Element Clickable 3 erreicht, wird durch Drücken des Steuerkreuzes DOWN (oder der entsprechenden Pfeiltaste) der Fokus auf den Bereich unterhalb von Column verschoben. Dadurch wird die Gruppe verlassen und die Markierung rechts wird ignoriert. Wenn keine fokussierbaren Elemente verfügbar sind, wird der Fokus nirgendwo verschoben, sondern bleibt bei Clickable 3.

Wenn Sie dieses Verhalten ändern und die gewünschte Navigation bereitstellen möchten, können Sie den focusProperties-Modifikator verwenden. Damit können Sie festlegen, was passiert, wenn die Fokussuche in Composable ein- oder ausläuft:

val otherComposable = remember { FocusRequester() }

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

Es ist möglich, den Fokus immer dann auf eine bestimmte Composable zu richten, wenn diese einen bestimmten Teil der Hierarchie betritt oder verlässt. Das ist beispielsweise der Fall, wenn Ihre UI zwei Spalten hat und Sie sicherstellen möchten, dass bei der Verarbeitung der ersten Spalte zum zweiten gewechselt wird:

Animation eines Bildschirms mit zwei Spalten mit Schaltflächen, die nebeneinander angeordnet sind und den Fokus von einer Spalte zur anderen animiert.
Abbildung 4: Animation eines Bildschirms mit zwei Spalten mit Schaltflächen, die nebeneinander angeordnet sind und den Fokus von einer Spalte zur anderen animiert.

Sobald der Fokus in diesem GIF die Clickable 3 Composable in Column 1 erreicht, ist das nächste Element, das hervorgehoben wird, Clickable 4 in einem anderen Column. Um dieses Verhalten zu erreichen, können Sie focusDirection mit den Werten enter und exit im focusProperties-Modifikator kombinieren. Beide benötigen ein Lambda, das als Parameter die Richtung verwendet, aus der der Fokus kommt, und ein FocusRequester zurückgibt. Dieses Lambda kann sich auf drei verschiedene Arten verhalten: Durch die Rückgabe von FocusRequester.Cancel wird der Fokus nicht mehr fortgesetzt, während FocusRequester.Default sein Verhalten nicht ändert. Wenn stattdessen das FocusRequester an ein anderes Composable angehängt ist, kann der Fokus zu diesem spezifischen Composable springen.

Richtung des Fokus ändern

Um den Fokus auf das nächste Element oder in eine genaue Richtung zu verschieben, können Sie den onPreviewKey-Modifikator nutzen und den LocalFocusManager mit dem Modifikator moveFocus andeuten, um den Fokus zu verstärken.

Das folgende Beispiel zeigt das Standardverhalten des Fokusmechanismus: Wenn ein tab-Tastendruck erkannt wird, wird zum nächsten Element in der Fokusliste gewechselt. Auch wenn dies normalerweise nicht konfiguriert werden muss, ist es wichtig, das Innenleben des Systems zu kennen, um das Standardverhalten ändern zu können.

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
        }
    }
)

In diesem Beispiel bewegt die focusManager.moveFocus()-Funktion den Fokus auf das angegebene Element oder die im Funktionsparameter angegebene Richtung.