Modifica comportamento dello stato attivo

A volte è necessario sostituire il comportamento predefinito dello stato attivo per gli elementi sullo schermo. Ad esempio, potresti voler raggruppare gli elementi componibili, impedire lo stato attivo di un determinato componibile, richiedere esplicitamente lo stato attivo su un elemento, acquisire o rilasciare lo stato attivo o impostare lo stato attivo del reindirizzamento all'entrata o all'uscita. Questa sezione descrive come modificare il comportamento dello stato attivo quando i valori predefiniti non sono ciò di cui hai bisogno.

Offri una navigazione coerente con i focus group

A volte, Jetpack Compose non indovina subito l'elemento successivo corretto per la navigazione a schede, soprattutto quando entrano in gioco un elemento Composables principale complesso come schede ed elenchi.

Anche se la ricerca dello stato attivo di solito segue l'ordine di dichiarazione di Composables, in alcuni casi questo è impossibile, ad esempio quando uno degli Composables nella gerarchia è un elemento scorrevole orizzontale non completamente visibile. come mostrato nell'esempio seguente.

Jetpack Compose potrebbe decidere di impostare lo stato attivo sull'elemento successivo più vicino all'inizio della schermata, come mostrato di seguito, anziché continuare sul percorso previsto per la navigazione unidirezionale:

Animazione di un'app che mostra una navigazione orizzontale in alto e un elenco di elementi sotto.
Figura 1. Animazione di un'app che mostra una navigazione orizzontale in alto e un elenco di elementi sotto

In questo esempio, è chiaro che gli sviluppatori non avevano intenzione di passare dalla scheda Cioccolatini alla prima immagine di seguito e per poi tornare alla scheda Pasticceria. Invece, volevano concentrarsi sulle schede fino all'ultima scheda per poi concentrarsi sui contenuti interni:

Animazione di un'app che mostra una navigazione orizzontale in alto e un elenco di elementi sotto.
Figura 2. Animazione di un'app che mostra una navigazione orizzontale in alto e un elenco di elementi sotto

Nelle situazioni in cui è importante che un gruppo di elementi componibili si concentri in sequenza, ad esempio nella riga Tab dell'esempio precedente, devi racchiudere Composable in un elemento principale che abbia il modificatore focusGroup():

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

La navigazione bidirezionale cerca l'elemento componibile più vicino per la direzione specificata: se un elemento di un altro gruppo è più vicino a un elemento non completamente visibile nel gruppo corrente, la navigazione sceglie quello più vicino. Per evitare questo comportamento, puoi applicare il modificatore focusGroup().

FocusGroup fa apparire un intero gruppo come una singola entità in termini di focus, ma il gruppo non sarà in primo piano, ma verrà concentrato sul gruppo più vicino. In questo modo, la navigazione sa di andare all'elemento non completamente visibile prima di uscire dal gruppo.

In questo caso, le tre istanze di FilterChip verranno impostate prima degli elementi SweetsCard, anche quando SweetsCards sono completamente visibili all'utente e alcuni FilterChip potrebbero essere nascosti. Questo accade perché il modificatore focusGroup indica al gestore dell'elemento attivo di regolare l'ordine in cui vengono attivati gli elementi, in modo che la navigazione sia più semplice e coerente con l'UI.

Senza il modificatore focusGroup, se FilterChipC non era visibile, la navigazione dello stato attivo lo rileverebbe per ultimo. Tuttavia, l'aggiunta di un modificatore di questo tipo lo rende non solo rilevabile, ma acquisirà anche lo stato attivo subito dopo FilterChipB, come previsto dagli utenti.

Creazione di un oggetto componibile con focus

Alcuni componibili sono pensati per lo stato attivo, come un pulsante o un componibile a cui è applicato il modificatore clickable. Se vuoi aggiungere specificamente un comportamento attivabile a un componibile, utilizza il modificatore focusable:

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

rendere un componibile non attivabile

Potrebbero verificarsi situazioni in cui alcuni dei tuoi elementi non dovrebbero entrare in gioco. In queste rare occasioni, puoi utilizzare canFocus property per escludere un Composable dall'elemento attivabile.

var checked by remember { mutableStateOf(false) }

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

Richiedi lo stato attivo della tastiera con FocusRequester

In alcuni casi, potresti voler richiedere esplicitamente lo stato attivo in risposta a un'interazione dell'utente. Ad esempio, potresti chiedere a un utente se vuole riavviare la compilazione di un modulo e se preme "sì" vuoi ridefinire il primo campo di quel modulo.

La prima cosa da fare è associare un oggetto FocusRequester al componibile su cui vuoi spostare lo stato attivo della tastiera. Nel seguente snippet di codice, un oggetto FocusRequester viene associato a un TextField impostando un modificatore chiamato Modifier.focusRequester:

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

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

Puoi chiamare il metodo requestFocus di FocusRequester per inviare richieste di stato attivo effettive. Devi richiamare questo metodo al di fuori di un contesto Composable (altrimenti, viene eseguito nuovamente a ogni ricomposizione). Il seguente snippet mostra come richiedere al sistema di spostare lo stato attivo della tastiera quando viene fatto clic sul pulsante:

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

Acquisisci e rilascia lo stato attivo

Puoi concentrarti su come fornire agli utenti i dati corretti di cui la tua app ha bisogno per eseguire le sue attività, ad esempio ottenere un indirizzo email o un numero di telefono valido. Sebbene gli stati di errore indichino agli utenti cosa sta succedendo, potresti aver bisogno del campo con informazioni errate per rimanere attivo finché non verrà risolto.

Per acquisire l'elemento attivo, puoi richiamare il metodo captureFocus() e rilasciarlo in seguito con il metodo freeFocus(), come nell'esempio seguente:

val textField = FocusRequester()

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

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

Precedenza dei modificatori di messa a fuoco

Modifiers può essere visto come elementi che hanno un solo elemento secondario, quindi, quando li aggiungi alla coda, ogni Modifier a sinistra (o in alto) aggrega il Modifier che segue a destra (o sotto). Ciò significa che il secondo Modifier è contenuto all'interno del primo, in modo che, quando dichiari due focusProperties, funziona solo quello più in alto, dato che i seguenti sono contenuti all'interno della riga più alta.

Per chiarire ulteriormente il concetto, consulta il seguente codice:

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

In questo caso, non verrà utilizzato l'elemento focusProperties che indica che l'elemento attivo è item2 corretto, poiché è contenuto in quello precedente; pertanto, item1 sarà l'elemento utilizzato.

Sfruttando questo approccio, un genitore può anche reimpostare il comportamento predefinito utilizzando FocusRequester.Default:

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

L'elemento principale non deve necessariamente far parte della stessa catena di modificatori. Un componibile padre può sovrascrivere una proprietà dello stato attivo di un componibile secondario. Ad esempio, considera questo FancyButton che rende il pulsante non attivabile:

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

Un utente può rendere di nuovo attivabile questo pulsante impostando canFocus su true:

FancyButton(Modifier.focusProperties { canFocus = true })

Come ogni Modifier, quelli relativi all'elemento attivo si comportano in modo diverso a seconda dell'ordine dichiarato. Ad esempio, un codice come il seguente rende attivabile Box, ma FocusRequester non è associato all'elemento attivabile perché viene dichiarato dopo l'elemento attivabile.

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

È importante ricordare che un elemento focusRequester è associato al primo elemento attivabile al di sotto della gerarchia, quindi focusRequester rimanda al primo elemento secondario attivabile. Se non è disponibile nessuna informazione, non indicherà nulla. Tuttavia, poiché Box è attivabile (grazie al modificatore focusable()), puoi accedervi utilizzando la navigazione bidirezionale.

Per fare un altro esempio, puoi ottenere entrambe le azioni seguenti, in quanto il modificatore onFocusChanged() fa riferimento al primo elemento attivabile che compare dopo i modificatori focusable() o focusTarget().

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

Stato attivo del reindirizzamento all'entrata o all'uscita

A volte, è necessario fornire un tipo di navigazione molto specifico, come quello mostrato nell'animazione di seguito:

Animazione di una schermata che mostra due colonne di pulsanti affiancati e che animano lo stato attivo da una colonna all'altra.
Figura 3. Animazione di una schermata che mostra due colonne di pulsanti affiancati e che anima l'elemento attivo da una colonna all'altra

Prima di vedere come crearlo, è importante capire il comportamento predefinito della ricerca focus. Senza alcuna modifica, una volta che la ricerca con stato attivo raggiunge l'elemento Clickable 3, premendo DOWN sul D-Pad (o sul tasto freccia equivalente) lo stato attivo si sposta su qualsiasi elemento visualizzato sotto Column, uscindo dal gruppo e ignorando quello a destra. Se non sono disponibili elementi attivabili, lo stato attivo non si sposta da nessuna parte, ma rimane su Clickable 3.

Per modificare questo comportamento e fornire la navigazione prevista, puoi utilizzare il modificatore focusProperties, che ti aiuta a gestire cosa succede quando la ricerca attiva entra o esce da Composable:

val otherComposable = remember { FocusRequester() }

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

Puoi indirizzare l'elemento attivo a un elemento Composable specifico ogni volta che entra o esce da una determinata parte della gerarchia, ad esempio quando la tua UI ha due colonne e vuoi assicurarti che, ogni volta che viene elaborata la prima, passa alla seconda:

Animazione di una schermata che mostra due colonne di pulsanti affiancati e che animano lo stato attivo da una colonna all'altra.
Figura 4. Animazione di una schermata che mostra due colonne di pulsanti affiancati e che anima l'elemento attivo da una colonna all'altra

In questa GIF, quando lo stato attivo raggiunge Clickable 3 Composable in Column 1, l'elemento attivo successivo è Clickable 4 in un altro Column. Questo comportamento può essere ottenuto combinando i valori focusDirection con i valori enter e exit all'interno del modificatore focusProperties. Entrambi hanno bisogno di un lambda che prenda come parametro la direzione da cui proviene l'elemento attivo e restituisca un FocusRequester. Questa funzione lambda può comportarsi in tre modi diversi: la restituzione di FocusRequester.Cancel interrompe l'attenzione per continuare, mentre FocusRequester.Default non ne modifica il comportamento. Se fornisci invece l'oggetto FocusRequester associato a un altro Composable, l'elemento attivo si sposta su quel determinato Composable.

Cambia la direzione di avanzamento della messa a fuoco

Per avanzare l'elemento attivo sull'elemento successivo o in una direzione precisa, puoi usare il modificatore onPreviewKey e suggerire LocalFocusManager per farlo avanzare con il modificatore moveFocus.

L'esempio seguente mostra il comportamento predefinito del meccanismo di messa a fuoco: quando viene rilevata una pressione di un tasto tab, lo stato attivo passa all'elemento successivo nell'elenco. Anche se di solito non è un aspetto che solitamente devi configurare, è importante conoscere i meccanismi interni del sistema per poter modificare il comportamento predefinito.

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 questo esempio, la funzione focusManager.moveFocus() sposta lo stato attivo sull'elemento specificato o sulla direzione implicata nel parametro della funzione.