Gérer le curseur du clavier dans Compose

1. Introduction

Les utilisateurs peuvent interagir avec votre application à l'aide d'un clavier physique, généralement sur les appareils à grand écran comme les tablettes et les appareils ChromeOS, mais aussi sur les appareils XR. Il est important que les utilisateurs puissent naviguer dans votre application aussi efficacement avec un clavier physique qu'avec un écran tactile. De plus, lorsque vous concevez votre application pour les écrans de télévision et de voiture, qui ne disposent pas forcément d'une entrée tactile et qui reposent plutôt sur des pavés directionnels ou des encodeurs rotatifs, vous devez appliquer des principes de navigation au clavier similaires.

Compose vous permet de gérer les entrées provenant de claviers physiques, de pavés directionnels et de codeurs rotatifs de manière unifiée. Un principe clé d'une bonne expérience utilisateur pour ces modes de saisie est que les utilisateurs peuvent déplacer de manière intuitive et cohérente le curseur du clavier vers le composant interactif avec lequel ils souhaitent interagir.

Dans cet atelier de programmation, vous allez apprendre les points suivants :

  • Comment implémenter des modèles de gestion du curseur courants pour une navigation intuitive et cohérente
  • Comment vérifier si le déplacement du curseur du clavier se comporte comme prévu

Prérequis

  • Expérience dan la création d'applications avec Compose.
  • Connaissances de base de Kotlin, y compris des lambdas et des coroutines

Objectif de l'atelier

Vous implémentez les modèles de gestion du curseur du clavier suivants :

  • Déplacement du curseur du clavier : du début à la fin, de haut en bas, en forme de Z
  • Focus initial logique : sélectionnez l'élément d'interface utilisateur avec lequel l'utilisateur est susceptible d'interagir
  • Restauration du curseur : déplacez le curseur sur l'élément d'interface utilisateur avec lequel l'utilisateur a interagi précédemment

Points abordés

  • Principes de base de la gestion du curseur dans Compose
  • Comment créer un élément d'interface utilisateur en tant que cible de curseur
  • Comment demander à ce que le curseur déplace un élément d'interface utilisateur
  • Comment déplacer le curseur du clavier vers un élément d'interface utilisateur spécifique dans un groupe d'éléments d'interface utilisateur

Ce dont vous avez besoin

  • Android Studio Ladybug ou version ultérieure
  • L'un des appareils suivants pour exécuter l'application exemple :
  • Un appareil à grand écran doté d'un clavier physique
  • Un appareil virtuel Android pour les appareils à grand écran, tel que l'émulateur redimensionnable

2. Configuration

  1. Clonez le dépôt GitHub contenant les ateliers de programmation propres aux grands écrans :
git clone https://github.com/android/large-screen-codelabs

Vous pouvez également télécharger et désarchiver le fichier ZIP "large-screen-codelabs" :

  1. Accédez au dossier focus-management-in-compose :
  2. Dans Android Studio, ouvrez le projet. Le dossier focus-management-in-compose contient un projet.
  3. Si vous ne disposez pas d'une tablette Android, d'un appareil pliable ou d'un appareil ChromeOS équipé d'un clavier physique, ouvrez le Device Manager (Gestionnaire d'appareils) dans Android Studio, puis créez l'appareil Resizable (Redimensionnable) dans la catégorie Phone (Téléphone).

Le Gestionnaire d'appareils d'Android Studio affiche la liste des appareils virtuels disponibles dans la catégorie "Téléphone". L'émulateur redimensionnable fait partie de cette catégorie.Figure 1. Configurer l'émulateur redimensionnable dans Android Studio

3. Explorer le code de démarrage

Le projet comporte deux modules :

  • start : contient le code de démarrage du projet. Vous allez modifier ce code pour terminer l'atelier de programmation.
  • solution : contient le code final pour cet atelier de programmation.

L'application exemple comporte trois onglets :

  • Focus target (Cible du curseur)
  • Focus traversal order (Ordre de balayage du curseur)
  • Focus group (Groupe de sélection)

L'onglet de la cible du curseur s'affiche lorsque l'application est lancée.

Première vue de l'application exemple. Elle comporte trois onglets, et le premier est sélectionné. L'onglet affiche trois cartes placées dans une colonne.

Figure 2. L'onglet Cible du curseur s'affiche au lancement de l'application.

Le package ui contient le code d'interface utilisateur suivant avec lequel vous interagissez :

4. Focus target (Cible du curseur)

Une cible de curseur est un élément d'interface utilisateur sur lequel le curseur peut se déplacer à l'aide du clavier. Les utilisateurs peuvent déplacer le curseur du clavier à l'aide de la touche Tab ou des touches directionnelles (fléchées) :

  • Touche Tab : le curseur se déplace vers la cible suivante ou précédente dans une seule dimension.
  • Touches directionnelles : le curseur peut se déplacer dans deux dimensions : vers le haut, le bas, la gauche et la droite.

Les onglets sont des cibles de curseur. Dans l'application exemple, l'arrière-plan des onglets est mis à jour visuellement lorsque l'onglet est sélectionné.

Le fichier d'animation GIF montre comment le curseur du clavier se déplace dans les éléments de l'interface utilisateur. Il se déplace sur les trois onglets, puis la première carte est sélectionnée.

Figure 3. L'arrière-plan du composant change lorsque le curseur se déplace vers une cible.

Les éléments d'interface utilisateur interactifs sont des cibles du curseur par défaut

Un composant interactif est une cible par défaut. En d'autres termes, l'élément d'interface utilisateur est une cible de curseur si les utilisateurs peuvent appuyer dessus.

L'application exemple comporte trois cartes dans l'onglet Focus target (Cible du curseur). Les 1er et 3e éléments sont des cibles de curseur. Le 2e élément ne l'est pas. L'arrière-plan de la 3e carte est mis à jour lorsque l'utilisateur déplace le curseur depuis la 1re carte avec la touche Tab.

L'animation GIF montre le mouvement initial du curseur du clavier dans l'onglet "Focus target" (Cible du curseur). Il ignore la deuxième carte et passe à la troisième à partir de la première lorsque l'utilisateur appuie sur la touche Tabulation sur la première carte.

Figure 4. Les cibles du curseur de l'application excluent la 2e carte.

Modifier la deuxième carte pour qu'elle soit une cible du curseur

Vous pouvez définir la 2e carte comme cible du curseur en la transformant en élément d'interface utilisateur interactif. Le moyen le plus simple consiste à utiliser le modificateur clickable comme suit :

  1. Ouvrez FocusTargetTab.kt dans le package tabs.
  2. Modifiez le composable SecondCard avec le modificateur clickable comme suit :
@Composable
fun FocusTargetTab(
    onClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    Column(
        verticalArrangement = Arrangement.spacedBy(16.dp),
        modifier = modifier
    ) {
        FirstCard(
            onClick = onClick,
            modifier = Modifier.width(240.dp)
        )
        SecondCard(
            modifier = Modifier
                .width(240.dp)
                .clickable(onClick = onClick)
        )
        ThirdCard(
            onClick = onClick,
            modifier = Modifier.width(240.dp)
        )
    }
}

Exécuter l'application

L'utilisateur peut désormais sélectionner la 2e carte en plus de la 1re carte et de la 3e carte. Vous pouvez essayer sur l'onglet Focus target (Cible du curseur). Vérifiez que vous pouvez déplacer le curseur de la 1re carte vers la 2e carte à l'aide de la touche Tab.

L'animation GIF montre le déplacement du curseur du clavier après la modification. Il quitte la première fiche lorsque l'utilisateur appuie sur la touche de tabulation.

Figure 5. Déplacement du curseur de la 1re carte vers la 2e carte à l'aide de la touche Tab

5. Parcours du curseur dans un schéma en forme de Z

Les utilisateurs s'attendent à ce que le curseur du clavier se déplace de gauche à droite et de haut en bas si les paramètres linguistiques sont configurés pour un système d'écriture de gauche à droite. Cet ordre de balayage est appelé schéma en forme de Z.

Toutefois, Compose ignore la mise en page lorsqu'il détermine la prochaine cible du curseur de la touche Tab et utilise à la place un balayage unidimensionnel basé sur l'ordre des appels de fonction composable.

Balayage du curseur unidimensionnel

L'ordre de balayage du curseur unidimensionnel provient de l'ordre des appels de fonction composable plutôt que de la mise en page de l'application.

Dans l'application exemple, le curseur se déplace dans l'ordre suivant dans l'onglet Focus traversal order (Ordre de balayage du curseur) :

  1. 1re carte
  2. 4e carte
  3. 3e carte
  4. 2e carte

L'animation GIF montre que le curseur du clavier se déplace différemment par rapport aux attentes de l'utilisateur.  Il passe de la 1re à la 3e carte, puis à la 4e et à la 2e. Cela peut aller à l'encontre des attentes de l'utilisateur.

Figure 6. Le balayage du curseur suit l'ordre des fonctions composables.

La fonction FocusTraversalOrderTab implémente l'Focus traversal order (Ordre de balayage du curseur) de l'application exemple. La fonction appelle les fonctions conposables pour les cartes : FirstCard, FourthCard, ThirdCard et SecondCard, dans cet ordre.

@Composable
fun FocusTraversalOrderTab(
    modifier: Modifier = Modifier
) {
    Row(
        horizontalArrangement = Arrangement.spacedBy(16.dp),
        modifier = modifier
    ) {
        Column(
            verticalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            FirstCard(
                onClick = onClick,
                modifier = Modifier.width(240.dp)
            )
            FourthCard(
                onClick = onClick,
                modifier = Modifier
                    .width(240.dp)
                    .offset(x = 256.dp)
            )
            ThirdCard(
                onClick = onClick,
                modifier = Modifier
                    .width(240.dp)
                    .offset(y = (-151).dp)
            )
        }
        SecondCard(
            modifier = Modifier.width(240.dp)
        )
    }
}

Mouvement du curseur dans le schéma en forme de Z

Vous pouvez intégrer le mouvement du curseur en forme de Z dans l'onglet Focus traversal order (Ordre de balayage du curseur) de l'application exemple en procédant comme suit :

  1. Ouvrez tabs.FocusTraversalOrderTab.kt.
  2. Supprimez le modificateur de décalage des composables ThirdCard et FourthCard.
  3. Remplacez la disposition de l'onglet (une ligne avec deux colonnes) par une disposition à une colonne avec deux lignes.
  4. Déplacez les composables FirstCard et SecondCard vers la première ligne.
  5. Déplacez les composables ThirdCard et FourthCard vers la deuxième ligne.

Le code modifié est le suivant :

@Composable
fun FocusTraversalOrderTab(
    onClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    Column(
        verticalArrangement = Arrangement.spacedBy(16.dp),
        modifier = modifier
    ) {
        Row(
            horizontalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            FirstCard(
                onClick = onClick,
                modifier = Modifier.width(240.dp),
            )
            SecondCard(
                onClick = onClick,
                modifier = Modifier.width(240.dp)
            )
        }
        Row(
            horizontalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            ThirdCard(
                onClick = onClick,
                modifier = Modifier.width(240.dp)
            )
            FourthCard(
                onClick = onClick,
                modifier = Modifier.width(240.dp)
            )
        }
    }
}

Exécuter l'application

L'utilisateur peut désormais déplacer le curseur de droite à gauche et de haut en bas, selon le schéma en forme de Z. Vous pouvez essayer dans l'onglet Focus traversal order (Ordre de balayage du curseur) et vérifier que le curseur se déplace dans l'ordre suivant à l'aide de la touche Tab :

  1. 1re carte
  2. 2e carte
  3. 3e carte
  4. 4e carte

L'animation GIF montre comment le curseur du clavier se déplace après la modification. Il se déplace de gauche à droite, de haut en bas, en suivant une forme de Z.

Figure 7. Balayage du curseur en forme de Z.

6. focusGroup

Le curseur passe de la 1re carte à la 3e carte à l'aide de la touche directionnelle right de l'onglet Focus group (Groupe de sélection). Ce mouvement est probablement un peu déroutant pour les utilisateurs, car les deux cartes ne sont pas côte à côte.

L'animation GIF montre que le curseur du clavier passe de la première carte à la troisième à l'aide de la touche de direction droite. Ces deux cartes sont placées sur des lignes différentes.

Figure 8. Déplacement inattendu du curseur de la 1re carte à la 3e.

Le balayage du curseur bidimensionnel fait référence aux informations de mise en page

Appuyer sur une touche de direction déclenche un balayage bidimensionnel. Il s'agit d'un déplacement de curseur courant sur les téléviseurs, car les utilisateurs interagissent avec votre application à l'aide d'un pavé directionnel. Appuyer sur les touches fléchées du clavier déclenche également un balayage à deux dimensions, car elles imitent la navigation avec un pavé directionnel.

Dans le balayage du curseur bidimensionnel, le système fait référence aux informations géométriques des éléments de l'interface utilisateur et détermine la cible du curseur pour le déplacement. Par exemple, le curseur se déplace vers la 1re carte à partir de l'onglet de la cible du curseur avec la touche directionnelle down. Si vous appuyez sur la touche directionnelle vers le haut, le curseur se déplace vers l'onglet Focus target (Cible de curseur)

Le GIF montre que le curseur passe à la première carte à partir de l'onglet "Focus target" (cible du curseur) avec la touche vers le bas, puis revient à l'onglet avec la touche vers le haut. Ces deux cibles du curseur sont les plus proches verticalement.

Figure 9. Balayage du curseur avec les touches de direction vers le bas et vers le haut.

Le balayage du curseur bidimensionnel ne se termine pas, contrairement au déplacement du curseur unidimensionnel avec la touche Tab. Par exemple, l'utilisateur ne peut pas déplacer le curseur avec la touche vers le bas lorsque la 2e carte est sélectionnée.

Le GIF montre que le curseur reste sur la deuxième carte, même si l'utilisateur appuie sur la touche de direction vers le bas, car aucune cible n'est placée sous la carte.

Figure 10. La touche directionnelle vers le bas ne peut pas déplacer le curseur lorsque la 2e fiche est sélectionnée.

Cibles du curseur au même niveau

Le code suivant implémente l'écran mentionné ci-dessus. Il existe quatre cibles de sélection : FirstCard, SecondCard, ThirdCard et FourthCard. Ces quatre cibles de sélection sont au même niveau, et ThirdCard est le premier élément à droite de FirstCard dans la mise en page. C'est pourquoi le curseur passe de la 1re carte à la 3e carte avec la touche directionnelle right.

@Composable
fun FocusGroupTab(
    onClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    Column(
        verticalArrangement = Arrangement.spacedBy(16.dp),
        modifier = modifier,
    ) {
        FirstCard(
            onClick = onClick,
            modifier = Modifier.width(208.dp)
        )
        Row(
            horizontalArrangement = Arrangement.spacedBy(16.dp),
        ) {
            SecondCard(
                onClick = onClick,
                modifier = Modifier.width(208.dp)
            )
            ThirdCard(
                onClick = onClick,
                modifier = Modifier.width(208.dp)
            )
            FourthCard(
                onClick = onClick,
                modifier = Modifier.width(208.dp)
            )
        }
    }
}

Grouper les cibles du curseur avec le modificateur focusGroup

Vous pouvez modifier le mouvement déroutant du curseur en procédant comme suit :

  1. Ouvrez tabs.FocusGroup.kt.
  2. Modifiez la fonction composable Column dans la fonction composable FocusGroupTab avec le modificateur focusGroup.

Le code mis à jour est le suivant :

@Composable
fun FocusGroupTab(
    onClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    Column(
        verticalArrangement = Arrangement.spacedBy(16.dp),
        modifier = modifier,
    ) {
        FirstCard(
            onClick = onClick,
            modifier = Modifier.width(208.dp)
        )
        Row(
            horizontalArrangement = Arrangement.spacedBy(16.dp),
            modifier = Modifier.focusGroup(),
        ) {
            SecondCard(
                onClick = onClick,
                modifier = Modifier.width(208.dp)
            )
            ThirdCard(
                onClick = onClick,
                modifier = Modifier.width(208.dp)
            )
            FourthCard(
                onClick = onClick,
                modifier = Modifier.width(208.dp)
            )
        }
    }
}

Le modificateur focusGroup crée un groupe de sélection composé des cibles du curseur dans le composant modifié. Les cibles du curseur dans le groupe de sélection et celles en dehors de ce groupe sont à des niveaux différents. Aucune cible du curseur n'est placée à droite du composable FirstCard. Par conséquent, le curseur ne se déplace vers aucune carte de la 1re carte avec la touche directionnelle right.

Exécuter l'application

Désormais, le curseur ne passe pas de la 1re carte à la 3e carte avec la touche directionnelle right dans l'onglet Focus group (Groupe de sélection) de l'application exemple.

7. Demander la sélection

Les utilisateurs ne peuvent pas utiliser de clavier ni de pavé directionnel pour sélectionner des éléments d'interface utilisateur arbitraires avec lesquels interagir. Les utilisateurs doivent déplacer le curseur du clavier vers un composant interactif avant d'interagir avec l'élément.

Par exemple, les utilisateurs doivent déplacer le curseur de l'onglet Focus target (Cible du curseur) vers la 1re carte avant d'interagir avec celle-ci. Vous pouvez réduire le nombre d'actions à effectuer pour lancer la tâche principale de l'utilisateur en définissant logiquement le focus initial.

L'animation GIF montre que l'utilisateur doit appuyer trois fois sur la touche de tabulation après avoir sélectionné l'onglet pour déplacer le curseur du clavier vers la première carte de l'onglet.

Figure 11. Trois appuis sur la touche Tab permettent de sélectionner la 1re carte.

Demander la sélection avec FocusRequester

Vous pouvez demander à ce que le curseur déplace un élément d'interface utilisateur avec FocusRequester. Un objet FocusRequester doit être associé à un élément d'interface utilisateur avant d'appeler la méthode requestFocus().

Définir le focus initial sur la première carte

Vous pouvez définir le focus initial sur la 1re carte en procédant comme suit :

  1. Ouvrez tabs.FocusTarget.kt.
  2. Déclarez la valeur firstCard dans la fonction composable FocusTargetTab et initialisez-la avec un objet FocusRequester renvoyé par la fonction remember.
  3. Modifiez la fonction composable FirstCard avec le modificateur focusRequester.
  4. Spécifiez la valeur firstCard comme argument du modificateur focusRequester.
  5. Appelez la fonction composable LaunchedEffect avec la valeur Unit, puis appelez la méthode requestFocus() sur la valeur firstCard dans le lambda transmis à la fonction composable LaunchedEffect.

Un objet FocusRequester est créé et associé à un élément d'interface utilisateur dans les deuxième et troisième étapes. À la cinquième étape, il est demandé au curseur de se déplacer sur l'élément d'interface utilisateur associé lorsque le composable FocusdTargetTab est composé pour la première fois.

Le code mis à jour se présente comme suit :

@Composable
fun FocusTargetTab(
    onClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    val firstCard = remember { FocusRequester() }

    Column(
        verticalArrangement = Arrangement.spacedBy(16.dp),
        modifier = modifier
    ) {
        FirstCard(
            onClick = onClick,
            modifier = Modifier
                .width(240.dp)
                .focusRequester(focusRequester = firstCard)
        )
        SecondCard(
            modifier = Modifier
                .width(240.dp)
                .clickable(onClick = onClick)
        )
        ThirdCard(
            onClick = onClick,
            modifier = Modifier.width(240.dp)
        )
    }

    LaunchedEffect(Unit) {
        firstCard.requestFocus()
    }
}

Exécuter l'application

Désormais, le curseur du clavier se place sur la 1re carte de l'onglet Focus target (Cible du curseur) lorsque l'onglet est sélectionné. Vous pouvez essayer en changeant d'onglet. De plus, la 1re carte est sélectionnée au lancement de l'application.

L'animation GIF montre que le curseur du clavier se déplace automatiquement vers la première carte lorsque l'utilisateur sélectionne l'onglet "Focus target" (Cible du curseur).

Figure 12. Le curseur est placé sur la 1re carte lorsque l'onglet Focus target (Cible du curseur) est sélectionné.

8. Placer le curseur sur l'onglet sélectionné

Vous pouvez spécifier la cible du curseur lorsque le curseur du clavier entre dans un groupe de sélection. Par exemple, vous pouvez déplacer le curseur sur l'onglet sélectionné lorsque l'utilisateur déplace le curseur sur la ligne d'onglets.

Pour implémenter ce comportement, procédez comme suit :

  1. Ouvrez App.kt.
  2. Déclarez la valeur focusRequesters dans la fonction composable App.
  3. Initialisez la valeur focusRequesters avec la valeur renvoyée par la fonction remember qui renvoie une liste d'objets FocusRequester. La longueur de la liste renvoyée doit être égale à celle de Screens.entries.
  4. Associez chaque objet FocusRequester de la valeur focusRequester au composable Tab en modifiant le composable "Tab" avec le modificateur focusRequester.
  5. Modifiez le composable "PrimaryTabRow" avec le modificateur focusProperties et le modificateur focusGroup.
  6. Transmettez un lambda au modificateur focusProperties et associez la propriété enter à un autre lambda.
  7. Renvoyez le "FocusRequester", qui est indexé avec la valeur selectedTabIndex dans la valeur focusRequesters, à partir du lambda associé à la propriété enter.

Le code modifié se présente comme suit :

@Composable
fun App(
    modifier: Modifier = Modifier,
) {
    val context = LocalContext.current

    var selectedScreen by rememberSaveable { mutableStateOf(Screen.FocusTarget) }
    val selectedTabIndex = Screen.entries.indexOf(selectedScreen)
    val focusRequesters = remember {
        List(Screen.entries.size) { FocusRequester() }
    }

    Column(modifier = modifier) {
        PrimaryTabRow(
            selectedTabIndex = selectedTabIndex,
            modifier = Modifier
                .focusProperties {
                    enter = {
                        focusRequesters[selectedTabIndex]
                    }
                }
                .focusGroup()
        ) {
            Screen.entries.forEachIndexed { index, screen ->
                Tab(
                    selected = screen == selectedScreen,
                    onClick = { selectedScreen = screen },
                    text = { Text(stringResource(screen.title)) },
                    modifier = Modifier.focusRequester(focusRequester = focusRequesters[index])
                )
            }
        }
        when (selectedScreen) {
            Screen.FocusTarget -> {
                FocusTargetTab(
                    onClick = context::onCardClicked,
                    modifier = Modifier.padding(32.dp),
                )
            }

            Screen.FocusTraversalOrder -> {
                FocusTraversalOrderTab(
                    onClick = context::onCardClicked,
                    modifier = Modifier.padding(32.dp)
                )
            }

            Screen.FocusRestoration -> {
                FocusGroupTab(
                    onClick = context::onCardClicked,
                    modifier = Modifier.padding(32.dp)
                )
            }
        }
    }
}

Vous pouvez contrôler le mouvement du curseur avec le modificateur focusProperties. Dans le lambda transmis au modificateur, modifiez "FocusProperties", qui est référencé lorsque le système choisit la cible du curseur lorsque les utilisateurs appuient sur la touche Tab ou sur les touches directionnelles lorsque l'élément d'interface utilisateur modifié est sélectionné.

Lorsque vous définissez la propriété enter, le système évalue le lambda défini sur la propriété et accède à l'élément d'interface utilisateur associé à l'objet FocusRequester renvoyé par le lambda évalué.

Exécuter l'application

Désormais, le curseur du clavier se déplace vers l'onglet sélectionné lorsque l'utilisateur déplace le curseur vers la ligne d'onglets. Pour ce faire, procédez comme suit :

  1. Exécuter l'application
  2. Sélectionnez l'onglet Focus group (Groupe de sélection).
  3. Placez le curseur sur la 1re carte à l'aide de la touche directionnelle down.
  4. Déplacez le curseur à l'aide de la touche directionnelle up.

Figure 13. Le curseur se déplace vers l'onglet sélectionné.

9. Restauration du curseur

Les utilisateurs s'attendent à pouvoir reprendre facilement une tâche lorsqu'elle est interrompue. La restauration du curseur permet de reprendre après une interruption. La restauration du curseur déplace le curseur du clavier vers l'élément d'interface utilisateur précédemment sélectionné.

L'écran d'accueil des applications de streaming vidéo est un cas d'utilisation typique de la restauration du curseur. L'écran affiche plusieurs listes de contenus vidéo, comme des films d'une catégorie ou des épisodes d'une série TV. Les utilisateurs parcourent les listes et trouvent des contenus intéressants. Parfois, les utilisateurs reviennent à la liste précédemment examinée et continuent de la parcourir. Grâce à la restauration du curseur, les utilisateurs peuvent continuer à naviguer sans que le curseur ne revienne sur le dernier élément de la liste.

Le modificateur "focusRestorer" restaure le focus sur un groupe de sélection

Utilisez le modificateur focusRestorer pour enregistrer et restaurer le curseur d'un groupe de sélection. Lorsque le curseur quitte le groupe de sélection, il stocke une référence à l'élément précédemment sélectionné. Lorsque le curseur revient dans le groupe de sélection, il est placé sur l'élément précédemment sélectionné.

Intégrer la restauration du curseur avec l'onglet "Focus group" (Groupe de sélection)

L'onglet Focus group (Groupe de sélection) de l'application exemple comporte une ligne contenant les éléments 2nd card (2e carte), 3rd card (3e carte) et 4th card (4e carte).

L'animation GIF montre que le curseur du clavier passe de la première carte à la deuxième, même si la troisième carte était sélectionnée auparavant.

Figure 14. Groupe de sélection contenant la 2e carte, la 3e carte et la 4e carte.

Vous pouvez intégrer la restauration du curseur dans la ligne en procédant comme suit :

  1. Ouvrez tab.FocusGroupTab.kt.
  2. Modifiez le composable Row dans le composable FocusGroupTab avec le modificateur focusRestorer. Le modificateur doit être appelé avant le modificateur focusGroup.

Le code modifié se présente comme suit :

@Composable
fun FocusGroupTab(
    onClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    Column(
        verticalArrangement = Arrangement.spacedBy(16.dp),
        modifier = modifier,
    ) {
        FirstCard(
            onClick = onClick,
            modifier = Modifier.width(208.dp)
        )
        Row(
            horizontalArrangement = Arrangement.spacedBy(16.dp),
            modifier = Modifier
                .focusRestorer()
                .focusGroup(),
        ) {
            SecondCard(
                onClick = onClick,
                modifier = Modifier.width(208.dp)
            )
            ThirdCard(
                onClick = onClick,
                modifier = Modifier.width(208.dp)
            )
            FourthCard(
                onClick = onClick,
                modifier = Modifier.width(208.dp)
            )
        }
    }
}

Exécuter l'application

La ligne de l'onglet Focus group (Groupe de sélection) est à nouveau sélectionnée. Vous pouvez essayer de la sélectionner en procédant comme suit :

  1. Sélectionnez l'onglet Focus group (Groupe de sélection).
  2. Déplacez le curseur sur la 1re carte.
  3. Placez le curseur sur la 4e carte à l'aide de la touche Tab.
  4. Déplacez le curseur sur la 1re carte avec la touche directionnelle up.
  5. Appuyez sur la touche Tab.

Le curseur du clavier se déplace vers la 4e carte, car le modificateur focusRestorer enregistre la référence de la carte et rétablit la sélection lorsque le curseur du clavier entre dans le groupe de sélection défini sur la ligne.

L'animation GIF montre que le curseur du clavier se déplace vers la carte précédemment sélectionnée dans une ligne lorsque le curseur du clavier y est à nouveau placé.

Figure 15. Le curseur revient sur la 4e carte après avoir appuyé sur la flèche vers le haut, puis sur Tab.

10. Écrire un test

Vous pouvez tester la gestion du curseur du clavier implémentée avec des tests. Compose fournit une API permettant de vérifier si un élément d'interface utilisateur est sélectionné et d'effectuer des appuis sur les touches des composants de l'interface utilisateur. Pour en savoir plus, consultez l'atelier de programmation Tester dans Jetpack Compose.

Tester l'onglet "Focus target" (Cible de sélection)

Dans la section précédente, vous avez modifié la fonction composable FocusTargetTab pour définir la deuxième carte comme cible du curseur. Créez un test pour l'implémentation que vous avez effectuée manuellement dans la section précédente. Le test peut être écrit en suivant les étapes ci-dessous :

  1. Ouvrez FocusTargetTabTest.kt. Vous allez modifier la fonction testSecondCardIsFocusTarget dans les étapes suivantes.
  2. Demandez à ce que le curseur se déplace vers la 1re carte en appelant la méthode requestFocus sur l'objet SemanticsNodeInteraction pour la 1re carte.
  3. Assurez-vous que la 1re carte est sélectionnée avec la méthode assertIsFocused().
  4. Appuyez sur la touche Tab en appelant la méthode pressKey avec la valeur Key.Tab dans le lambda transmis à la méthode performKeyInput.
  5. Vérifiez si le curseur du clavier se déplace vers la 2e carte en appelant la méthode assertIsFocused() sur l'objet SemanticsNodeInteraction pour la 2e carte.

Le code mis à jour se présente comme suit :

@OptIn(ExperimentalTestApi::class, ExperimentalComposeUiApi::class)
@Test
fun testSecondCardIsFocusTarget() {
    composeTestRule.setContent {
        LocalInputModeManager
            .current
            .requestInputMode(InputMode.Keyboard)
        FocusTargetTab(onClick = {})
    }
    val context = InstrumentationRegistry.getInstrumentation().targetContext

    // Ensure the 1st card is focused
    composeTestRule
        .onNodeWithText(context.getString(R.string.first_card))
        .requestFocus()
        .performKeyInput { pressKey(Key.Tab) }

    // Test if focus moves to the 2nd card from the 1st card with Tab key
    composeTestRule
        .onNodeWithText(context.getString(R.string.second_card))
        .assertIsFocused()
}

Exécuter l'application

Vous pouvez exécuter le test en cliquant sur l'icône triangulaire affichée à gauche de la déclaration de classe FocusTargetTest. Pour en savoir plus, consultez la section Exécuter des tests de Tester dans Android Studio.

Android Studio affiche un menu contextuel pour exécuter "FocusTargetTabTest".

11. Félicitations

Bravo ! Vous avez appris les principes de base de la gestion du curseur du clavier:

  • Focus target (Cible du curseur)
  • Balayage du curseur

Vous pouvez contrôler l'ordre de balayage du curseur à l'aide des modificateurs Compose suivants :

  • Le modificateur focusGroup
  • Le modificateur focusProperties

Vous avez implémenté le modèle typique de l'expérience utilisateur avec un clavier physique, le focus initial et la restauration du curseur. Ces modèles sont implémentés en combinant les API suivantes :

  • Classe FocusRequester
  • Le modificateur focusRequester
  • Le modificateur focusRestorer
  • Fonction composable LaunchedEffect

L'expérience utilisateur implémentée peut être testée avec des tests d'instrumentation. Compose permet d'effectuer des pressions sur les touches et de vérifier si un SemanticsNode est sélectionné au clavier ou non.

En savoir plus