Gesten verstehen

Es gibt mehrere Begriffe und Konzepte, die Sie verstehen sollten wenn Sie an der Gestenhandhabung in einer Anwendung arbeiten. Auf dieser Seite werden die Nutzungsbedingungen Zeigerereignisse und Gesten und stellt die verschiedenen Ebenen für Gesten. Außerdem werden die Ereignisdaten und Verbreitung von Inhalten.

Definitionen

Um die verschiedenen Konzepte auf dieser Seite zu verstehen, müssen Sie einige der verwendeten Terminologie:

  • Zeiger: Ein physisches Objekt, über das Sie mit Ihrer Anwendung interagieren können. Bei Mobilgeräten ist der Zeiger am häufigsten, dass Ihr Finger mit Touchscreen verwenden. Alternativ kannst du einen Eingabestift verwenden, um deinen Finger zu ersetzen. Bei großen Bildschirmen können Sie über eine Maus oder ein Touchpad indirekt mit Display. Ein Eingabegerät muss in der Lage sein, auf "verweisen" zu können. auf einer Koordinate sodass z. B. eine Tastatur nicht als Zeiger. In Compose wird der Zeigertyp mithilfe von PointerType
  • Zeigerereignis: Beschreibt eine Interaktion auf niedriger Ebene mit einem oder mehreren Zeigern. mit der Anwendung zu einem bestimmten Zeitpunkt. Jede Zeigerinteraktion, wie z. B. Putting einen Finger auf dem Bildschirm zeigt oder die Maus ziehen, löst ein Ereignis aus. In Alle relevanten Informationen für ein solches Ereignis sind in der Klasse PointerEvent.
  • Geste: Eine Folge von Zeigerereignissen, die als ein einzelnes Aktion ausführen. Tippgesten können z. B. als Abfolge eines Abwärtsspiels betrachtet werden. gefolgt von einem "up"-Ereignis. Es gibt häufige Gesten, die von vielen zum Beispiel durch Tippen, Ziehen oder Umwandeln. Sie können aber auch wenn nötig.

Verschiedene Abstraktionsebenen

Jetpack Compose bietet verschiedene Abstraktionsebenen für die Verarbeitung von Gesten. Ganz oben befindet sich der Support für Komponenten. Zusammensetzbare Elemente wie Button werden automatisch Gesten unterstützt. Unterstützung für Touch-Gesten zu benutzerdefinierten Komponenten lassen sich beliebige Gesten-Modifikatoren wie clickable zusammensetzbare Funktionen verwenden. Wenn Sie eine benutzerdefinierte Touch-Geste benötigen, können Sie den pointerInput-Modifikator.

Nutzen Sie in der Regel die höchste Abstraktionsebene, Funktionen, die Sie benötigen. So profitieren Sie von den im Layer. Beispielsweise enthält Button weitere semantische Informationen, die für als clickable, die mehr Informationen enthält als eine unbearbeitete pointerInput-Implementierung.

Komponentenunterstützung

Viele vorkonfigurierte Komponenten in Compose enthalten eine Art interne Geste Umgang mit Ihren Daten. Ein LazyColumn reagiert beispielsweise auf Ziehgesten wie folgt: Wenn Sie durch den Inhalt scrollen, erscheint eine Button mit einer Welle, wenn Sie auf das Display drücken. und die Komponente SwipeToDismiss eine Wischlogik zum Schließen eines -Elements. Diese Art der Gestenhandhabung funktioniert automatisch.

Neben der internen Gestensteuerung muss der Anrufer bei vielen Komponenten um die Geste auszuführen. Ein Button erkennt beispielsweise automatisch und löst ein Klickereignis aus. Du übergibst ein Lambda von onClick an Button, um auf die Geste reagieren. Genauso fügst du einem Lambda onValueChange zu einem Slider, um zu reagieren, wenn der Nutzer den Schieberegler zieht.

Je nach Anwendungsfall sollten Sie Gesten, die in Komponenten enthalten sind, sofort einsatzbereite Unterstützung für Fokus und Barrierefreiheit, und sie sind getestet. Ein Button ist beispielsweise auf spezielle Weise markiert, sodass Bedienungshilfen beschreiben sie korrekt als Schaltfläche und nicht einfach anklickbares Element:

// 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 der Funktion „Schreiben“ finden Sie unter Bedienungshilfen in Schreiben:

Beliebigen zusammensetzbaren Funktionen mit Modifikatoren bestimmte Touch-Gesten hinzufügen

Sie können Bewegungsmodifikatoren auf jede beliebige zusammensetzbare Funktion anwenden, um zusammensetzbare Funktionen hören auf Gesten. Sie können beispielsweise eine allgemeine Box Tippgesten mit clickable oder mit einem Column verarbeiten. vertikales Scrollen durch Anwenden von verticalScroll verarbeiten können.

Es gibt viele Modifikatoren, um verschiedene Arten von Gesten zu verarbeiten:

In der Regel sollten Sie vorgefertigte Modifikatoren für Touch-Gesten statt benutzerdefinierter Touch-Gesten verwenden. Die Modifikatoren bieten mehr Funktionen zusätzlich zur Verarbeitung reiner Zeigerereignisse. Der Modifikator clickable fügt beispielsweise nicht nur die Erkennung von Drücken und semantische Informationen, visuelle Hinweise auf Interaktionen, Mausbewegung, Fokus und Tastaturunterstützung. Sie können den Quellcode von clickable, um zu sehen, wird hinzugefügt.

Beliebigen zusammensetzbaren Funktionen mit dem pointerInput-Modifikator benutzerdefinierte Touch-Gesten hinzufügen

Nicht jede Geste wird mit einem vorkonfigurierten Bewegungsmodifikator implementiert. Für Sie können z. B. keinen Modifikator verwenden, um auf Ziehbewegungen zu reagieren, Strg-Klick oder mit drei Fingern tippen. Du kannst stattdessen deine eigene Touch-Geste schreiben um diese benutzerdefinierten Gesten zu identifizieren. Sie können einen Gesten-Handler mit den pointerInput-Modifikator, mit dem Sie auf den Rohzeiger zugreifen können Ereignisse.

Mit dem folgenden Code werden rohe Pointer-Ereignisse überwacht:

@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 aufteilen, sind die Hauptkomponenten:

  • Der pointerInput-Modifikator. Sie übergeben ihm einen oder mehrere Schlüssel. Wenn der Parameter ändert sich der Wert eines dieser Schlüssel, ist das Lambda für den Modifikatorinhalt erneut ausgeführt werden. Im Beispiel wird ein optionaler Filter an die zusammensetzbare Funktion übergeben. Wenn ändert sich der Wert dieses Filters, sollte der Zeiger-Event-Handler um sicherzustellen, dass die richtigen Ereignisse protokolliert werden.
  • awaitPointerEventScope erstellt einen Koroutinebereich, mit dem Sie Folgendes tun können: auf Zeigerereignisse warten.
  • awaitPointerEvent unterbricht die Koroutine bis zum nächsten Zeigerereignis erfolgt.

Das Abhören von Roheingabeereignissen ist zwar leistungsstark, es ist jedoch auch schwierig, eine benutzerdefinierte Touch-Geste basierend auf diesen Rohdaten. Um das Erstellen benutzerdefinierter stehen viele Dienstprogrammmethoden zur Verfügung.

Vollständige Gesten erkennen

Anstatt die Rohzeigerereignisse zu verarbeiten, können Sie auf bestimmte Gesten warten. und entsprechend reagieren. Die AwaitPointerEventScope bietet Methoden zum Überwachen von:

Dies sind Detektoren der obersten Ebene. Es können also nicht mehrere Detektoren innerhalb eines einzelnen Detektors hinzugefügt werden. pointerInput-Modifikator. Das folgende Snippet erkennt nur die Berührungen, nicht Drags:

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 die zweite Detektor wird nie erreicht. Wenn Sie mehr als einen Gesten-Listener für eine zusammensetzbare Funktion. 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 Zeiger-nach-unten-Ereignis. Sie können die awaitEachGesture-Hilfsmethode anstelle der while(true)-Schleife, die durchläuft jedes Rohereignis. Mit der Methode awaitEachGesture wird der enthält einen Block, wenn alle Zeiger angehoben wurden. Dies zeigt an, dass die Geste Abgeschlossen:

@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, awaitEachGesture zu verwenden, es sei denn, auf Zeigerereignisse zu reagieren, ohne Gesten zu erkennen. Ein Beispiel: hoverable, die nicht auf Zeiger- oder Aufwärts-Ereignisse reagiert, sondern nur muss wissen, wann ein Zeiger in seine Grenzen eintritt oder ihn verlässt.

Auf ein bestimmtes Ereignis oder eine bestimmte Bewegung warten

Es gibt eine Reihe von Methoden, mit denen häufige Teile von Gesten identifiziert werden können:

Berechnungen für Multi-Touchpoint-Ereignisse anwenden

Führt ein Nutzer eine Multi-Touch-Geste mit mehr als einem Zeiger aus, ist es komplex, die erforderliche Transformation auf Grundlage der Rohwerte zu verstehen. Wenn der transformable-Modifikator oder der detectTransformGestures nicht genügend differenzierte Kontrolle über Ihren Anwendungsfall bieten, können Sie auf Rohdaten warten 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. Ereignis -Weiterleitung funktioniert so:

  • Zeigerereignisse werden in einer zusammensetzbaren Hierarchie abgefertigt. Der Moment, in dem ein neuer Zeiger sein erstes Zeigerereignis auslöst, beginnt das System mit den Treffertests. „Aktiv“ zusammensetzbaren Funktionen. Eine zusammensetzbare Funktion gilt als aktiv, wenn Verarbeitungsfunktionen für die Zeigereingabe. Treffertestabläufe vom oberen Rand der Benutzeroberfläche Baum nach unten. Eine zusammensetzbare Funktion heißt „Treffer“ Zeitpunkt des Zeigerereignisses innerhalb der Grenzen dieser zusammensetzbaren Funktion. Dieser Prozess führt zu einer Kette zusammensetzbaren Funktionen, bei denen Treffertests erfolgreich sind.
  • Wenn mehrere zusammensetzbare Funktionen auf derselben Ebene Baum ist nur die zusammensetzbare Funktion mit dem höchsten Z-Index "hit". Für Wenn Sie beispielsweise zwei zusammensetzbare Funktionen vom Typ Button zu einer Box hinzufügen, die sich überschneiden, Die darüber gezeichnete Karte empfängt Zeigerereignisse. Sie können theoretisch können Sie dieses Verhalten überschreiben, indem Sie eine eigene PointerInputModifierNode erstellen. und sharePointerInputWithSiblings auf „true“ setzen.
  • Weitere Ereignisse für denselben Zeiger werden an dieselbe Kette von zusammensetzbare Funktionen erstellt und die Daten gemäß der Logik für die Ereignisweitergabe verarbeitet werden. Das System führt für diesen Zeiger keine weiteren Treffertests durch. Das bedeutet, dass jeder zusammensetzbare Funktion in der Kette empfängt alle Ereignisse für diesen Zeiger, auch wenn die außerhalb der Grenzen der zusammensetzbaren Funktion liegen. Zusammensetzbare Elemente, die nicht in der Kette nie Zeigerereignisse erhalten, auch wenn der Zeiger innerhalb ihrer Grenzen.

Mouseover-Ereignisse, die durch Bewegen der Maus oder eines Eingabestifts ausgelöst werden, stellen eine Ausnahme von die hier definierten Regeln an. Hover-Ereignisse werden an jede zusammensetzbare Funktion gesendet, die sie treffen. Also wenn ein Nutzer mit der Maus auf einen Zeiger von den Grenzen einer zusammensetzbaren Funktion zur nächsten bewegt, statt sie an die erste zusammensetzbare Funktion zu senden, neu zusammensetzbar.

Ereignisnutzung

Ist mehr als einer zusammensetzbaren Funktion ein Gesten-Handler zugewiesen, werden diese keine Konflikte zwischen Handlern verursachen. Sehen Sie sich zum Beispiel diese UI an:

Listenelement mit einem Bild, einer Spalte mit zwei Texten und einer Schaltfläche.

Wenn ein Nutzer auf die Lesezeichenschaltfläche tippt, übernimmt die Lambda-Funktion onClick der Schaltfläche dies Touch-Geste. Wenn ein Nutzer auf einen anderen Teil des Listeneintrags tippt, wird die ListItem diese Geste verarbeitet und zum entsprechenden Artikel navigiert. Was die Zeigereingabe betrifft, Die Schaltfläche muss dieses Ereignis nutzen, damit das übergeordnete Element weiß, dass dies nicht geschieht. nicht mehr darauf reagieren können. Gesten, die in den vorgefertigten Komponenten integriert sind, und Gängige Bewegungsmodifikatoren beinhalten dieses Verhalten. Ihre eigene benutzerdefinierte Geste schreiben, müssen Sie die Ereignisse manuell verarbeiten. Sie tun das mit der Methode PointerInputChange.consume:

Modifier.pointerInput(Unit) {

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

Die Übernahme eines Ereignisses beendet nicht die Weitergabe des Ereignisses an andere zusammensetzbare Funktionen. A Die zusammensetzbaren Funktionen müssen stattdessen verbrauchte Ereignisse explizit ignorieren. Beim Schreiben können Sie prüfen, ob ein Ereignis bereits von einem anderen :

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 trifft. Wenn jedoch mehr als eine dieser zusammensetzbaren Funktionen vorhanden ist, in welcher Reihenfolge verbreiten? Wenn Sie das Beispiel aus dem letzten Abschnitt nehmen, im folgenden UI-Baum, in dem nur ListItem und Button auf Zeigerereignisse:

Baumstruktur Die oberste Ebene ist ListItem, die zweite Ebene verfügt über ein Bild, eine Spalte und eine Schaltfläche und die Spalte ist in zwei Texte aufgeteilt. „ListItem“ und „Button“ sind markiert.

Zeigerereignisse fließen dreimal durch jede dieser zusammensetzbaren Funktionen. "passes":

  • In der ersten Karte wird das Ereignis vom oberen Rand des UI-Baums an den unten. Bei diesem Ablauf kann ein übergeordnetes Element ein Ereignis abfangen, bevor das untergeordnete Element es konsumieren. Beispielsweise müssen Kurzinfos einen lange drücken, anstatt sie an ihr Kind weiterzugeben. In unserem Beispiel: ListItem empfängt das Ereignis vor Button.
  • In der Hauptkarte fließt das Ereignis von den Blattknoten der Benutzeroberfläche bis zum Stamm des UI-Baums. In dieser Phase nutzt du normalerweise Gesten. die Standardkarte für die Überwachung von Ereignissen. Gesten in dieser Karte bzw. diesem Ticket verarbeiten Blattknoten haben Vorrang vor ihren übergeordneten Knoten, d. h. Verhaltens für die meisten Gesten ist. In unserem Beispiel erhält die Button das Ereignis vor ListItem.
  • In der endgültigen Karte läuft das Ereignis noch einmal vom oberen Rand der Benutzeroberfläche aus. bis zu den Blattknoten. Durch diesen Ablauf können Elemente weiter oben im Stapel auf die Ereignisdaten durch die übergeordneten Elemente reagieren. Über eine Schaltfläche werden beispielsweise Wenn ein Drücken in einen Ziehpunkt des scrollbaren übergeordneten Elements verwandelt wird, wird die Wellenform angezeigt.

Der Ereignisfluss kann folgendermaßen visuell dargestellt werden:

Sobald eine Eingabeänderung verarbeitet wurde, werden diese Informationen von dieser Punkt im Ablauf ab:

Im Code kannst du 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 dasselbe identische Ereignis Aufrufe der Await-Methode, obwohl die Daten zum Verbrauch geändert.

Touch-Gesten testen

In Ihren Testmethoden können Sie manuell Zeigerereignisse mit der Methode performTouchInput-Methode. So können Sie entweder auf höherer Ebene Touch-Gesten wie Auseinander- und Zusammenziehen oder langes Klicken oder Bewegungen auf niedriger Ebene, z. B. den Cursor um eine bestimmte Anzahl von Pixeln bewegen):

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

Weitere Beispiele finden Sie in der Dokumentation zu performTouchInput.

Weitere Informationen

Weitere Informationen zu Touch-Gesten in Jetpack Compose findest du hier: Ressourcen: