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:
- Gérer les appuis et les pressions avec
clickable
combinedClickable
,selectable
,toggleable
ettriStateToggleable
. - Gérer le défilement avec
horizontalScroll
verticalScroll
et des modificateursscrollable
plus génériques. - Gérer le déplacement avec
draggable
etswipeable
modificateur. - Gérez les gestes à plusieurs doigts tels que le panoramique, la rotation et le zoom, avec
le modificateur
transformable
.
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:
- Appuyer, appuyer, appuyer deux fois et appuyer de manière prolongée:
detectTapGestures
- Glissements:
detectHorizontalDragGestures
,detectVerticalDragGestures
,detectDragGestures
etdetectDragGesturesAfterLongPress
- Transformations:
detectTransformGestures
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:
- Suspendre jusqu'à ce qu'un pointeur tombe en panne avec
awaitFirstDown
, ou attendre tous les autres pointeurs pour augmenter avecwaitForUpOrCancellation
. - Créer un écouteur de glisser de bas niveau à l'aide de
awaitTouchSlopOrCancellation
etawaitDragOrCancellation
. Le gestionnaire de gestes est d'abord suspendu jusqu'à le pointeur atteint la pente tactile, puis s'arrête jusqu'au premier événement de déplacement. est visible. Si vous n'êtes intéressé que par les glissements sur un seul axe, utilisezawaitHorizontalTouchSlopOrCancellation
ou plusawaitHorizontalDragOrCancellation
ouawaitVerticalTouchSlopOrCancellation
ou plusawaitVerticalDragOrCancellation
à la place. - Suspendre jusqu'à ce qu'un appui prolongé sur
awaitLongPressOrCancellation
se produise. - Utilisez la méthode
drag
pour écouter en continu les événements de déplacement.horizontalDrag
ouverticalDrag
pour écouter les événements de déplacement sur un appareil axe vertical.
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émentBox
, 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 proprePointerInputModifierNode
et en définissantsharePointerInputWithSiblings
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:
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:
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 avantButton
. - 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 avantListItem
. - 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:
Recommandations personnalisées
- Remarque : Le texte du lien s'affiche lorsque JavaScript est désactivé
- Accessibilité dans Compose
- Défilement
- Appuyez et appuyez