Es gibt einige Begriffe und Konzepte, die für die Gestenhandhabung in einer App wichtig sind. Auf dieser Seite werden die Begriffe „Zeiger“, „Zeigerereignisse“ und „Gesten“ erläutert. Außerdem werden die verschiedenen Abstraktionsebenen für Gesten vorgestellt. Außerdem werden der Ereigniskonsum und die Verbreitung von Ereignissen ausführlicher behandelt.
Definitionen
Um die verschiedenen Konzepte auf dieser Seite zu verstehen, müssen Sie einige der verwendeten Begriffe kennen:
- Zeiger: Ein physisches Objekt, das Sie zur Interaktion mit Ihrer Anwendung verwenden können.
Bei Mobilgeräten ist der Zeiger am häufigsten, wenn ein Finger mit dem Touchscreen interagiert. Alternativ kannst du den Finger auch mit einem Eingabestift ersetzen.
Bei großen Bildschirmen können Sie eine Maus oder ein Touchpad verwenden, um indirekt mit dem Display zu interagieren. Ein Eingabegerät muss in der Lage sein, auf eine Koordinate zu „Punkten“, damit es als Zeiger betrachtet wird. Daher kann eine Tastatur beispielsweise nicht als Zeiger betrachtet werden. In „Compose“ wird der Zeigertyp mit
PointerType
in Zeigeränderungen einbezogen. - Zeigerereignis: Beschreibt eine Low-Level-Interaktion eines oder mehrerer Zeiger mit der Anwendung zu einem bestimmten Zeitpunkt. Jede Zeigerinteraktion, z. B. das Platzieren eines Fingers auf den Bildschirm oder das Ziehen einer Maus, würde ein Ereignis auslösen. In Composer befinden sich alle relevanten Informationen für ein solches Ereignis in der Klasse
PointerEvent
. - Geste: Eine Abfolge von Zeigerereignissen, die als eine einzelne Aktion interpretiert werden können. Eine Tippgeste kann beispielsweise als Abfolge eines Abwärtsereignisses gefolgt von einem Aufwärtsereignis betrachtet werden. Es gibt gängige Gesten, die von vielen Apps verwendet werden, z. B. Tippen, Ziehen oder Transformieren. Sie können bei Bedarf aber auch eigene benutzerdefinierte Gesten erstellen.
Verschiedene Abstraktionsebenen
Jetpack Compose bietet verschiedene Abstraktionsebenen für die Verarbeitung von Gesten.
Auf der obersten Ebene finden Sie den Support für Komponenten. Zusammensetzbare Funktionen wie Button
enthalten automatisch die Unterstützung von Gesten. Wenn Sie Gesten für benutzerdefinierte Komponenten unterstützen möchten, können Sie beliebigen zusammensetzbaren Funktionen Gestenmodifikatoren wie clickable
hinzufügen. Wenn Sie eine benutzerdefinierte Geste benötigen, können Sie schließlich den Modifikator pointerInput
verwenden.
In der Regel sollten Sie auf der höchsten Abstraktionsebene aufbauen, die die erforderliche Funktionalität bietet. So profitieren Sie von den Best Practices, die in der Ebene enthalten sind. Beispielsweise enthält Button
mehr semantische Informationen, die für die Barrierefreiheit verwendet werden, als clickable
, das mehr Informationen als eine Rohimplementierung von pointerInput
enthält.
Unterstützung von Komponenten
Viele vorkonfigurierte Komponenten in Compose enthalten eine Art interne Gestenbehandlung. Ein LazyColumn
reagiert auf Ziehbewegungen, indem es durch seinen Inhalt scrollt, ein Button
zeigt eine Welle an, wenn Sie darauf drücken, und die Komponente SwipeToDismiss
enthält Wischlogik zum Schließen eines Elements. Diese Art der Gestenhandhabung funktioniert automatisch.
Neben der internen Gestenhandhabung muss der Aufrufer bei vielen Komponenten auch die Geste verarbeiten. Ein Button
erkennt beispielsweise automatisch Tippen und löst ein Klickereignis aus. Sie übergeben ein onClick
-Lambda an Button
, um auf die Geste zu reagieren. Auf ähnliche Weise fügst du einem Slider
ein onValueChange
-Lambda hinzu, um darauf zu reagieren, dass der Nutzer den Schieberegler zieht.
Wenn es Ihrem Anwendungsfall entspricht, sollten Sie Gesten in Komponenten verwenden, da sie sofort einsatzbereit für Fokus und Bedienungshilfen sind und gut getestet sind. Beispielsweise wird ein Button
speziell markiert, damit die Bedienungshilfen sie korrekt als Schaltfläche und nicht als jedes anklickbare Element beschreiben:
// 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!") }
Weitere Informationen zu den Bedienungshilfen in Compose finden Sie unter Bedienungshilfen in Composer.
Beliebigen zusammensetzbaren Funktionen bestimmte Gesten mit Modifikatoren hinzufügen
Sie können Gestenmodifikatoren auf jede beliebige zusammensetzbare Funktion anwenden, damit die zusammensetzbare Funktion auf Gesten wartet. Du kannst beispielsweise einem allgemeinen Box
Tippgesten zuweisen, indem du ihn clickable
gibst, oder einem Column
vertikalen Scrollen überlassen, indem du verticalScroll
anwendest.
Es gibt viele Modifikatoren, mit denen verschiedene Arten von Gesten gehandhabt werden können:
- Mit den Modifikatoren
clickable
,combinedClickable
,selectable
,toggleable
undtriStateToggleable
können Sie Tippen und Drücken verarbeiten. - Bearbeite das Scrollen mit den Modifikatoren
horizontalScroll
,verticalScroll
und allgemeinerscrollable
. - Ziehbewegungen mit dem Modifikator
draggable
undswipeable
verwenden. - Multi-Touch-Gesten wie Schwenken, Drehen und Zoomen können mit dem Modifikator
transformable
verwendet werden.
Grundsätzlich sollten Sie sofort einsatzbereite Gestenmodifikatoren gegenüber benutzerdefinierten Gestenhandhabungen bevorzugen.
Die Modifikatoren bieten neben der Verarbeitung reiner Zeigerereignisse weitere Funktionen.
Der Modifikator clickable
fügt beispielsweise nicht nur die Erkennung von Drücken und Tippen hinzu, sondern fügt auch semantische Informationen, visuelle Hinweise zu Interaktionen, Bewegen des Mauszeigers, Fokus und Tastaturunterstützung hinzu. Im Quellcode von clickable
können Sie sehen, wie die Funktionen hinzugefügt werden.
Mit dem pointerInput
-Modifikator benutzerdefinierte Gesten zu beliebigen zusammensetzbaren Funktionen hinzufügen
Nicht jede Geste wird mit einem vorkonfigurierten Gestenmodifikator implementiert. Beispielsweise ist es nicht möglich, mit einem Modifikator auf langes Drücken, Klicken bei gedrückter Steuerungstaste oder Tippen mit drei Fingern auf Ziehbewegungen zu reagieren. Stattdessen können Sie einen eigenen Gesten-Handler schreiben, um diese benutzerdefinierten Gesten zu identifizieren. Sie können einen Gesten-Handler mit dem Modifikator pointerInput
erstellen, der Zugriff auf die Rohzeigerereignisse gewährt.
Der folgende Code überwacht rohe Zeigerereignisse:
@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}" } } } } ) } }
Wenn Sie dieses Snippet aufschlüsseln, bestehen die folgenden Kernkomponenten:
- Der
pointerInput
-Modifikator. Sie übergeben einen oder mehrere Schlüssel. Wenn sich der Wert eines dieser Schlüssel ändert, wird die Lambda-Funktion für Modifikatorinhalte noch einmal ausgeführt. Das Beispiel übergibt einen optionalen Filter an die zusammensetzbare Funktion. Wenn sich der Wert dieses Filters ändert, sollte der Zeiger-Event-Handler noch einmal ausgeführt werden, damit die richtigen Ereignisse protokolliert werden. awaitPointerEventScope
erstellt einen Koroutinenbereich, der zum Warten auf Zeigerereignisse verwendet werden kann.awaitPointerEvent
hält die Koroutine an, bis ein nächstes Zeigerereignis auftritt.
Das Überwachen von Roheingabeereignissen ist zwar leistungsstark, es ist aber auch komplex, eine benutzerdefinierte Geste auf der Grundlage dieser Rohdaten zu schreiben. Es gibt viele Dienstprogrammmethoden, um das Erstellen benutzerdefinierter Touch-Gesten zu vereinfachen.
Vollständige Touch-Gesten erkennen
Anstatt die Rohzeigerereignisse zu verarbeiten, können Sie auf das Auftreten bestimmter Gesten warten und entsprechend reagieren. Der AwaitPointerEventScope
bietet Methoden zum Überwachen:
- Drücken, tippen, doppeltippen und lange drücken:
detectTapGestures
- Ziehbewegungen:
detectHorizontalDragGestures
,detectVerticalDragGestures
,detectDragGestures
unddetectDragGesturesAfterLongPress
- Transformationen:
detectTransformGestures
Da es sich um Detektoren der obersten Ebene handelt, können Sie nicht mehrere Detektoren mit einem pointerInput
-Modifikator hinzufügen. Das folgende Snippet erkennt nur die Fingertipps, nicht die Ziehbewegungen:
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" } } ) }
Intern blockiert die Methode detectTapGestures
die Koroutine und der zweite Detektor wird nie erreicht. Wenn Sie einer zusammensetzbaren Funktion mehrere Gesten-Listener hinzufügen müssen, verwenden Sie stattdessen separate pointerInput
-Modifikatorinstanzen:
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" } } ) }
Ereignisse pro Geste verarbeiten
Gesten beginnen per Definition mit einem Ereignis vom Typ „Zeiger nach unten“. Sie können die Hilfsmethode awaitEachGesture
anstelle der while(true)
-Schleife verwenden, die jedes Rohereignis durchläuft. Die Methode awaitEachGesture
startet den enthaltenden Block neu, wenn alle Zeiger angehoben wurden, was anzeigt, dass die Geste abgeschlossen ist:
@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() } } } ) }
In der Praxis empfiehlt es sich fast immer, awaitEachGesture
zu verwenden, es sei denn, Sie reagieren auf Zeigerereignisse, ohne Gesten zu identifizieren. Ein Beispiel dafür ist hoverable
, das nicht auf Zeiger-Nach-unten- oder Nach-oben-Ereignisse reagiert – sie muss nur wissen, wenn ein Zeiger in seine Grenzen hinausgeht oder diese verlässt.
Auf ein bestimmtes Ereignis oder eine untergeordnete Bewegung warten
Es gibt eine Reihe von Methoden, mit denen häufige Teile von Gesten identifiziert werden können:
- Anhalten, bis ein Zeiger mit
awaitFirstDown
sinkt, oder warten Sie, bis alle Zeiger mitwaitForUpOrCancellation
nach oben gehen. - Erstellen Sie mit
awaitTouchSlopOrCancellation
undawaitDragOrCancellation
einen Low-Level-Drag-Listener. Der Gesten-Handler wird erst angehalten, bis der Zeiger den Touch-Slop erreicht, und dann wieder pausiert, bis ein erstes Drag-Ereignis ausgelöst wird. Wenn Sie nur an Ziehbewegungen entlang einer einzelnen Achse interessiert sind, verwenden Sie stattdessenawaitHorizontalTouchSlopOrCancellation
+awaitHorizontalDragOrCancellation
oderawaitVerticalTouchSlopOrCancellation
+awaitVerticalDragOrCancellation
. - Anhalten, bis
awaitLongPressOrCancellation
lange gedrückt wird. - Verwenden Sie die Methode
drag
, um Drag-Ereignisse kontinuierlich zu beobachten, oderhorizontalDrag
oderverticalDrag
, um Drag-Ereignisse auf einer Achse zu beobachten.
Berechnungen für Multi-Touch-Ereignisse anwenden
Wenn ein Nutzer eine Multi-Touch-Geste mit mehr als einem Zeiger ausführt, ist es schwierig, die erforderliche Transformation anhand der Rohwerte zu verstehen.
Wenn der Modifikator transformable
oder die Methoden detectTransformGestures
nicht genügend detaillierte Steuerungsmöglichkeiten für Ihren Anwendungsfall bieten, können Sie die Rohereignisse beobachten und Berechnungen darauf anwenden. Diese Hilfsmethoden sind calculateCentroid
, calculateCentroidSize
, calculatePan
, calculateRotation
und calculateZoom
.
Ereignisweiterleitung und Treffertests
Nicht jedes Zeigerereignis wird an jeden pointerInput
-Modifikator gesendet. Die Ereignisweiterleitung funktioniert so:
- Zeigerereignisse werden an eine zusammensetzbare Hierarchie weitergeleitet. In dem Moment, in dem ein neuer Zeiger sein erstes Zeigerereignis auslöst, beginnt das System mit einem Treffertest der „zulässigen“ zusammensetzbaren Funktionen. Eine zusammensetzbare Funktion gilt als zulässig, wenn sie Funktionen zur Verarbeitung von Zeigereingaben hat. Hit-Testing-Abläufe vom oberen zum unteren Rand des UI-Baums. Eine zusammensetzbare Funktion wird als „Treffer“ bezeichnet, wenn das Zeigerereignis innerhalb der Grenzen dieser zusammensetzbaren Funktion aufgetreten ist. Dieser Prozess führt zu einer Kette von zusammensetzbaren Funktionen, die positive Treffertests durchlaufen.
- Wenn sich mehrere zulässige zusammensetzbare Funktionen auf derselben Ebene der Struktur befinden, wird standardmäßig nur die zusammensetzbare Funktion mit dem höchsten Z-Index „hit“ verwendet. Wenn Sie beispielsweise zwei überlappende
Button
-zusammensetzbare Funktionen zu einerBox
hinzufügen, empfängt nur die darüber liegende Zeigerereignisse. Sie können dieses Verhalten theoretisch überschreiben, indem Sie eine eigenePointerInputModifierNode
-Implementierung erstellen undsharePointerInputWithSiblings
auf „true“ setzen. - Weitere Ereignisse für denselben Zeiger werden an dieselbe Kette von zusammensetzbaren Funktionen weitergeleitet und fließen gemäß der Ereignis-Weitergabelogik. Das System führt keine weiteren Treffertests für diesen Zeiger durch. Das bedeutet, dass jede zusammensetzbare Funktion in der Kette alle Ereignisse für diesen Zeiger empfängt, auch wenn diese außerhalb der Grenzen dieser zusammensetzbaren Funktion auftreten. Zusammensetzbare Funktionen, die sich nicht in der Kette befinden, erhalten nie Zeigerereignisse, selbst wenn sich der Zeiger innerhalb ihrer Grenzen befindet.
Hover-Ereignisse, die durch Bewegen der Maus oder eines Eingabestifts ausgelöst werden, sind eine Ausnahme zu den hier definierten Regeln. Hover-Ereignisse werden an jede zusammensetzbare Funktion gesendet, auf die sie zutreffen. Wenn also ein Nutzer den Mauszeiger von den Grenzen einer zusammensetzbaren Funktion zur nächsten bewegt, werden die Ereignisse nicht an diese erste zusammensetzbare Funktion gesendet.
Ereignisverbrauch
Wenn mehreren zusammensetzbaren Funktionen ein Gesten-Handler zugewiesen ist, sollten sich diese Handler nicht widersprechen. Sehen Sie sich zum Beispiel diese UI an:
Wenn ein Nutzer auf die Lesezeichenschaltfläche tippt, wird diese Bewegung durch das onClick
-Lambda-Element der Schaltfläche ausgeführt. Wenn ein Nutzer auf einen anderen Teil des Listenelements tippt, steuert ListItem
diese Geste und ruft den Artikel auf. Was die Zeigereingabe betrifft, muss die Schaltfläche dieses Ereignis aufnehmen, damit das übergeordnete Element weiß, dass es nicht mehr darauf reagieren soll. Zu den Gesten, die in vorkonfigurierten Komponenten enthalten sind, und in den gängigen Gestenmodifikatoren ist dieses Nutzungsverhalten enthalten. Wenn Sie jedoch eine eigene benutzerdefinierte Geste schreiben, müssen Sie Ereignisse manuell verarbeiten. Dazu verwenden Sie die Methode PointerInputChange.consume
:
Modifier.pointerInput(Unit) { awaitEachGesture { while (true) { val event = awaitPointerEvent() // consume all changes event.changes.forEach { it.consume() } } } }
Durch die Verarbeitung eines Ereignisses wird die Ereignisweitergabe an andere zusammensetzbare Funktionen nicht gestoppt. Stattdessen muss eine zusammensetzbare Funktion verbrauchte Ereignisse explizit ignorieren. Wenn Sie benutzerdefinierte Touch-Gesten schreiben, sollten Sie prüfen, ob ein Ereignis bereits von einem anderen Element verarbeitet wurde:
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 } } } }
Ereignisweitergabe
Wie bereits erwähnt, werden Zeigeränderungen an jede zusammensetzbare Funktion übergeben, auf die sie angewendet wird.
Wenn jedoch mehr als eine solche zusammensetzbare Funktion existiert, in welcher Reihenfolge werden die Ereignisse übertragen? Wenn Sie das Beispiel aus dem letzten Abschnitt nehmen, wird diese UI in den folgenden UI-Baum umgewandelt, in dem nur ListItem
und Button
auf Zeigerereignisse reagieren:
Zeigerereignisse fließen durch jede dieser zusammensetzbaren Funktionen dreimal in drei Durchgängen:
- In der ersten Karte bzw. dem ersten Ticket fließt das Ereignis vom oberen Rand der UI-Struktur nach unten. Dieser Ablauf ermöglicht es einem übergeordneten Element, ein Ereignis abzufangen, bevor das untergeordnete Element es verarbeiten kann. Beispielsweise müssen Kurzinfos ein langes Drücken abfangen, anstatt es an die untergeordneten Elemente weiterzugeben. In unserem Beispiel empfängt
ListItem
das Ereignis vor demButton
. - In der Hauptkarte fließt das Ereignis von den Blattknoten der UI-Struktur bis zum Stamm der UI-Baum. In dieser Phase werden normalerweise Gesten verwendet. Sie ist die Standardkarte beim Überwachen von Ereignissen. Bei der Verarbeitung von Touch-Gesten in dieser Karte bzw. diesem Ticket haben Blattknoten Vorrang vor ihren übergeordneten Elementen. Dies ist für die meisten Gesten das logischste Verhalten. In unserem Beispiel empfängt
Button
das Ereignis vor demListItem
. - In der Endgültigen Karte fließt das Ereignis noch einmal vom oberen Rand der UI-Baumstruktur zu den Blattknoten. Durch diesen Ablauf können Elemente weiter oben im Stack auf den Ereignisverbrauch durch ihr übergeordnetes Element reagieren. Beispielsweise entfernt eine Schaltfläche die Wellenlinie, wenn sich beim Drücken eine Ziehbewegung des scrollbaren übergeordneten Elements ändert.
Visuell kann der Ereignisfluss so dargestellt werden:
Sobald eine Eingabeänderung vorgenommen wurde, werden diese Informationen ab diesem Punkt im Ablauf übergeben:
Im Code können Sie die gewünschte Karte bzw. das gewünschte Ticket angeben:
Modifier.pointerInput(Unit) { awaitPointerEventScope { val eventOnInitialPass = awaitPointerEvent(PointerEventPass.Initial) val eventOnMainPass = awaitPointerEvent(PointerEventPass.Main) // default val eventOnFinalPass = awaitPointerEvent(PointerEventPass.Final) } }
In diesem Code-Snippet wird von jedem dieser Await-Methodenaufrufe dasselbe identische Ereignis zurückgegeben, obwohl sich die Daten zum Verbrauch möglicherweise geändert haben.
Touch-Gesten testen
In Ihren Testmethoden können Sie Zeigerereignisse manuell mit der Methode performTouchInput
senden. So können Sie entweder vollständige Touch-Gesten (z. B. Auseinander- und Zusammenziehen oder langes Klicken) oder Low-Level-Gesten (z. B. Bewegen des Cursors um eine bestimmte Anzahl von Pixeln) ausführen:
composeTestRule.onNodeWithTag("MyList").performTouchInput { swipeUp() swipeDown() click() }
Weitere Beispiele finden Sie in der Dokumentation zu performTouchInput
.
Weitere Informationen
Weitere Informationen zu Gesten in Jetpack Compose finden Sie in den folgenden Ressourcen:
Empfehlungen für dich
- Hinweis: Der Linktext wird angezeigt, wenn JavaScript deaktiviert ist.
- Bedienungshilfen in der Funktion „Compose“
- Scrollen
- Tippen und drücken