Komponenten der Benutzeroberfläche geben dem Gerätenutzer Feedback darüber, wie sie auf Nutzerinteraktionen reagieren. Jede Komponente reagiert auf ihre eigene Art und Weise auf Interaktionen, sodass der Nutzer weiß, was seine Interaktionen tun. Wenn ein Nutzer beispielsweise eine Schaltfläche auf dem Touchscreen eines Geräts berührt, ändert sich die Schaltfläche wahrscheinlich in irgendeiner Weise, z. B. durch Hinzufügen einer Hervorhebungsfarbe. Durch diese Änderung weiß der Nutzer, dass er auf die Schaltfläche geklickt hat. Wenn der Nutzer das nicht tun wollte, wird er den Finger von der Schaltfläche wegziehen, bevor er sie wieder loslässt. Andernfalls wird die Schaltfläche aktiviert.
In der Dokumentation zu Touch-Gesten wird beschrieben, wie Composer-Komponenten Low-Level-Zeigerereignisse wie Zeigerbewegungen und Klicks verarbeiten. Diese untergeordneten Ereignisse werden standardmäßig in übergeordnete Interaktionen abstrahiert. So kann eine Reihe von Zeigerereignissen zum Drücken und Loslassen der Schaltfläche hinzugefügt werden. Wenn Sie diese übergeordneten Abstraktionen verstehen, können Sie anpassen, wie Ihre UI auf den Nutzer reagiert. Sie können beispielsweise anpassen, wie sich die Darstellung einer Komponente ändert, wenn der Nutzer mit ihr interagiert, oder ein Protokoll dieser Nutzeraktionen führen. Dieses Dokument enthält die Informationen, die Sie benötigen, um die Standard-UI-Elemente anzupassen oder eigene zu entwerfen.
Interaktionen
In vielen Fällen müssen Sie nicht wissen, wie die Komponente „Compose“ Nutzerinteraktionen interpretiert. Button
stützt sich beispielsweise auf Modifier.clickable
, um herauszufinden, ob der Nutzer auf die Schaltfläche geklickt hat. Wenn du eine typische Schaltfläche zu deiner App hinzufügst, kannst du den onClick
-Code der Schaltfläche definieren. Modifier.clickable
führt diesen Code dann bei Bedarf aus. Sie müssen also nicht wissen, ob der Nutzer auf den Bildschirm getippt oder die Schaltfläche mit einer Tastatur ausgewählt hat. Modifier.clickable
erkennt, dass der Nutzer einen Klick ausgeführt hat, und antwortet, indem er Ihren onClick
-Code ausführt.
Wenn Sie jedoch die Reaktion der UI-Komponente auf das Nutzerverhalten anpassen möchten, müssen Sie möglicherweise mehr darüber wissen, was im Hintergrund passiert. Dieser Abschnitt enthält einige dieser Informationen.
Wenn ein Nutzer mit einer UI-Komponente interagiert, repräsentiert das System sein Verhalten, indem eine Reihe von Interaction
-Ereignissen generiert wird. Wenn ein Nutzer beispielsweise eine Schaltfläche berührt, wird über die Schaltfläche PressInteraction.Press
generiert.
Wenn der Nutzer den Finger von der Schaltfläche hebt, wird ein PressInteraction.Release
generiert. Dadurch wird die Schaltfläche darüber informiert, dass der Klick abgeschlossen ist. Wenn der Nutzer hingegen seinen Finger außerhalb der Schaltfläche zieht und dann wieder loshebt, wird für die Schaltfläche PressInteraction.Cancel
generiert. Das bedeutet, dass das Drücken der Schaltfläche abgebrochen, aber nicht abgeschlossen wurde.
Diese Interaktionen sind ohne Meinung. Das heißt, diese Interaktionen auf niedriger Ebene haben nicht die Absicht, die Bedeutung der Nutzeraktionen oder deren Reihenfolge zu interpretieren. Sie interpretieren auch nicht, welche Nutzeraktionen Vorrang vor anderen Aktionen haben könnten.
Diese Interaktionen erfolgen in der Regel paarweise, jeweils mit einem Anfang und einem Ende. Die zweite Interaktion enthält einen Verweis auf die erste. Wenn ein Nutzer beispielsweise auf eine Schaltfläche tippt und dann seinen Finger hebt, wird durch die Berührung eine PressInteraction.Press
-Interaktion generiert und durch den Release wird ein PressInteraction.Release
generiert. Release
hat eine press
-Eigenschaft, die die Anfangs-PressInteraction.Press
identifiziert.
Sie können die Interaktionen für eine bestimmte Komponente sehen, indem Sie deren InteractionSource
beobachten. InteractionSource
basiert auf Kotlin-Abläufen. Sie können also die Interaktionen daraus wie bei jedem anderen Ablauf erfassen. Weitere Informationen zu dieser Designentscheidung finden Sie im Blogpost Illuminating Interactions (auf Englisch).
Interaktionsstatus
Sie können die integrierte Funktionalität Ihrer Komponenten erweitern, indem Sie auch die Interaktionen selbst erfassen. Vielleicht möchten Sie z. B., dass sich die
Farbe einer Schaltfläche ändert, wenn sie gedrückt wird. Die einfachste Möglichkeit zum Verfolgen der Interaktionen besteht darin, den entsprechenden Interaktionsstatus zu beobachten. InteractionSource
bietet eine Reihe von Methoden, mit denen verschiedene Interaktionsstatus als Status angezeigt werden. Wenn Sie beispielsweise sehen möchten, ob eine bestimmte Schaltfläche gedrückt wurde, können Sie die entsprechende InteractionSource.collectIsPressedAsState()
-Methode aufrufen:
val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() Button( onClick = { /* do something */ }, interactionSource = interactionSource ) { Text(if (isPressed) "Pressed!" else "Not pressed") }
Neben collectIsPressedAsState()
bietet die Funktion collectIsFocusedAsState()
, collectIsDraggedAsState()
und collectIsHoveredAsState()
. Diese Methoden sind praktische Methoden, die auf untergeordneten InteractionSource
APIs basieren. In einigen Fällen kann es sinnvoll sein, diese untergeordneten Funktionen direkt zu verwenden.
Angenommen, Sie müssen auch wissen, ob eine Schaltfläche gedrückt wird und ob sie gezogen wird. Wenn Sie sowohl collectIsPressedAsState()
als auch collectIsDraggedAsState()
verwenden, werden bei der Funktion „Compose“ viele doppelte Arbeitsschritte ausgeführt und es kann nicht garantiert werden, dass alle Interaktionen in der richtigen Reihenfolge angezeigt werden. In solchen Fällen können Sie direkt mit der InteractionSource
arbeiten. Weitere Informationen zum Tracking der Interaktionen mit InteractionSource
finden Sie unter Mit InteractionSource
arbeiten.
Im folgenden Abschnitt wird beschrieben, wie Interaktionen mit InteractionSource
bzw. MutableInteractionSource
aufgenommen und ausgegeben werden.
Interaction
abrufen und ausgeben
InteractionSource
stellt einen schreibgeschützten Stream von Interactions
dar. Ein Interaction
kann nicht an ein InteractionSource
gesendet werden. Zum Ausgeben von Interaction
s müssen Sie eine MutableInteractionSource
verwenden, die über InteractionSource
erweitert wird.
Modifikatoren und Komponenten können Interactions
verarbeiten, ausgeben oder ausgeben und ausgeben.
In den folgenden Abschnitten wird beschrieben, wie Interaktionen von Modifikatoren und Komponenten übernommen und ausgegeben werden.
Beispiel für einen Konsummodifikator
Bei einem Modifikator, der einen Rahmen für den fokussierten Zustand zeichnet, müssen Sie nur Interactions
beobachten, sodass Sie ein InteractionSource
akzeptieren können:
fun Modifier.focusBorder(interactionSource: InteractionSource): Modifier { // ... }
Aus der Funktionssignatur geht klar hervor, dass dieser Modifikator ein Nutzer ist. Er kann Interaction
s verbrauchen, aber nicht ausgeben.
Beispiel für produzierenden Modifikator
Für einen Modifikator, der Hover-Ereignisse wie Modifier.hoverable
verarbeitet, müssen Sie Interactions
ausgeben und stattdessen einen MutableInteractionSource
als Parameter akzeptieren:
fun Modifier.hover(interactionSource: MutableInteractionSource, enabled: Boolean): Modifier { // ... }
Dieser Modifikator ist ein Producer. Er kann den angegebenen MutableInteractionSource
verwenden, um HoverInteractions
auszugeben, wenn der Mauszeiger darauf bewegt wird oder nicht.
Komponenten erstellen, die
Allgemeine Komponenten wie ein Material-Button
fungieren sowohl als Ersteller als auch als Nutzer. Sie verarbeiten Eingabe- und Fokusereignisse und ändern auch ihr Aussehen als Reaktion auf diese Ereignisse, z. B. das Einblenden von Wellen oder das Animieren von Höhenprofilen. Daher wird MutableInteractionSource
direkt als Parameter verfügbar gemacht, sodass Sie Ihre eigene gespeicherte Instanz bereitstellen können:
@Composable fun Button( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, // exposes MutableInteractionSource as a parameter interactionSource: MutableInteractionSource? = null, elevation: ButtonElevation? = ButtonDefaults.elevatedButtonElevation(), shape: Shape = MaterialTheme.shapes.small, border: BorderStroke? = null, colors: ButtonColors = ButtonDefaults.buttonColors(), contentPadding: PaddingValues = ButtonDefaults.ContentPadding, content: @Composable RowScope.() -> Unit ) { /* content() */ }
Dadurch kann die MutableInteractionSource
aus der Komponente gezogen und alle von der Komponente erzeugten Interaction
s beobachtet werden. Damit lässt sich die Darstellung dieser Komponente oder einer anderen Komponente auf der UI steuern.
Wenn Sie Ihre eigenen interaktiven übergeordneten Komponenten erstellen, empfehlen wir, MutableInteractionSource
auf diese Weise als Parameter verfügbar zu machen. Abgesehen von den Best Practices für das Hochheben des Zustands ist dies auch das Lesen und Steuern des visuellen Zustands einer Komponente auf die gleiche Weise, wie jede andere Art von Status (z. B. aktivierter Zustand) gelesen und gesteuert werden kann.
Compose folgt einem mehrschichtigen Architekturansatz. Übergeordnete Material-Komponenten werden also auf grundlegenden Bausteinen aufgebaut, aus denen die Interaction
s erzeugt werden, die sie zum Steuern von Wellen und anderen visuellen Effekten benötigen. Die Basisbibliothek bietet allgemeine Interaktionsmodifikatoren wie Modifier.hoverable
, Modifier.focusable
und Modifier.draggable
.
Wenn Sie eine Komponente erstellen möchten, die auf Hover-Ereignisse reagiert, können Sie einfach Modifier.hoverable
verwenden und einen MutableInteractionSource
als Parameter übergeben.
Wenn der Mauszeiger auf die Komponente bewegt wird, gibt sie HoverInteraction
s aus, mit denen Sie die Darstellung der Komponente ändern können.
// This InteractionSource will emit hover interactions val interactionSource = remember { MutableInteractionSource() } Box( Modifier .size(100.dp) .hoverable(interactionSource = interactionSource), contentAlignment = Alignment.Center ) { Text("Hello!") }
Wenn Sie diese Komponente außerdem fokussierbar machen möchten, können Sie Modifier.focusable
hinzufügen und dieselbe MutableInteractionSource
als Parameter übergeben. Jetzt werden sowohl HoverInteraction.Enter/Exit
als auch FocusInteraction.Focus/Unfocus
über dieselbe MutableInteractionSource
ausgegeben. Sie können die Darstellung für beide Arten von Interaktionen an derselben Stelle anpassen:
// This InteractionSource will emit hover and focus interactions val interactionSource = remember { MutableInteractionSource() } Box( Modifier .size(100.dp) .hoverable(interactionSource = interactionSource) .focusable(interactionSource = interactionSource), contentAlignment = Alignment.Center ) { Text("Hello!") }
Modifier.clickable
ist eine noch höhere Abstraktion als hoverable
und focusable
. Damit eine Komponente anklickbar ist, muss der Mauszeiger darüber bewegt werden. Komponenten, auf die geklickt werden kann, sollten ebenfalls fokussierbar sein. Mit Modifier.clickable
können Sie eine Komponente erstellen, die Interaktionen mit dem Mauszeiger verarbeitet, fokussiert und betätigt, ohne untergeordnete APIs kombinieren zu müssen. Wenn die Komponente auch anklickbar sein soll, können Sie hoverable
und focusable
durch clickable
ersetzen:
// This InteractionSource will emit hover, focus, and press interactions val interactionSource = remember { MutableInteractionSource() } Box( Modifier .size(100.dp) .clickable( onClick = {}, interactionSource = interactionSource, // Also show a ripple effect indication = ripple() ), contentAlignment = Alignment.Center ) { Text("Hello!") }
Mit InteractionSource
arbeiten
Wenn Sie allgemeine Informationen zu Interaktionen mit einer Komponente benötigen, können Sie standardmäßige Flow APIs für die InteractionSource
dieser Komponente verwenden.
Angenommen, Sie möchten eine Liste der Presse- und Drag-Interaktionen für ein InteractionSource
verwalten. Dieser Code erledigt die Hälfte der Arbeit, d. h., die neuen
Vorgänge werden der Liste hinzugefügt:
val interactionSource = remember { MutableInteractionSource() } val interactions = remember { mutableStateListOf<Interaction>() } LaunchedEffect(interactionSource) { interactionSource.interactions.collect { interaction -> when (interaction) { is PressInteraction.Press -> { interactions.add(interaction) } is DragInteraction.Start -> { interactions.add(interaction) } } } }
Allerdings müssen Sie nicht nur die neuen Interaktionen hinzufügen, sondern auch Interaktionen am Ende entfernen (z. B. wenn der Nutzer den Finger von der Komponente hebt). Das ist einfach, da die Endinteraktionen immer einen Verweis auf die verknüpfte Startinteraktion enthalten. Dieser Code zeigt, wie Sie die beendeten Interaktionen entfernen:
val interactionSource = remember { MutableInteractionSource() } val interactions = remember { mutableStateListOf<Interaction>() } LaunchedEffect(interactionSource) { interactionSource.interactions.collect { interaction -> when (interaction) { is PressInteraction.Press -> { interactions.add(interaction) } is PressInteraction.Release -> { interactions.remove(interaction.press) } is PressInteraction.Cancel -> { interactions.remove(interaction.press) } is DragInteraction.Start -> { interactions.add(interaction) } is DragInteraction.Stop -> { interactions.remove(interaction.start) } is DragInteraction.Cancel -> { interactions.remove(interaction.start) } } } }
Wenn Sie wissen möchten, ob die Komponente gerade gedrückt oder gezogen wird, müssen Sie lediglich prüfen, ob interactions
leer ist:
val isPressedOrDragged = interactions.isNotEmpty()
Wenn Sie wissen möchten, was die letzte Interaktion war, schauen Sie sich einfach das letzte Element in der Liste an. Die Ripple-Implementierung von „Compose“ findet beispielsweise das passende Status-Overlay für die letzte Interaktion:
val lastInteraction = when (interactions.lastOrNull()) { is DragInteraction.Start -> "Dragged" is PressInteraction.Press -> "Pressed" else -> "No state" }
Da alle Interaction
s derselben Struktur folgen, gibt es keinen großen Unterschied im Code, wenn Sie mit verschiedenen Arten von Nutzerinteraktionen arbeiten. Das Muster ist insgesamt gleich.
Die vorherigen Beispiele in diesem Abschnitt stellen die Flow
von Interaktionen mit State
dar. So lassen sich aktualisierte Werte leichter beobachten, da das Lesen des Statuswerts automatisch zu Neuzusammensetzungen führt. Die Zusammensetzung erfolgt jedoch als Batch für den Pre-Frame. Wenn sich also der Status ändert und innerhalb desselben Frames wieder zurückkehrt, ist die Änderung für Komponenten, die den Status beobachten, nicht sichtbar.
Dies ist für Interaktionen wichtig, da Interaktionen regelmäßig im selben Frame beginnen und enden können. Hier ein Beispiel vom vorherigen Beispiel mit Button
:
val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() Button(onClick = { /* do something */ }, interactionSource = interactionSource) { Text(if (isPressed) "Pressed!" else "Not pressed") }
Wenn ein Drücken im selben Frame beginnt und endet, wird der Text nie als „Gepresst!“ angezeigt. In den meisten Fällen ist das kein Problem. Wenn für einen so kurzen Zeitraum ein visueller Effekt angezeigt wird, kommt es zu Flimmern und ist für den Nutzer nicht sehr wahrnehmbar. In einigen Fällen, z. B. bei der Darstellung eines Welleneffekts oder einer ähnlichen Animation, kann es sinnvoll sein, den Effekt mindestens für einen minimalen Zeitraum zu zeigen, anstatt die Wiedergabe sofort zu stoppen, wenn Sie nicht mehr auf die Schaltfläche drücken. Dazu können Sie Animationen direkt aus dem Collect-Lambda heraus starten und stoppen, anstatt in einen Zustand zu schreiben. Ein Beispiel für dieses Muster finden Sie im Abschnitt Erweitertes Indication
mit animiertem Rahmen erstellen.
Beispiel: Build-Komponente mit benutzerdefinierter Interaktionsverwaltung
Im Folgenden finden Sie ein Beispiel für eine geänderte Schaltfläche, um zu sehen, wie Sie Komponenten mit einer benutzerdefinierten Eingabeantwort erstellen können. Angenommen, Sie möchten eine Schaltfläche, die durch Ändern ihres Aussehens auf Drücken reagiert:
Erstellen Sie dazu eine benutzerdefinierte zusammensetzbare Funktion basierend auf Button
und legen Sie fest, dass ein zusätzlicher icon
-Parameter benötigt wird, um das Symbol zu zeichnen (in diesem Fall ein Einkaufswagen). Sie rufen collectIsPressedAsState()
auf, um zu erfassen, ob der Nutzer den Mauszeiger auf die Schaltfläche bewegt. Ist dies der Fall, fügen Sie das Symbol hinzu. So sieht der Code aus:
@Composable fun PressIconButton( onClick: () -> Unit, icon: @Composable () -> Unit, text: @Composable () -> Unit, modifier: Modifier = Modifier, interactionSource: MutableInteractionSource? = null ) { val isPressed = interactionSource?.collectIsPressedAsState()?.value ?: false Button( onClick = onClick, modifier = modifier, interactionSource = interactionSource ) { AnimatedVisibility(visible = isPressed) { if (isPressed) { Row { icon() Spacer(Modifier.size(ButtonDefaults.IconSpacing)) } } } text() } }
Und so sieht es aus, wenn Sie diese neue zusammensetzbare Funktion verwenden:
PressIconButton( onClick = {}, icon = { Icon(Icons.Filled.ShoppingCart, contentDescription = null) }, text = { Text("Add to cart") } )
Da dieses neue PressIconButton
auf dem bestehenden Material Button
basiert, reagiert es wie gewohnt auf Nutzerinteraktionen. Wenn der Nutzer auf die Schaltfläche drückt, ändert sich die Deckkraft leicht, wie bei einem gewöhnlichen Material-Button
.
Mit Indication
einen wiederverwendbaren benutzerdefinierten Effekt erstellen und anwenden
In den vorherigen Abschnitten haben Sie erfahren, wie Sie einen Teil einer Komponente in Reaktion auf verschiedene Interaction
s ändern, z. B. wenn beim Drücken ein Symbol angezeigt wird. Auf die gleiche Weise können Sie den Wert von Parametern, die Sie für eine Komponente angeben, oder den in einer Komponente angezeigten Inhalt ändern. Dies gilt jedoch nur für einzelne Komponenten. Häufig hat eine Anwendung oder ein Designsystem ein generisches System für zustandsorientierte visuelle Effekte – einen Effekt, der einheitlich auf alle Komponenten angewendet werden sollte.
Wenn Sie ein solches Designsystem erstellen, kann es aus folgenden Gründen schwierig sein, eine Komponente anzupassen und diese Anpassung für andere Komponenten wiederzuverwenden:
- Jede Komponente im Designsystem benötigt denselben Textbaustein
- Es kann leicht vergessen werden, diesen Effekt auf neu erstellte Komponenten und benutzerdefinierte anklickbare Komponenten anzuwenden.
- Es kann schwierig sein, den benutzerdefinierten Effekt mit anderen Effekten zu kombinieren
Mit Indication
können Sie diese Probleme vermeiden und eine benutzerdefinierte Komponente auf Ihrem gesamten System problemlos skalieren.
Indication
steht für einen wiederverwendbaren visuellen Effekt, der auf Komponenten in einer Anwendung oder einem Designsystem angewendet werden kann. Indication
ist in zwei Teile unterteilt:
IndicationNodeFactory
: Eine Factory, dieModifier.Node
-Instanzen erstellt, die visuelle Effekte für eine Komponente rendern. Bei einfacheren Implementierungen, die sich nicht komponentenübergreifend ändern, kann dies ein Singleton (Objekt) sein, das für die gesamte Anwendung wiederverwendet wird.Diese Instanzen können zustandsorientiert oder zustandslos sein. Da sie pro Komponente erstellt werden, können sie Werte aus einer
CompositionLocal
abrufen, um ihre Darstellung oder ihr Verhalten in einer bestimmten Komponente wie bei jeder anderenModifier.Node
-Komponente zu ändern.Modifier.indication
: Ein Modifikator, derIndication
für eine Komponente zieht.Modifier.clickable
und andere Interaktionsmodifikatoren auf hoher Ebene akzeptieren einen Anzeigeparameter direkt. Sie geben also nicht nurInteraction
s aus, sondern können auch visuelle Effekte für die von ihnen ausgegebenenInteraction
s darstellen. Für einfache Fälle können Sie also einfachModifier.clickable
verwenden, ohneModifier.indication
zu benötigen.
Effekt durch ein Indication
ersetzen
In diesem Abschnitt wird beschrieben, wie ein manueller Skalierungseffekt, der auf eine bestimmte Schaltfläche angewendet wurde, durch ein Indikationsäquivalent ersetzt wird, das für mehrere Komponenten wiederverwendet werden kann.
Mit dem folgenden Code wird eine Schaltfläche erstellt, die beim Drücken nach unten skaliert wird:
val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() val scale by animateFloatAsState(targetValue = if (isPressed) 0.9f else 1f, label = "scale") Button( modifier = Modifier.scale(scale), onClick = { }, interactionSource = interactionSource ) { Text(if (isPressed) "Pressed!" else "Not pressed") }
So konvertieren Sie den Skalierungseffekt im Snippet oben in ein Indication
:
Erstellen Sie das
Modifier.Node
, mit dem der Skalierungseffekt angewendet wird. Wenn der Knoten verbunden ist, beobachtet er die Interaktionsquelle, ähnlich wie in den vorherigen Beispielen. Der einzige Unterschied besteht darin, dass Animationen direkt gestartet und nicht die eingehenden Interaktionen in den Status konvertiert werden.Der Knoten muss
DrawModifierNode
implementieren, damit erContentDrawScope#draw()
überschreiben und einen Skalierungseffekt mit denselben Zeichenbefehlen wie jede andere Grafik-API in Compose rendern kann.Wenn Sie
drawContent()
aufrufen, die vomContentDrawScope
-Empfänger verfügbar ist, wird die eigentliche Komponente gezeichnet, auf die dieIndication
angewendet werden soll. Sie müssen diese Funktion also nur innerhalb einer Skalierungstransformation aufrufen. Achte darauf, dass deineIndication
-Implementierungen an irgendeinem Punkt immerdrawContent()
aufrufen. Andernfalls wird die Komponente, auf die duIndication
anwendest, nicht gezeichnet.private class ScaleNode(private val interactionSource: InteractionSource) : Modifier.Node(), DrawModifierNode { var currentPressPosition: Offset = Offset.Zero val animatedScalePercent = Animatable(1f) private suspend fun animateToPressed(pressPosition: Offset) { currentPressPosition = pressPosition animatedScalePercent.animateTo(0.9f, spring()) } private suspend fun animateToResting() { animatedScalePercent.animateTo(1f, spring()) } override fun onAttach() { coroutineScope.launch { interactionSource.interactions.collectLatest { interaction -> when (interaction) { is PressInteraction.Press -> animateToPressed(interaction.pressPosition) is PressInteraction.Release -> animateToResting() is PressInteraction.Cancel -> animateToResting() } } } } override fun ContentDrawScope.draw() { scale( scale = animatedScalePercent.value, pivot = currentPressPosition ) { this@draw.drawContent() } } }
Erstellen Sie die
IndicationNodeFactory
. Seine einzige Aufgabe besteht darin, eine neue Knoteninstanz für eine bereitgestellte Interaktionsquelle zu erstellen. Da es keine Parameter zum Konfigurieren der Anzeige gibt, kann die Factory ein Objekt sein:object ScaleIndication : IndicationNodeFactory { override fun create(interactionSource: InteractionSource): DelegatableNode { return ScaleNode(interactionSource) } override fun equals(other: Any?): Boolean = other === ScaleIndication override fun hashCode() = 100 }
Modifier.clickable
verwendet internModifier.indication
. Um eine anklickbare Komponente mitScaleIndication
zu erstellen, müssen Sie lediglichIndication
als Parameter fürclickable
angeben:Box( modifier = Modifier .size(100.dp) .clickable( onClick = {}, indication = ScaleIndication, interactionSource = null ) .background(Color.Blue), contentAlignment = Alignment.Center ) { Text("Hello!", color = Color.White) }
Dies vereinfacht auch das Erstellen wiederverwendbarer übergeordneter Komponenten mit einem benutzerdefinierten
Indication
– eine Schaltfläche könnte so aussehen:@Composable fun ScaleButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource? = null, shape: Shape = CircleShape, content: @Composable RowScope.() -> Unit ) { Row( modifier = modifier .defaultMinSize(minWidth = 76.dp, minHeight = 48.dp) .clickable( enabled = enabled, indication = ScaleIndication, interactionSource = interactionSource, onClick = onClick ) .border(width = 2.dp, color = Color.Blue, shape = shape) .padding(horizontal = 16.dp, vertical = 8.dp), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, content = content ) }
Sie können die Schaltfläche dann wie folgt verwenden:
ScaleButton(onClick = {}) { Icon(Icons.Filled.ShoppingCart, "") Spacer(Modifier.padding(10.dp)) Text(text = "Add to cart!") }
Erweiterte Indication
mit animiertem Rahmen erstellen
Indication
ist nicht nur auf Transformationseffekte wie die Skalierung einer Komponente beschränkt. Da IndicationNodeFactory
ein Modifier.Node
zurückgibt, können Sie wie bei anderen Zeichen-APIs beliebige Effekte über oder unter dem Inhalt zeichnen. Sie können beispielsweise einen animierten Rahmen um die Komponente und ein Overlay auf der Komponente zeichnen, wenn sie gedrückt wird:
Die Indication
-Implementierung ist dem vorherigen Beispiel sehr ähnlich. Sie erstellt lediglich einen Knoten mit einigen Parametern. Da der animierte Rahmen von der Form und dem Rahmen der Komponente abhängt, für die Indication
verwendet wird, müssen für die Indication
-Implementierung auch Form und Rahmenbreite als Parameter angegeben werden:
data class NeonIndication(private val shape: Shape, private val borderWidth: Dp) : IndicationNodeFactory { override fun create(interactionSource: InteractionSource): DelegatableNode { return NeonNode( shape, // Double the border size for a stronger press effect borderWidth * 2, interactionSource ) } }
Die Modifier.Node
-Implementierung ist außerdem vom Konzept her identisch, auch wenn der Zeichencode komplizierter ist. Wie zuvor beobachtet sie InteractionSource
, wenn sie angehängt wird, startet Animationen und implementiert DrawModifierNode
, um den Effekt über den Inhalt zu ziehen:
private class NeonNode( private val shape: Shape, private val borderWidth: Dp, private val interactionSource: InteractionSource ) : Modifier.Node(), DrawModifierNode { var currentPressPosition: Offset = Offset.Zero val animatedProgress = Animatable(0f) val animatedPressAlpha = Animatable(1f) var pressedAnimation: Job? = null var restingAnimation: Job? = null private suspend fun animateToPressed(pressPosition: Offset) { // Finish any existing animations, in case of a new press while we are still showing // an animation for a previous one restingAnimation?.cancel() pressedAnimation?.cancel() pressedAnimation = coroutineScope.launch { currentPressPosition = pressPosition animatedPressAlpha.snapTo(1f) animatedProgress.snapTo(0f) animatedProgress.animateTo(1f, tween(450)) } } private fun animateToResting() { restingAnimation = coroutineScope.launch { // Wait for the existing press animation to finish if it is still ongoing pressedAnimation?.join() animatedPressAlpha.animateTo(0f, tween(250)) animatedProgress.snapTo(0f) } } override fun onAttach() { coroutineScope.launch { interactionSource.interactions.collect { interaction -> when (interaction) { is PressInteraction.Press -> animateToPressed(interaction.pressPosition) is PressInteraction.Release -> animateToResting() is PressInteraction.Cancel -> animateToResting() } } } } override fun ContentDrawScope.draw() { val (startPosition, endPosition) = calculateGradientStartAndEndFromPressPosition( currentPressPosition, size ) val brush = animateBrush( startPosition = startPosition, endPosition = endPosition, progress = animatedProgress.value ) val alpha = animatedPressAlpha.value drawContent() val outline = shape.createOutline(size, layoutDirection, this) // Draw overlay on top of content drawOutline( outline = outline, brush = brush, alpha = alpha * 0.1f ) // Draw border on top of overlay drawOutline( outline = outline, brush = brush, alpha = alpha, style = Stroke(width = borderWidth.toPx()) ) } /** * Calculates a gradient start / end where start is the point on the bounding rectangle of * size [size] that intercepts with the line drawn from the center to [pressPosition], * and end is the intercept on the opposite end of that line. */ private fun calculateGradientStartAndEndFromPressPosition( pressPosition: Offset, size: Size ): Pair<Offset, Offset> { // Convert to offset from the center val offset = pressPosition - size.center // y = mx + c, c is 0, so just test for x and y to see where the intercept is val gradient = offset.y / offset.x // We are starting from the center, so halve the width and height - convert the sign // to match the offset val width = (size.width / 2f) * sign(offset.x) val height = (size.height / 2f) * sign(offset.y) val x = height / gradient val y = gradient * width // Figure out which intercept lies within bounds val intercept = if (abs(y) <= abs(height)) { Offset(width, y) } else { Offset(x, height) } // Convert back to offsets from 0,0 val start = intercept + size.center val end = Offset(size.width - start.x, size.height - start.y) return start to end } private fun animateBrush( startPosition: Offset, endPosition: Offset, progress: Float ): Brush { if (progress == 0f) return TransparentBrush // This is *expensive* - we are doing a lot of allocations on each animation frame. To // recreate a similar effect in a performant way, it would be better to create one large // gradient and translate it on each frame, instead of creating a whole new gradient // and shader. The current approach will be janky! val colorStops = buildList { when { progress < 1 / 6f -> { val adjustedProgress = progress * 6f add(0f to Blue) add(adjustedProgress to Color.Transparent) } progress < 2 / 6f -> { val adjustedProgress = (progress - 1 / 6f) * 6f add(0f to Purple) add(adjustedProgress * MaxBlueStop to Blue) add(adjustedProgress to Blue) add(1f to Color.Transparent) } progress < 3 / 6f -> { val adjustedProgress = (progress - 2 / 6f) * 6f add(0f to Pink) add(adjustedProgress * MaxPurpleStop to Purple) add(MaxBlueStop to Blue) add(1f to Blue) } progress < 4 / 6f -> { val adjustedProgress = (progress - 3 / 6f) * 6f add(0f to Orange) add(adjustedProgress * MaxPinkStop to Pink) add(MaxPurpleStop to Purple) add(MaxBlueStop to Blue) add(1f to Blue) } progress < 5 / 6f -> { val adjustedProgress = (progress - 4 / 6f) * 6f add(0f to Yellow) add(adjustedProgress * MaxOrangeStop to Orange) add(MaxPinkStop to Pink) add(MaxPurpleStop to Purple) add(MaxBlueStop to Blue) add(1f to Blue) } else -> { val adjustedProgress = (progress - 5 / 6f) * 6f add(0f to Yellow) add(adjustedProgress * MaxYellowStop to Yellow) add(MaxOrangeStop to Orange) add(MaxPinkStop to Pink) add(MaxPurpleStop to Purple) add(MaxBlueStop to Blue) add(1f to Blue) } } } return linearGradient( colorStops = colorStops.toTypedArray(), start = startPosition, end = endPosition ) } companion object { val TransparentBrush = SolidColor(Color.Transparent) val Blue = Color(0xFF30C0D8) val Purple = Color(0xFF7848A8) val Pink = Color(0xFFF03078) val Orange = Color(0xFFF07800) val Yellow = Color(0xFFF0D800) const val MaxYellowStop = 0.16f const val MaxOrangeStop = 0.33f const val MaxPinkStop = 0.5f const val MaxPurpleStop = 0.67f const val MaxBlueStop = 0.83f } }
Der Hauptunterschied besteht darin, dass es jetzt eine Mindestdauer für die Animation mit der Funktion animateToResting()
gibt. Das heißt, selbst wenn das Drücken der Taste sofort loslässt, wird die Animation fortgesetzt. Auch mehrere schnelles Drücken wird zu Beginn von animateToPressed
ausgeführt. Wenn ein Drücken während einer vorhandenen „Betätigen“- oder „Pause“-Animation erfolgt, wird die vorherige Animation abgebrochen und die Drücken-Animation von vorn beginnen. Um mehrere Effekte gleichzeitig zu unterstützen (z. B. bei Wellen, bei denen eine neue Wellenanimation auf anderen Wellen gezeichnet wird), können Sie die Animationen in einer Liste verfolgen, anstatt vorhandene Animationen zu verwerfen und neue zu starten.
Empfehlungen für dich
- Hinweis: Der Linktext wird angezeigt, wenn JavaScript deaktiviert ist.
- Gesten und Bewegungen
- Kotlin für Jetpack Compose
- Materialkomponenten und Layouts