Modifier le comportement de mise au point

Il est parfois nécessaire d'ignorer le comportement de sélection par défaut des éléments de votre écran. Par exemple, vous pouvez regrouper des composables, empêcher la sélection d'un certain composable, demander explicitement le ciblage sur un composable donné, capturer ou libérer le focus, ou rediriger le curseur lors de l'entrée ou de la sortie. Cette section explique comment modifier le comportement de sélection lorsque les valeurs par défaut ne vous conviennent pas.

Proposez une navigation cohérente avec les groupes d'étude

Parfois, Jetpack Compose ne devine pas immédiatement l'élément suivant correct pour la navigation par onglets, en particulier lorsque des Composables parents complexes tels que des onglets et des listes entrent en jeu.

Bien que la recherche ciblée suit généralement l'ordre de déclaration de Composables, cela est impossible dans certains cas, par exemple lorsque l'un des Composables de la hiérarchie est un élément à défilement horizontal qui n'est pas entièrement visible. Ce processus est illustré dans l'exemple ci-dessous.

Jetpack Compose peut décider de placer le curseur sur l'élément suivant le plus proche du début de l'écran, comme indiqué ci-dessous, plutôt que de suivre le chemin attendu pour la navigation dans un sens:

Animation d'une application montrant une navigation horizontale supérieure et une liste d'éléments en dessous.
Figure 1. Animation d'une application montrant une navigation horizontale supérieure et une liste d'éléments en dessous

Dans cet exemple, il est clair que les développeurs n'avaient pas l'intention de passer de l'onglet Chocolats à la première image ci-dessous, puis de revenir à l'onglet Pastries. Au lieu de cela, il souhaitait se concentrer sur les onglets jusqu'au dernier, puis sur le contenu interne:

Animation d'une application montrant une navigation horizontale supérieure et une liste d'éléments en dessous.
Figure 2. Animation d'une application montrant une navigation horizontale supérieure et une liste d'éléments en dessous

Dans les cas où il est important qu'un groupe de composables obtienne le focus de manière séquentielle, comme dans la ligne de tabulation de l'exemple précédent, vous devez encapsuler le Composable dans un parent disposant du modificateur focusGroup():

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

La navigation bidirectionnelle recherche le composable le plus proche dans la direction donnée. Si un élément d'un autre groupe est plus proche qu'un élément non entièrement visible du groupe actuel, la navigation sélectionne l'élément le plus proche. Pour éviter ce comportement, vous pouvez appliquer le modificateur focusGroup().

FocusGroup fait apparaître un groupe entier comme une seule entité en termes de ciblage, mais le groupe lui-même ne reçoit pas le ciblage, mais l'enfant le plus proche obtient le focus. De cette manière, la navigation sait qu'il doit accéder à l'élément non entièrement visible avant de quitter le groupe.

Dans ce cas, les trois instances de FilterChip sont ciblées avant les éléments SweetsCard, même si les SweetsCards sont entièrement visibles par l'utilisateur et que certaines FilterChip peuvent être masquées. En effet, le modificateur focusGroup indique au gestionnaire de focus d'ajuster l'ordre dans lequel les éléments sont sélectionnés afin que la navigation soit plus facile et plus cohérente avec l'interface utilisateur.

Sans le modificateur focusGroup, si FilterChipC n'était pas visible, la navigation de sélection la sélectionne en dernier. Cependant, l'ajout d'un tel modificateur le rend non seulement visible, mais permet également d'acquérir l'attention des utilisateurs juste après FilterChipB, comme s'y attendent les utilisateurs.

Rendre un composable sélectionnable

Certains composables peuvent être sélectionnés par conception, tels qu'un bouton ou un composable auquel le modificateur clickable est associé. Si vous souhaitez ajouter spécifiquement un comportement sélectionnable à un composable, utilisez le modificateur focusable:

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

Rendre un composable non sélectionnable

Dans certains cas, certains de vos éléments ne devraient pas participer à la mise au point. Dans ces rares cas, vous pouvez utiliser canFocus property pour exclure une Composable de la sélection.

var checked by remember { mutableStateOf(false) }

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

Demander la sélection au clavier avec FocusRequester

Dans certains cas, vous pouvez demander explicitement la sélection en réponse à une interaction utilisateur. Par exemple, vous pouvez demander à un utilisateur s'il souhaite recommencer à remplir un formulaire. S'il appuie sur "Oui", vous souhaitez alors recentrer le premier champ de ce formulaire.

La première chose à faire est d'associer un objet FocusRequester au composable vers lequel vous souhaitez déplacer le focus du clavier. Dans l'extrait de code suivant, un objet FocusRequester est associé à un TextField en définissant un modificateur appelé Modifier.focusRequester:

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

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

Vous pouvez appeler la méthode requestFocus de FocusRequester pour envoyer des requêtes de sélection réelles. Vous devez appeler cette méthode en dehors d'un contexte Composable (dans le cas contraire, elle est réexécutée à chaque recomposition). L'extrait de code suivant montre comment demander au système de déplacer la sélection au clavier lorsque l'utilisateur clique sur le bouton:

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

Capturer et relâcher la mise au point

Vous pouvez tirer parti de la sélection pour guider vos utilisateurs afin qu'ils fournissent les données dont votre application a besoin pour effectuer sa tâche (par exemple, obtenir une adresse e-mail ou un numéro de téléphone valides). Bien que les états d'erreur informent vos utilisateurs de ce qui se passe, vous aurez peut-être besoin que le champ contenant des informations erronées reste sélectionné jusqu'à ce qu'il soit corrigé.

Pour capturer le focus, vous pouvez appeler la méthode captureFocus(), puis la libérer par la suite avec la méthode freeFocus(), comme dans l'exemple suivant:

val textField = FocusRequester()

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

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

Priorité des modificateurs de sélection

Les Modifiers peuvent être considérés comme des éléments qui n'ont qu'un seul enfant. Par conséquent, lorsque vous les mettez en file d'attente, chaque Modifier à gauche (ou en haut) encapsule l'élément Modifier qui suit à droite (ou en dessous). Cela signifie que la deuxième Modifier est contenue dans la première. Ainsi, lorsque vous déclarez deux focusProperties, seul le plus haut fonctionne, car les suivants sont contenus dans le plus haut.

Pour mieux comprendre le concept, consultez le code suivant:

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

Dans ce cas, le focusProperties indiquant item2 comme focus droit ne sera pas utilisé, car il est contenu dans le précédent. Par conséquent, item1 sera celui utilisé.

En utilisant cette approche, un parent peut également réinitialiser le comportement par défaut à l'aide de FocusRequester.Default:

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

Le parent ne doit pas nécessairement faire partie de la même chaîne de modificateurs. Un composable parent peut écraser une propriété de focus d'un composable enfant. Prenons l'exemple de ce FancyButton qui rend le bouton non sélectionnable:

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

Un utilisateur peut rendre ce bouton sélectionnable en définissant canFocus sur true:

FancyButton(Modifier.focusProperties { canFocus = true })

Comme toutes les Modifier, celles liées à la sélection se comportent différemment en fonction de l'ordre dans lequel vous les déclarez. Par exemple, le code suivant rend le Box sélectionnable, mais FocusRequester n'est pas associé à ce composant sélectionnable, car il est déclaré après celui-ci.

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

N'oubliez pas qu'un focusRequester est associé au premier élément sélectionnable en dessous dans la hiérarchie. Ainsi, focusRequester pointe vers le premier enfant sélectionnable. Si aucun d'eux n'est disponible, il ne pointe vers rien. Toutefois, comme Box peut être sélectionné (grâce au modificateur focusable()), vous pouvez y accéder en utilisant la navigation bidirectionnelle.

Un autre exemple peut également fonctionner, car le modificateur onFocusChanged() fait référence au premier élément sélectionnable qui apparaît après les modificateurs focusable() ou focusTarget().

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

Rediriger le curseur à l'entrée ou à la sortie

Parfois, vous devez fournir un type de navigation très spécifique, comme celui illustré dans l'animation ci-dessous:

Animation d'un écran montrant deux colonnes de boutons placés côte à côte et animant le focus d'une colonne à l'autre.
Figure 3. Animation d'un écran montrant deux colonnes de boutons placés côte à côte et animant le focus d'une colonne à l'autre

Avant de plonger dans le processus de création, il est important de comprendre le comportement par défaut de la recherche ciblée. Sans modification, une fois que la recherche ciblée atteint l'élément Clickable 3, appuyer sur DOWN sur le pavé directionnel (ou sur la touche fléchée équivalente) déplace le curseur sur l'élément affiché sous Column, quitte le groupe et ignore celui de droite. Si aucun élément sélectionnable n'est disponible, le focus ne se déplace pas, mais reste sur Clickable 3.

Pour modifier ce comportement et fournir la navigation prévue, vous pouvez utiliser le modificateur focusProperties, qui vous aide à gérer ce qui se passe lorsque la recherche ciblée entre dans le Composable ou la quitte:

val otherComposable = remember { FocusRequester() }

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

Il est possible de diriger le curseur vers un élément Composable spécifique chaque fois qu'il entre dans une certaine partie de la hiérarchie ou qu'il en sort (par exemple, lorsque votre UI comporte deux colonnes et que vous souhaitez vous assurer que, chaque fois que la première est traitée, le curseur passe à la seconde) :

Animation d'un écran montrant deux colonnes de boutons placés côte à côte et animant le focus d'une colonne à l'autre.
Figure 4. Animation d'un écran montrant deux colonnes de boutons placés côte à côte et animant le focus d'une colonne à l'autre

Dans cet GIF, une fois que le curseur atteint la Clickable 3 Composable dans Column 1, l'élément suivant sélectionné est Clickable 4 dans un autre Column. Ce comportement peut être obtenu en combinant les valeurs focusDirection avec les valeurs enter et exit dans le modificateur focusProperties. Ils ont tous deux besoin d'un lambda qui prend comme paramètre la direction de départ et renvoie un FocusRequester. Ce lambda peut se comporter de trois manières différentes: le renvoi de FocusRequester.Cancel empêche la sélection de poursuivre, tandis que FocusRequester.Default ne modifie pas son comportement. Si vous indiquez à la place le FocusRequester associé à un autre Composable, le curseur se déplace sur cette Composable spécifique.

Modifier la direction de la progression de la mise au point

Pour avancer le curseur sur l'élément suivant ou vers une direction précise, vous pouvez exploiter le modificateur onPreviewKey et impliquer le LocalFocusManager pour avancer le curseur avec le modificateur moveFocus.

L'exemple suivant montre le comportement par défaut du mécanisme de sélection: lorsqu'une touche tab est détectée, le curseur passe à l'élément suivant dans la liste de sélection. Bien que cette configuration ne soit généralement pas nécessaire, il est important de connaître le fonctionnement interne du système pour pouvoir modifier le comportement par défaut.

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

Dans cet exemple, la fonction focusManager.moveFocus() fait passer le curseur sur l'élément spécifié ou sur la direction implicite dans le paramètre de la fonction.