Comprendre les gestes

Plusieurs termes et concepts sont importants à comprendre lorsque vous travaillez sur la gestion des gestes dans une application. Cette page explique les conditions d'utilisation pointeurs, événements de pointeur et gestes, et présente les différentes abstractions différents pour les gestes. Elle approfondit également la consommation d'événements et la propagation.

Définitions

Pour comprendre les différents concepts de cette page, vous devez connaître certains de la terminologie utilisée:

  • Pointeur: objet physique qui vous permet d'interagir avec votre application. Sur les appareils mobiles, le pointeur le plus courant est l'écran tactile. Vous pouvez également utiliser un stylet pour remplacer votre doigt. Pour les grands écrans, vous pouvez utiliser une souris ou un pavé tactile pour interagir indirectement avec l'écran. Un périphérique d'entrée doit pouvoir "pointer" à une coordonnée afin d’être est considéré comme un pointeur. Un clavier, par exemple, ne peut pas être considéré pointeur. Dans Compose, le type de pointeur est inclus dans les modifications de pointeur à l'aide de PointerType
  • Événement de pointeur: décrit une interaction de bas niveau avec un ou plusieurs pointeurs. avec l'application à un moment donné. Toute interaction avec le pointeur, telle que le placement un doigt sur l’écran ou en faisant glisser une souris, déclencherait un événement. Dans Compose, toutes les informations pertinentes pour un tel événement sont incluses dans le PointerEvent.
  • Geste: séquence d'événements de pointeur pouvant être interprétés comme un seul action. Par exemple, un tapotement peut être considéré comme une séquence de gestes vers le bas suivi d'un événement "up". Il existe des gestes courants utilisés par de nombreux comme appuyer, glisser ou transformer, mais vous pouvez aussi créer vos propres un geste lorsque cela est nécessaire.

Différents niveaux d'abstraction

Jetpack Compose fournit différents niveaux d'abstraction pour la gestion des gestes. Le niveau supérieur est la compatibilité des composants. Des composables tels que Button incluent automatiquement la prise en charge des gestes. Pour ajouter la prise en charge des gestes aux vous pouvez ajouter des modificateurs de geste tels que clickable composables. Enfin, si vous avez besoin d'un geste personnalisé, vous pouvez utiliser Modificateur pointerInput.

En règle générale, utilisez le niveau d'abstraction le plus élevé qui offre les fonctionnalités dont vous avez besoin. Vous bénéficiez ainsi des bonnes pratiques dans le calque. Par exemple, Button contient davantage d'informations sémantiques, utilisées pour l'accessibilité, que clickable, qui contient plus d'informations qu'une Implémentation de pointerInput.

Compatibilité avec les composants

De nombreux composants prêts à l'emploi de Compose incluent des gestes internes gestion. Par exemple, un LazyColumn répond aux gestes de glissement en lorsque vous faites défiler le contenu, un Button affiche une ondulation lorsque vous appuyez dessus. et le composant SwipeToDismiss contient une logique de balayage . Ce type de gestion des gestes fonctionne automatiquement.

En plus de la gestion interne des gestes, de nombreux composants nécessitent que l'appelant gérer le geste. Par exemple, un Button détecte automatiquement les actions sans contact. et déclenche un événement de clic. Vous transmettez un lambda onClick à Button pour réagissent au geste. De même, vous ajoutez un lambda onValueChange à une Slider pour réagir lorsque l'utilisateur fait glisser la poignée du curseur.

Une fois que cela est adapté à votre cas d'utilisation, privilégiez les gestes inclus dans les composants, car ils incluent une prise en charge prête à l'emploi pour la mise au point et l'accessibilité. bien testés. Par exemple, un Button est marqué d'une manière spéciale afin que les services d'accessibilité le décrivent correctement comme un bouton, et non comme n'importe quelle élément cliquable:

// Talkback: "Click me!, Button, double tap to activate"
Button(onClick = { /* TODO */ }) { Text("Click me!") }
// Talkback: "Click me!, double tap to activate"
Box(Modifier.clickable { /* TODO */ }) { Text("Click me!") }

Pour en savoir plus sur l'accessibilité dans Compose, consultez Accessibilité dans Nouveau message.

Ajouter des gestes spécifiques à des composables arbitraires avec des modificateurs

Vous pouvez appliquer des modificateurs de geste à n'importe quel composable pour que la fonction composable d'écoute des gestes. Par exemple, vous pouvez laisser un élément Box générique gérez les gestes tactiles en le faisant clickable, ou laissez un Column gérer le défilement vertical en appliquant verticalScroll.

De nombreux modificateurs permettent de gérer différents types de gestes:

En règle générale, il est préférable d'utiliser des modificateurs de gestes prêts à l'emploi par rapport à la gestion des gestes personnalisés. Les modificateurs ajoutent des fonctionnalités en plus de la gestion des événements de pointeur pur. Par exemple, le modificateur clickable ajoute non seulement la détection des pressions et mais il ajoute aussi des informations sémantiques, des indications visuelles sur les interactions, le survol, le focus et la prise en charge du clavier. Vous pouvez consulter le code source de clickable pour voir comment est en cours d'ajout.

Ajouter un geste personnalisé à des composables arbitraires avec le modificateur pointerInput

Tous les gestes ne sont pas implémentés avec un modificateur de geste prêt à l'emploi. Pour Par exemple, vous ne pouvez pas utiliser de modificateur pour réagir à un déplacement après un appui prolongé, en maintenant la touche Ctrl enfoncée ou en appuyant avec trois doigts. Vous pouvez écrire votre propre geste pour identifier ces gestes personnalisés. Vous pouvez créer un gestionnaire de gestes avec Le modificateur pointerInput, qui vous donne accès au pointeur brut événements.

Le code suivant écoute les événements de pointeur brut:

@Composable
private fun LogPointerEvents(filter: PointerEventType? = null) {
    var log by remember { mutableStateOf("") }
    Column {
        Text(log)
        Box(
            Modifier
                .size(100.dp)
                .background(Color.Red)
                .pointerInput(filter) {
                    awaitPointerEventScope {
                        while (true) {
                            val event = awaitPointerEvent()
                            // handle pointer event
                            if (filter == null || event.type == filter) {
                                log = "${event.type}, ${event.changes.first().position}"
                            }
                        }
                    }
                }
        )
    }
}

Si vous scindez cet extrait, les composants principaux sont les suivants:

  • Modificateur pointerInput. Vous lui transmettez une ou plusieurs clés. Lorsque de l'une de ces touches change, le lambda du contenu de modificateur est réexécuté. L'exemple transmet un filtre facultatif au composable. Si la valeur de ce filtre change, le gestionnaire d'événements de pointeur doit être réexécutée pour s’assurer que les bons événements sont consignés.
  • awaitPointerEventScope crée un champ d'application de coroutine pouvant être utilisé pour attend les événements de pointeur.
  • awaitPointerEvent suspend la coroutine jusqu'à un événement de pointeur suivant se produit.

Bien que l'écoute d'événements d'entrée brutes soit puissante, il est également complexe d'écrire un geste personnalisé basé sur ces données brutes. Pour simplifier la création gestes, de nombreuses méthodes utilitaires sont disponibles.

Détecter les gestes complets

Au lieu de gérer les événements de pointeur brut, vous pouvez écouter des gestes spécifiques et de réagir de manière appropriée. Le AwaitPointerEventScope fournit méthodes d'écoute:

Il s'agit de détecteurs de niveau supérieur. Vous ne pouvez donc pas ajouter plusieurs détecteurs dans un même détecteur. Modificateur pointerInput. L'extrait de code suivant ne détecte que les actions effectuées, fait glisser:

var log by remember { mutableStateOf("") }
Column {
    Text(log)
    Box(
        Modifier
            .size(100.dp)
            .background(Color.Red)
            .pointerInput(Unit) {
                detectTapGestures { log = "Tap!" }
                // Never reached
                detectDragGestures { _, _ -> log = "Dragging" }
            }
    )
}

En interne, la méthode detectTapGestures bloque la coroutine, et la seconde et le détecteur de fumée n'est jamais atteint. Si vous devez ajouter plusieurs écouteurs de gestes un composable, utilisez plutôt des instances de modificateur pointerInput distinctes:

var log by remember { mutableStateOf("") }
Column {
    Text(log)
    Box(
        Modifier
            .size(100.dp)
            .background(Color.Red)
            .pointerInput(Unit) {
                detectTapGestures { log = "Tap!" }
            }
            .pointerInput(Unit) {
                // These drag events will correctly be triggered
                detectDragGestures { _, _ -> log = "Dragging" }
            }
    )
}

Gérer les événements par geste

Par définition, les gestes commencent par un événement "Pointer vers le bas". Vous pouvez utiliser awaitEachGesture au lieu de la boucle while(true) qui passe par chaque événement brut. La méthode awaitEachGesture redémarre contenant le bloc lorsque tous les pointeurs ont été levés, ce qui indique que le geste est terminé:

@Composable
private fun SimpleClickable(onClick: () -> Unit) {
    Box(
        Modifier
            .size(100.dp)
            .pointerInput(onClick) {
                awaitEachGesture {
                    awaitFirstDown().also { it.consume() }
                    val up = waitForUpOrCancellation()
                    if (up != null) {
                        up.consume()
                        onClick()
                    }
                }
            }
    )
}

En pratique, vous utiliserez presque toujours awaitEachGesture, sauf si vous répondre aux événements de pointeur sans identifier les gestes. En voici un exemple : hoverable, qui ne répond pas aux événements de type "pointer vers le bas" ou "vers le haut", mais simplement doit savoir quand un pointeur entre ou sort de ses limites.

Attendre un événement ou un sous-gest spécifique

Un ensemble de méthodes permet d'identifier les parties courantes des gestes:

Appliquer des calculs pour les événements tactiles multipoints

Lorsqu'un utilisateur effectue un geste multipoint à l'aide de plusieurs pointeurs, il est complexe de comprendre la transformation requise en fonction des valeurs brutes. Si le modificateur transformable ou detectTransformGestures ne permettent pas un contrôle assez précis pour votre cas d'utilisation, vous pouvez écouter les événements bruts et appliquer des calculs sur ceux-ci. Ces méthodes d'assistance sont calculateCentroid, calculateCentroidSize, calculatePan, calculateRotation et calculateZoom.

Envoi d'événements et test de positionnement

Tous les événements de pointeur ne sont pas envoyés à tous les modificateurs pointerInput. Événement Le processus de répartition est le suivant:

  • Les événements de pointeur sont envoyés à une hiérarchie composable. Le moment où un le nouveau pointeur déclenche son premier événement de pointeur, le système lance le test de positionnement les "éligibles" composables. Un composable est considéré comme éligible lorsqu'il a de gestion des entrées de pointeur. Flux de test de positionnement en haut de l'UI vers le bas. Un composable est "hit" Lorsque l'événement de pointeur s'est produit dans les limites de ce composable. Ce processus crée une chaîne de composables qui effectuent des tests de positionnement positifs.
  • Par défaut, lorsqu'il existe plusieurs composables éligibles au même niveau de dans l'arborescence, seul le composable dont le z-index est le plus élevé est "hit". Pour Par exemple, lorsque vous ajoutez deux composables Button qui se chevauchent à un élément Box, seuls celui dessiné au-dessus reçoit tous les événements de pointeur. En théorie, vous pouvez ignorer ce comportement en créant votre propre PointerInputModifierNode et en définissant sharePointerInputWithSiblings sur "true".
  • Les autres événements correspondant au même pointeur sont envoyés à cette même chaîne de composables et au flux conformément à la logique de propagation des événements. Le système n'effectue plus de test de positionnement pour ce pointeur. Cela signifie que chaque le composable de la chaîne reçoit tous les événements de ce pointeur, même si qui se produisent en dehors des limites de ce composable. Les composables qui ne sont pas de la chaîne ne reçoivent jamais d'événements de pointeur, même lorsqu'il est à l'intérieur de leurs limites.

Les événements de survol, déclenchés par le passage d'une souris ou d'un stylet, font exception à la règle définies ici. Les événements de pointage sont envoyés à tous les composables qu'ils appuient. Donc lorsqu'un utilisateur pointe un pointeur entre les limites d'un composable et le suivant, Au lieu d'envoyer les événements à ce premier composable, ils sont envoyés au nouveau composable.

Consommation d'événements

Lorsque plusieurs composables sont associés à un gestionnaire de gestes, et les gestionnaires de gestion ne doivent pas entrer en conflit. Examinons par exemple cette interface utilisateur:

Élément de liste avec une image, une colonne avec deux textes et un bouton.

Lorsqu'un utilisateur appuie sur le bouton de favori, le lambda onClick du bouton gère cela. geste. Lorsqu'un utilisateur appuie sur n'importe quelle autre partie de l'élément de liste, le ListItem gère ce geste et accède à l'article. Concernant l'entrée de pointeur, le bouton doit utiliser cet événement pour que son parent sache n'y réagissent plus. Les gestes inclus dans les composants prêts à l'emploi et les commandes Les modificateurs de geste courants incluent ce comportement, mais si vous un geste personnalisé, vous devez utiliser les événements manuellement. À faire avec la méthode PointerInputChange.consume:

Modifier.pointerInput(Unit) {

    awaitEachGesture {
        while (true) {
            val event = awaitPointerEvent()
            // consume all changes
            event.changes.forEach { it.consume() }
        }
    }
}

L'utilisation d'un événement n'arrête pas la propagation de l'événement aux autres composables. A Le composable doit ignorer explicitement les événements consommés à la place. Lors de l'écriture des gestes personnalisés, vérifiez si un événement a déjà été consommé par un autre :

Modifier.pointerInput(Unit) {
    awaitEachGesture {
        while (true) {
            val event = awaitPointerEvent()
            if (event.changes.any { it.isConsumed }) {
                // A pointer is consumed by another gesture handler
            } else {
                // Handle unconsumed event
            }
        }
    }
}

Propagation des événements

Comme indiqué précédemment, les modifications du pointeur sont transmises à chaque composable auquel il correspond. Mais s'il existe plusieurs composables de ce type, dans quel ordre les événements propager ? Si vous prenez l'exemple de la section précédente, cette UI se traduit par l'arborescence d'UI suivante, où seuls ListItem et Button répondent événements de pointeur:

Structure arborescente. La couche supérieure est ListItem, la deuxième contient Image, Column et Button, et la Column est divisée en deux éléments Text. ListItem et Button sont mis en évidence.

Les événements de pointeur transitent trois fois par chacun de ces composables, pendant trois "passes":

  • Dans la carte initiale, l'événement part du haut de l'arborescence de l'UI vers en bas. Ce flux permet à un parent d'intercepter un événement avant que l'enfant ne puisse les consommer. Par exemple, les info-bulles doivent intercepter l'utiliser de manière prolongée au lieu de la transmettre à leurs enfants. Dans notre Par exemple, ListItem reçoit l'événement avant Button.
  • Dans la carte principale, l'événement passe des nœuds feuilles de l'arborescence de l'interface utilisateur jusqu'à racine de l'arborescence de l'UI. Au cours de cette phase, on utilise généralement des gestes, la carte par défaut lors de l'écoute d'événements. Gérer les gestes dans cette carte signifie que les nœuds feuilles ont priorité sur leurs parents, ce qui est le le comportement le plus logique pour la plupart des gestes. Dans notre exemple, Button reçoit l'événement avant ListItem.
  • Dans la carte finale, l'événement s'exécute une fois de plus en partant du haut de l'UI. aux nœuds feuilles. Ce flux permet aux éléments situés plus haut dans la pile à la consommation d'événements par leurs parents. Par exemple, un bouton supprime une ondulation lorsqu'une pression se transforme en déplacement de l'élément parent à faire défiler.

Visuellement, le flux d'événements peut être représenté comme suit:

Une fois qu'une modification d'entrée est consommée, ces informations sont transmises à partir du point suivant:

Dans le code, vous pouvez spécifier la carte qui vous intéresse:

Modifier.pointerInput(Unit) {
    awaitPointerEventScope {
        val eventOnInitialPass = awaitPointerEvent(PointerEventPass.Initial)
        val eventOnMainPass = awaitPointerEvent(PointerEventPass.Main) // default
        val eventOnFinalPass = awaitPointerEvent(PointerEventPass.Final)
    }
}

Dans cet extrait de code, le même événement identique est renvoyé par chacune des ces appels de méthode "await", bien que les données sur la consommation modifié.

Tester les gestes

Dans vos méthodes de test, vous pouvez envoyer manuellement des événements de pointeur à l'aide de la méthode performTouchInput. Cela vous permet d'effectuer des tâches les gestes complets (pincer ou cliquer de manière prolongée, par exemple) ou les gestes de faible niveau (comme en déplaçant le curseur d'une certaine quantité de pixels):

composeTestRule.onNodeWithTag("MyList").performTouchInput {
    swipeUp()
    swipeDown()
    click()
}

Consultez la documentation performTouchInput pour obtenir d'autres exemples.

En savoir plus

Pour en savoir plus sur les gestes dans Jetpack Compose, consultez les ressources suivantes : ressources: