Komponenten der Benutzeroberfläche geben Gerätenutzern Feedback, auf Interaktionen von Nutzenden reagieren können. Jede Komponente reagiert auf ihre eigenen Interaktionen, sodass die Nutzenden wissen, was ihre Interaktionen tun. Für Wenn eine nutzende Person eine Schaltfläche auf dem Touchscreen eines Geräts berührt, in irgendeiner Weise verändert, z. B. durch Hinzufügen einer Hervorhebungsfarbe. Diese Änderung informiert die Nutzenden darüber, dass sie die Schaltfläche berührt haben. Wenn die Nutzenden keine Aktionen damit sie wissen, dass sie ihren Finger von der Schaltfläche weg ziehen müssen, andernfalls wird die Taste aktiviert.
<ph type="x-smartling-placeholder"> <ph type="x-smartling-placeholder">Dokumentation zum Schreiben von Gesten behandelt, wie Zusammensetzungskomponenten verarbeiten Zeigerereignisse auf unterer Ebene, wie Zeigerbewegungen und Klicks. Die Funktion „Compose“ abstrahiert diese Low-Level-Ereignisse in Interaktionen auf höherer Ebene – z. B. kann eine Reihe von Zeigerereignissen durch Drücken und Loslassen der Taste. Das Verständnis dieser übergeordneten Abstraktionen kann können Sie anpassen, wie Ihre UI auf Nutzende reagiert. Vielleicht möchten Sie zum Beispiel um anzupassen, wie sich das Erscheinungsbild einer Komponente ändert, wenn der Nutzer mit dem oder einfach nur ein Protokoll dieser Nutzeraktionen führen. Dieses Dokument enthält die Informationen, die Sie benötigen, um die Standard-UI-Elemente, oder eigene Designs erstellen.
<ph type="x-smartling-placeholder">Interaktionen
Häufig reicht es nicht aus, nur die Funktion der Komponente zum Schreiben zu kennen.
die Interpretation der
Nutzerinteraktionen. Zum Beispiel stützt sich Button
auf
Modifier.clickable
um herauszufinden, ob die Nutzenden
auf die Schaltfläche geklickt haben. Wenn Sie eine typische
zu deiner App hinzuzufügen, kannst du den onClick
-Code der Schaltfläche definieren und
Modifier.clickable
führt diesen Code gegebenenfalls aus. Das bedeutet, dass Sie keine
ob die Nutzenden auf den Bildschirm getippt oder die Schaltfläche
Tastatur; Modifier.clickable
stellt fest, dass der Nutzer einen Klick ausgeführt hat, und
mit deinem onClick
-Code antwortet.
Wenn Sie jedoch die Reaktion Ihrer UI-Komponente auf das Nutzerverhalten anpassen möchten, müssen Sie vielleicht mehr über das Geschehen im Hintergrund wissen. In diesem Abschnitt erhalten Sie einige dieser Informationen.
Wenn ein Nutzer mit einer UI-Komponente interagiert, repräsentiert das System sein Verhalten
indem eine Reihe von
Interaction
Ereignisse. Wenn ein Nutzer beispielsweise eine Schaltfläche berührt, generiert die Schaltfläche
PressInteraction.Press
Wenn Nutzende den Finger in die Taste heben, wird ein
PressInteraction.Release
,
um der Schaltfläche mitzuteilen, dass der Klick beendet wurde. Wenn andererseits der
Nutzende mit dem Finger aus der Schaltfläche heraus und hebt sie dann an,
generieren
PressInteraction.Cancel
,
, um anzuzeigen, dass das Drücken auf die Schaltfläche abgebrochen und nicht abgeschlossen wurde.
Diese Interaktionen sind unbeeindruckend. Das heißt, diese Low-Level-Interaktionen die Bedeutung der Nutzeraktionen oder ihre Sequenz hinzufügen. Sie berücksichtigen auch nicht, welche Nutzeraktionen Vorrang vor anderen Aktionen.
Diese Interaktionen gehen in der Regel paarweise, es gibt einen Anfang und ein Ende. Die zweite
Interaktion enthält einen Verweis auf die erste Interaktion. Wenn ein Nutzer z. B.
eine Taste berühren und dann den Finger heben, erzeugt die Berührung eine
PressInteraction.Press
Interaktion und die Veröffentlichung generiert eine
PressInteraction.Release
;
Der Release
verfügt über eine press
-Eigenschaft, die den Anfang
PressInteraction.Press
.
Sie können die Interaktionen für eine bestimmte Komponente
InteractionSource
InteractionSource
basiert auf Kotlin
, sodass Sie die darin enthaltenen Interaktionen auf dieselbe Weise erfassen können.
würden Sie mit jedem
anderen Ablauf arbeiten. Weitere Informationen zu dieser Designentscheidung
Weitere Informationen findest du im Blogpost Illuminating Interactions.
Interaktionsstatus
Sie können die integrierte Funktionalität Ihrer Komponenten auch durch
die Interaktionen selbst nachverfolgen können. Vielleicht möchten Sie eine Schaltfläche,
die Farbe ändern, wenn darauf
gedrückt wird. Am einfachsten lassen sich Interaktionen verfolgen,
den entsprechenden Interaktionsstatus beobachten. InteractionSource
bietet eine Nummer an
die verschiedene Interaktionsstatus als Zustand zeigen. Wenn beispielsweise
um zu sehen, ob eine bestimmte Schaltfläche
gedrückt wurde, können Sie deren
InteractionSource.collectIsPressedAsState()
:
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 Compose auch
collectIsFocusedAsState()
, collectIsDraggedAsState()
und
collectIsHoveredAsState()
. Diese Methoden sind eigentlich Convenience-Methoden
basierend auf untergeordneten InteractionSource
-APIs. In einigen Fällen können Sie
diese untergeordneten Funktionen
direkt verwenden möchten.
Angenommen, Sie müssen wissen, ob eine Taste gedrückt wird, und
auch beim Ziehen. Wenn Sie beide collectIsPressedAsState()
verwenden
und collectIsDraggedAsState()
, „Schreiben“ macht viele doppelte Einträge.
können wir nicht garantieren,
dass alle Interaktionen in der richtigen Reihenfolge stattfinden. Für
ist es vielleicht sinnvoll, direkt mit dem
InteractionSource
Weitere Informationen zum Tracking der Interaktionen
Sie selbst mit InteractionSource
finden Sie unter Mit InteractionSource
arbeiten.
Im folgenden Abschnitt wird beschrieben, wie Sie Interaktionen mit
InteractionSource
bzw. MutableInteractionSource
.
Interaction
verbrauchen und ausgeben
InteractionSource
steht für einen schreibgeschützten Stream von Interactions
.
Interaction
an InteractionSource
ausgeben. Ausstrahlen
Interaction
s müssen Sie eine MutableInteractionSource
verwenden, die von
InteractionSource
Modifikatoren und Komponenten können Interactions
verbrauchen, ausgeben oder verbrauchen und ausgeben.
In den folgenden Abschnitten wird beschrieben, wie Interaktionen von beiden
Modifikatoren und Komponenten.
Beispiel für die Nutzung des Modifikators
Bei einem Modifikator, der einen Rahmen für den fokussierten Zustand zeichnet, müssen Sie nur
Interactions
, damit Sie InteractionSource
akzeptieren können:
fun Modifier.focusBorder(interactionSource: InteractionSource): Modifier { // ... }
Aus der Funktionssignatur geht klar hervor, dass es sich bei diesem Modifikator um einen Nutzer handelt.
kann Interaction
s verbrauchen, aber nicht ausgeben.
Beispiel für Produktionsmodifikator
Für einen Modifikator, der Hover-Ereignisse wie Modifier.hoverable
verarbeitet, geben Sie
müssen Interactions
ausgeben und MutableInteractionSource
als
Parameter:
fun Modifier.hover(interactionSource: MutableInteractionSource, enabled: Boolean): Modifier { // ... }
Dieser Modifikator ist ein Ersteller – er kann den angegebenen
MutableInteractionSource
, um HoverInteractions
auszugeben, wenn der Mauszeiger darauf bewegt wird, oder
ohne Mausbewegung.
Komponenten erstellen, die verbrauchen und
Übergeordnete Komponenten wie Material-Button
fungieren sowohl als Produzenten als auch
Verbraucher:innen. Sie können Eingabe- und Fokusereignisse verarbeiten und auch ihr Aussehen ändern.
etwa als Reaktion auf solche Ereignisse, z. B. das Darstellen von Wellen oder
Höhe über dem Meeresspiegel. Daher wird MutableInteractionSource
direkt als
verwenden, damit Sie Ihre eigene gespeicherte Instanz angeben 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() */ }
Dies ermöglicht das Anheben der
MutableInteractionSource
der Komponente benötigen und alle
Von der Komponente erzeugte Interaction
s. Hiermit können Sie die
oder einer anderen Komponente in Ihrer Benutzeroberfläche.
Wenn Sie Ihre eigenen interaktiven Komponenten auf hoher Ebene erstellen,
dass Sie MutableInteractionSource
auf diese Weise als Parameter verfügbar machen. Neben
gemäß den Best Practices für das Winden, erleichtert das Lesen und
den visuellen Zustand einer Komponente auf die gleiche Weise steuern wie
(z. B. der aktivierte Status) gelesen und gesteuert werden kann.
Compose folgt einer mehrstufigen Architektur,
sodass hochwertige Material-Komponenten auf einem Fundament
Blöcke, die die Interaction
erzeugen, die sie zur Steuerung von Ripples und anderen Elementen benötigen
visuelle Effekte. Die Foundation Library bietet allgemeine Interaktionsmodifikatoren
z. B. Modifier.hoverable
, Modifier.focusable
und
Modifier.draggable
Um eine Komponente zu erstellen, die auf Hover-Ereignisse reagiert, können Sie einfach
Modifier.hoverable
und übergeben Sie MutableInteractionSource
als Parameter.
Wenn der Mauszeiger auf die Komponente bewegt wird, werden HoverInteraction
s ausgegeben. Sie können
um die Darstellung der Komponente zu ändern.
// This InteractionSource will emit hover interactions val interactionSource = remember { MutableInteractionSource() } Box( Modifier .size(100.dp) .hoverable(interactionSource = interactionSource), contentAlignment = Alignment.Center ) { Text("Hello!") }
Um diese Komponente auch fokussierbar zu machen, können Sie Modifier.focusable
hinzufügen und
denselben MutableInteractionSource
wie ein Parameter. Jetzt haben beide
HoverInteraction.Enter/Exit
und FocusInteraction.Focus/Unfocus
wurden ausgegeben
MutableInteractionSource
und Sie können den
Darstellung für beide Interaktionsarten an derselben Stelle:
// 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 ein noch höherer
als hoverable
und focusable
, damit eine Komponente
anklickbar ist, sie ist implizit
schwebbar und Komponenten, auf die geklickt werden kann, sollten
auch fokussierbar sein. Mit Modifier.clickable
können Sie eine Komponente erstellen,
ermöglicht die Interaktion mit dem Mauszeiger,
Fokus und Drücken, ohne dass niedrigere
und APIs auf unterschiedlicher Ebene. Wenn Ihre Komponente auch anklickbar sein soll, können Sie
Ersetzen Sie hoverable
und focusable
durch clickable
:
// 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 Low-Level-Informationen zu den Interaktionen mit einer Komponente benötigen, können Sie
Sie verwenden standardmäßige Flow APIs für die InteractionSource
dieser Komponente.
Angenommen, Sie möchten eine Liste mit den Tasten
Interaktionen für ein InteractionSource
. Dieser Code macht die halbe Arbeit,
sobald sie eintreffen:
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) } } } }
Neben den neuen Interaktionen müssen Sie aber auch Interaktionen entfernen, wenn sie beendet werden (z. B. wenn der Nutzer den Finger vom Komponente). Das ist einfach, da die Endinteraktionen immer einen auf die zugehörige Startinteraktion. In diesem Code sehen Sie, wie Sie Beendete Interaktionen:
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,
Sie müssen nur prüfen, ob interactions
leer ist:
val isPressedOrDragged = interactions.isNotEmpty()
Wenn Sie wissen möchten, was die letzte Interaktion war, schauen Sie sich die Element in der Liste. So wird beispielsweise die Compose-Ripple-Implementierung ermittelt, welches Status-Overlay für die letzte Interaktion verwendet werden soll:
val lastInteraction = when (interactions.lastOrNull()) { is DragInteraction.Start -> "Dragged" is PressInteraction.Press -> "Pressed" else -> "No state" }
Da alle Interaction
s der gleichen Struktur folgen, gibt es kaum eine
Unterschiede im Code bei verschiedenen Arten von Nutzerinteraktionen –
Muster gleich.
Die vorherigen Beispiele in diesem Abschnitt stellen die Flow
von
Interaktionen mit State
So lassen sich aktualisierte Werte leicht beobachten,
da das Lesen des Statuswerts
automatisch zu einer Neuzusammensetzung führt. Sie können jedoch
auf einem Pre-Frame in einem Batch basiert. Das heißt, wenn sich der Status ändert
innerhalb desselben Frames zurückwechselt, werden Komponenten, die den Zustand beobachten,
sehen Sie die Änderung.
Das ist wichtig für Interaktionen, da Interaktionen regelmäßig beginnen und enden können.
innerhalb desselben Frames. Verwenden Sie beispielsweise das vorherige 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
„Gedrückt!“ In den meisten Fällen stellt dies kein Problem dar, sondern zeigt einen visuellen Effekt für die
führt zu einem so kurzen Zeitraum zum Flackern.
für die Nutzenden erkennbar sind. In einigen Fällen kann es zu Welleneffekten oder
Animationen ähnlich aussehen, sollten Sie den Effekt mindestens
anstatt sofort anzuhalten, wenn die Taste nicht mehr gedrückt wird. Bis
können Sie Animationen direkt in der Sammlung starten und stoppen.
anstatt in einen Zustand zu schreiben. Ein Beispiel für dieses Muster finden Sie
im Bereich Erweiterte Indication
mit animiertem Rahmen erstellen
Beispiel: Build-Komponente mit benutzerdefinierter Interaktionsbehandlung
Im Folgenden wird gezeigt, wie Sie Komponenten mit einer benutzerdefinierten Eingabeantwort erstellen können. Beispiel für eine geänderte Schaltfläche. Angenommen, Sie möchten eine Schaltfläche, reagiert auf das Drücken der Betätigung, indem er sein Aussehen ändert:
<ph type="x-smartling-placeholder">Erstellen Sie dazu eine benutzerdefinierte zusammensetzbare Funktion auf Basis von Button
und lassen Sie
Zusätzlichen icon
-Parameter zum Zeichnen des Symbols (in diesem Fall einen Einkaufswagen). Ich
collectIsPressedAsState()
aufrufen, um zu verfolgen, ob der Nutzer den Mauszeiger auf das
Schaltfläche; wenn dies der Fall ist, 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, die neue zusammensetzbare Funktion zu verwenden:
PressIconButton( onClick = {}, icon = { Icon(Icons.Filled.ShoppingCart, contentDescription = null) }, text = { Text("Add to cart") } )
Weil dieses neue PressIconButton
auf dem vorhandenen Material aufbaut
Button
, reagiert es wie gewohnt auf Nutzerinteraktionen. Wenn Nutzende
klickt, ändert sich die Deckkraft wie bei einer gewöhnlichen
Material: Button
.
Mit Indication
einen wiederverwendbaren benutzerdefinierten Effekt erstellen und anwenden
In den vorherigen Abschnitten haben Sie gelernt, wie Sie einen Teil einer Komponente als Reaktion
in verschiedene Interaction
s, z. B. Anzeige eines Symbols, wenn gedrückt wird. Das Gleiche
kann verwendet werden, um den Wert der Parameter zu ändern, die Sie einem
oder den in einer Komponente angezeigten Inhalt ändern. Dies ist jedoch
Gilt nur für die einzelnen Komponenten. Oft wird ein Anwendungs- oder Designsystem
haben wir ein generisches System
für zustandsorientierte visuelle Effekte.
einheitlich auf alle Komponenten angewendet werden.
Wenn Sie ein solches Designsystem erstellen, die Wiederverwendung dieser Anpassung für andere Komponenten schwierig für den folgenden Gründen:
- Jede Komponente im Designsystem benötigt denselben Standardcode.
- Es kann leicht vergessen werden, diesen Effekt auf neu erstellte Komponenten und anklickbare Komponenten
- Es kann schwierig sein, den benutzerdefinierten Effekt mit anderen Effekten zu kombinieren
Um diese Probleme zu vermeiden und eine benutzerdefinierte Komponente einfach
für Ihr gesamtes System zu skalieren,
können Sie Indication
verwenden.
Indication
steht für einen wiederverwendbaren visuellen Effekt, der auf allen
Komponenten in einem Anwendungs- oder Designsystem. Indication
ist in zwei Teile geteilt
Teile:
IndicationNodeFactory
: Eine Factory, dieModifier.Node
-Instanzen erstellt, die visuelle Effekte für eine Komponente zu rendern. Für einfachere Implementierungen, die keine komponentenübergreifend ändern, kann dies ein Singleton (Objekt) sein und in der gesamten Anwendung.Diese Instanzen können zustandsorientiert oder zustandslos sein. Da sie gemäß Komponente können sie Werte von einem
CompositionLocal
abrufen, um zu ändern, wie bei anderen Komponenten auch innerhalb einer bestimmten KomponenteModifier.Node
.Modifier.indication
: Ein Modifikator, mit demIndication
für ein Komponente.Modifier.clickable
und andere allgemeine Interaktionsmodifikatoren akzeptieren einen Indication-Parameter direkt, sodass nicht nur DatenInteraction
s, kann aber auch visuelle Effekte fürInteraction
s zeichnen, die sie emit aus. In einfachen Fällen können Sie alsoModifier.clickable
ohne derModifier.indication
benötigt.
Effekt durch Indication
ersetzen
In diesem Abschnitt wird beschrieben, wie Sie einen manuellen Skalierungseffekt ersetzen, der auf einen spezielle Schaltfläche mit einer entsprechenden Kennzeichnung, die in mehreren Komponenten.
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") }
Um den Skaleneffekt im Snippet oben in ein Indication
zu konvertieren, folgen Sie
diese Schritte:
Erstellen Sie die
Modifier.Node
für die Anwendung des Skalierungseffekts. Anschließend beobachtet der Knoten die Interaktionsquelle, ähnlich wie Beispiele. Der einzige Unterschied besteht darin, dass Animationen direkt gestartet werden, anstatt die eingehenden Interactions in Zustand umzuwandeln.Der Knoten muss
DrawModifierNode
implementieren, damit er überschreiben kannContentDrawScope#draw()
erstellen und mit derselben Zeichnung einen Skalierungseffekt rendern wie bei allen anderen Grafik-APIs in Compose.Wenn du
drawContent()
über denContentDrawScope
-Empfänger anrufen kannst, wird die eigentliche Komponente, auf die dieIndication
angewendet werden soll. Sie müssen also diese Funktion innerhalb einer Skalierungstransformation aufrufen. Achten Sie darauf, Implementierungen vonIndication
rufen irgendwann immerdrawContent()
auf. Andernfalls wird die Komponente, auf die SieIndication
anwenden, 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
. Die einzige Aufgabe besteht darin, Neue Knoteninstanz für eine angegebene Interaktionsquelle. Da es keine Parameter zur Konfiguration der Angabe verwenden, 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
verwendetModifier.indication
intern. Komponente mitScaleIndication
klicken, müssen Sie nur die SchaltflächeIndication
als Parameter fürclickable
:Box( modifier = Modifier .size(100.dp) .clickable( onClick = {}, indication = ScaleIndication, interactionSource = null ) .background(Color.Blue), contentAlignment = Alignment.Center ) { Text("Hello!", color = Color.White) }
Dies erleichtert auch die Erstellung hochwertiger, wiederverwendbarer Komponenten mithilfe einer 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 ) }
Anschließend können Sie die Schaltfläche folgendermaßen verwenden:
ScaleButton(onClick = {}) { Icon(Icons.Filled.ShoppingCart, "") Spacer(Modifier.padding(10.dp)) Text(text = "Add to cart!") }
Indication
mit animiertem Rahmen erstellen
Indication
ist nicht nur auf Transformationseffekte beschränkt, wie z. B. die Skalierung eines
Komponente. Da IndicationNodeFactory
ein Modifier.Node
zurückgibt, können Sie
Effekten über oder unter dem Inhalt, wie bei anderen Zeichen-APIs. Für
können Sie einen animierten Rahmen um die Komponente und ein Overlay auf
über der Komponente, wenn sie gedrückt wird:
Die Indication
-Implementierung hier ist dem vorherigen Beispiel sehr ähnlich:
Es wird lediglich ein Knoten mit
einigen Parametern erstellt. Da der animierte Rahmen
an der Form und am Rahmen der Komponente, für die das Indication
verwendet wird,
Bei der Implementierung von Indication
müssen außerdem Form und Rahmenbreite angegeben werden
als Parameter verwenden:
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 auch konzeptionell gleich, selbst wenn die
ist etwas komplizierter. Wie zuvor wird InteractionSource
Nach dem Anhängen werden Animationen gestartet und DrawModifierNode
zum Zeichnen von
die Auswirkungen auf den Inhalt:
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
Animation mit der animateToResting()
-Funktion, sodass Sie
sofort loslassen, wird die Presseanimation fortgesetzt. Es gibt auch eine
zum mehrfachen Schnelldrücken zu Beginn von animateToPressed
– wenn eine
während einer bestehenden Animation läuft, wird die vorherige Animation
abgebrochen und die Presseanimation beginnt am Anfang. Um mehrere
gleichzeitige Effekte wie Wellen, bei denen eine neue Wellenanimation
zusätzlich zu anderen Ripples), können Sie die Animationen in einer Liste verfolgen, anstatt
bestehende Animationen abbrechen und neue starten.
Empfehlungen für dich
- Hinweis: Der Linktext wird angezeigt, wenn JavaScript deaktiviert ist.
- Touch-Gesten
- Kotlin für Jetpack Compose
- Materialkomponenten und Layouts