Zmiana sposobu ustawiania ostrości

Czasami konieczne jest zastąpienie domyślnego działania zaznaczenia elementów na ekranie. Możesz na przykład grupować funkcje kompozycyjne, uniemożliwić zaznaczenie danego elementu kompozycyjnego, wyraźnie poprosić o fokus na jeden z nich, ujęcie lub zwolnienie zaznaczenia albo przekierowywanie fokusu na wejście lub wyjście. Z tej sekcji dowiesz się, jak zmienić sposób działania, gdy domyślne ustawienia nie są potrzebne.

Spójna nawigacja dzięki grupom fokusowym

Czasami Jetpack Compose nie od razu odgaduje, jaki będzie następny element do nawigacji po kartach, zwłaszcza gdy w grę wchodzą złożone elementy nadrzędnego Composables, takie jak karty czy listy.

Wyszukiwanie zaznaczenia zwykle odbywa się zgodnie z kolejnością deklaracji elementu Composables, ale w niektórych przypadkach jest to niemożliwe, np. gdy element Composables w hierarchii ma element, który można przewijać w poziomie i nie jest on w pełni widoczny. Widać to w przykładzie poniżej.

Jetpack Compose może zdecydować się na umieszczenie następnego elementu najbliżej początku ekranu, jak pokazano poniżej, zamiast kontynuować oczekiwaną ścieżkę w przypadku nawigacji jednokierunkowej:

Animacja aplikacji przedstawiająca górną, poziomą nawigację i listę elementów poniżej.
Rysunek 1. Animacja aplikacji przedstawiająca górną, poziomą nawigację i listę elementów poniżej.

W tym przykładzie widać, że deweloperzy nie zamierzali przeskoczyć z karty Czekoladki na pierwszy obraz poniżej, a potem z powrotem otworzyć kartę Ciasteczka. Chodziło o to, aby skupić się na kartach aż do ostatniej, a następnie na ich treści wewnętrznej:

Animacja aplikacji przedstawiająca górną, poziomą nawigację i listę elementów poniżej.
Rysunek 2. Animacja aplikacji przedstawiająca górną, poziomą nawigację i listę elementów poniżej.

Gdy ważne jest, aby grupa elementów kompozycyjnych była zaznaczana sekwencyjnie, tak jak w wierszu Karty z poprzedniego przykładu, trzeba umieścić element Composable w elemencie nadrzędnym z modyfikatorem focusGroup():

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

Nawigacja dwukierunkowa szuka najbliższego elementu kompozycyjnego dla danego kierunku – jeśli element z innej grupy znajduje się bliżej niewidocznego elementu w bieżącej grupie, nawigacja wybiera najbliższy element. Aby tego uniknąć, możesz zastosować modyfikator focusGroup().

Funkcja FocusGroup sprawia, że cała grupa wygląda jak pojedynczy element pod względem fokusu, ale sama grupa nie jest koncentrowana – zamiast tego koncentruje się najbliższy element podrzędny. Dzięki temu nawigacja wie, że przed opuszczeniem grupy może przejść do niewidocznego elementu.

W tym przypadku 3 wystąpienia FilterChip będą zaznaczone przed elementami SweetsCard, nawet jeśli SweetsCards są w pełni widoczne dla użytkownika, a niektóre FilterChip mogą być ukryte. Dzieje się tak, ponieważ modyfikator focusGroup informuje menedżera zaznaczenia o konieczności dostosowania kolejności elementów, w której są wskazywane elementy. Dzięki temu nawigacja jest łatwiejsza i spójniejsza z interfejsem.

Bez modyfikatora focusGroup, jeśli element FilterChipC nie był widoczny, nawigacja po skupieniu zajęłaby go jako ostatnia. Dodanie takiego modyfikatora sprawia jednak, że jest on nie tylko wykrywalny, ale także przyciąga uwagę zaraz po FilterChipB, zgodnie z oczekiwaniami użytkowników.

Tworzenie elementu kompozycyjnego, do którego można zaznaczyć element

Niektóre elementy kompozycyjne z założenia można zaznaczyć, na przykład przycisk lub funkcja kompozycyjna z dołączonym modyfikatorem clickable. Jeśli chcesz dodać do funkcji kompozycyjnej działanie możliwe do zaznaczenia, użyj modyfikatora focusable:

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

Ustawianie funkcji kompozycyjnej bez zaznaczenia

Może się zdarzyć, że niektóre elementy nie powinny się na niej znaleźć. W rzadkich przypadkach możesz użyć komponentu canFocus property, aby wykluczyć element Composable z możliwości zaznaczenia.

var checked by remember { mutableStateOf(false) }

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

Wysyłanie prośby o zaznaczenie klawiatury przy użyciu klawisza FocusRequester

W niektórych przypadkach możesz chcieć wprost zażądać zaznaczenia jako odpowiedzi na interakcję użytkownika. Możesz na przykład zapytać użytkownika, czy chce jeszcze raz rozpocząć wypełnianie formularza, a jeśli kliknie „Tak”, – aby zmienić jego fokus na pierwsze pole formularza.

Najpierw musisz powiązać obiekt FocusRequester z funkcją kompozycyjną, na którą chcesz przenieść zaznaczenie. W tym fragmencie kodu obiekt FocusRequester jest powiązany z obiektem TextField przez ustawienie modyfikatora o nazwie Modifier.focusRequester:

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

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

Aby wysyłać rzeczywiste żądania skupienia, możesz wywoływać metodę requestFocus klasy FocusRequester. Tę metodę należy wywołać poza kontekstem Composable (w przeciwnym razie będzie wykonywana ponownie przy każdej zmianie kompozycji). Ten fragment kodu pokazuje, jak zażądać od systemu przeniesienia fokusu klawiatury po kliknięciu przycisku:

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

Uchwyć i zwolnij fokus

Możesz je wykorzystać, aby wskazać użytkownikom, których danych potrzebuje Twoja aplikacja do wykonania zadania, takich jak prawidłowy adres e-mail lub numer telefonu. Stany błędów informują użytkowników, co się dzieje, jednak pole z błędnymi informacjami może pozostać aktywne, dopóki problem nie zostanie naprawiony.

Aby przechwycić zaznaczenie, możesz wywołać metodę captureFocus(), a następnie zwolnić ją za pomocą metody freeFocus(), jak w tym przykładzie:

val textField = FocusRequester()

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

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

Pierwszeństwo modyfikatorów ostrości

Modifiers można rozpoznać jako elementy, które mają tylko 1 element podrzędny, więc gdy umieścisz je w kolejce, każdy element Modifier po lewej (lub u góry) zawija tag Modifier, który następuje po prawej (lub poniżej). Oznacza to, że druga wartość Modifier jest zawarta w pierwszej, więc podczas zadeklarowania dwóch elementów focusProperties działa tylko najwyżej jeden z nich, ponieważ te znajdują się na samej górze.

Aby lepiej wyjaśnić, o co chodzi, przeczytaj ten kod:

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

W tym przypadku obiekt focusProperties wskazujący item2 jako prawidłowy nie zostanie użyty, ponieważ znajduje się w poprzednim elemencie, więc używany będzie item1.

Korzystając z tego podejścia, rodzic może też przywrócić domyślne zachowanie, używając parametru FocusRequester.Default:

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

Element nadrzędny nie musi być częścią tego samego łańcucha modyfikatora. Nadrzędny obiekt kompozycyjny może zastąpić właściwość elementu podrzędnego elementu kompozycyjnego. Weźmy na przykład ten element FancyButton, który sprawia, że nie można zaznaczyć przycisku:

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

Użytkownik może ponownie aktywować ten przycisk, ustawiając canFocus na true:

FancyButton(Modifier.focusProperties { canFocus = true })

Tak jak w przypadku każdego elementu Modifier, funkcje związane z fokusem działają różnie w zależności od kolejności ich zadeklarowania. Na przykład kod poniżej umożliwia zaznaczenie elementu Box, ale element FocusRequester nie jest z nim powiązany, ponieważ jest zadeklarowany po elemencie, który można zaznaczyć.

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

Pamiętaj, że element focusRequester jest powiązany z pierwszym elementem w hierarchii, który znajduje się pod nim, więc focusRequester wskazuje pierwszy element podrzędny, który można zaznaczyć. Jeśli żadna nie będzie dostępna, element nie będzie niczego wskazywać. Ponieważ jednak obiekt Box można zaznaczyć (dzięki modyfikatorowi focusable()), możesz przechodzić do niego za pomocą nawigacji dwukierunkowej.

Inny przykład będzie działać, ponieważ modyfikator onFocusChanged() odnosi się do pierwszego elementu, który można zaznaczyć, który pojawia się po modyfikatorach focusable() lub focusTarget().

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

Przekieruj fokus po otwarciu lub zamknięciu

Czasem musisz udostępnić konkretny rodzaj nawigacji, np. ten na ilustracji poniżej:

Animacja przedstawiająca ekran z 2 kolumnami przycisków umieszczonymi obok siebie i animowanymi fokusem z jednej kolumny do drugiej.
Rysunek 3. Animacja przedstawiająca ekran z 2 kolumnami przycisków umieszczonymi obok siebie i animowanymi fokusem z jednej kolumny do drugiej.

Zanim przejdziemy do szczegółów, warto poznać działanie domyślnego wyszukiwania zaznaczenia. Bez żadnych modyfikacji, gdy zaznaczenie dotrze do elementu Clickable 3, naciśnięcie DOWN na padzie kierunkowym (lub odpowiedniego klawisza strzałki) spowoduje przeniesienie zaznaczenia na element, który jest wyświetlany pod elementem Column, spowoduje to zamknięcie grupy i zignoruje to po prawej stronie. Jeśli nie ma dostępnych elementów, do których można przejść, zaznaczenie nie przesuwa się w żaden sposób, ale pozostaje na elemencie Clickable 3.

Aby zmienić ten sposób działania i zapewnić użytkownikom odpowiednią nawigację, możesz skorzystać z modyfikatora focusProperties, który pomaga zarządzać tym, co ma się dziać, gdy zaznaczone pole wyszukiwania pojawi się w polu Composable, a co poza nim:

val otherComposable = remember { FocusRequester() }

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

Możesz skierować zaznaczenie na konkretny element Composable za każdym razem, gdy wchodzi on lub opuszcza pewną część hierarchii, np. jeśli w interfejsie użytkownika są 2 kolumny i chcesz mieć pewność, że przy każdym przetwarzaniu pierwszej z nich fokus zostanie przełączony na drugą:

Animacja przedstawiająca ekran z 2 kolumnami przycisków umieszczonymi obok siebie i animowanymi fokusem z jednej kolumny do drugiej.
Rysunek 4. Animacja przedstawiająca ekran z 2 kolumnami przycisków umieszczonymi obok siebie i animowanymi fokusem z jednej kolumny do drugiej.

W tym GIF-ie, gdy zaznaczenie osiągnie wartość Clickable 3 Composable w okresie Column 1, kolejny zaznaczony element to Clickable 4 w innym elemencie Column. Można to osiągnąć, łącząc wartości focusDirection z wartościami enter i exit w modyfikatorze focusProperties. Wymagają one funkcji lambda, która określa kierunek, z którego pochodzi punkt skupienia, i zwraca FocusRequester. Ta funkcja lambda może działać na 3 różne sposoby: zwrócenie pola FocusRequester.Cancel zatrzymuje zaznaczenie, a element FocusRequester.Default nie zmienia swojego działania. Dodanie elementu FocusRequester powiązanego z innym elementem Composable powoduje przejście do tego konkretnego elementu Composable.

Zmiana kierunku przechodzenia

Aby przenieść zaznaczenie na następny element lub w kierunku konkretnego kierunku, możesz użyć modyfikatora onPreviewKey i wskazać element LocalFocusManager, który przesuwa punkt ostrości za pomocą modyfikatora moveFocus.

Poniższy przykład pokazuje domyślne zachowanie mechanizmu zaznaczania: po wykryciu naciśnięcia klawisza tab następuje przejście do następnego elementu na liście. Zwykle nie trzeba go konfigurować, ale warto znać wewnętrzne działanie systemu, aby mieć możliwość zmiany tego działania.

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

W tym przykładzie funkcja focusManager.moveFocus() przenosi zaznaczenie na określony element lub kierunek określony w parametrze funkcji.