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">
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
Interactions 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 Interactions 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 Interactions. 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 HoverInteractions 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 Interactions 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 Interactions, 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
CompositionLocalabrufen, um zu ändern, wie bei anderen Komponenten auch innerhalb einer bestimmten KomponenteModifier.Node.Modifier.indication: Ein Modifikator, mit demIndicationfür ein Komponente.Modifier.clickableund andere allgemeine Interaktionsmodifikatoren akzeptieren einen Indication-Parameter direkt, sodass nicht nur DatenInteractions, kann aber auch visuelle Effekte fürInteractions zeichnen, die sie emit aus. In einfachen Fällen können Sie alsoModifier.clickableohne derModifier.indicationbenö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.Nodefü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
DrawModifierNodeimplementieren, 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 dieIndicationangewendet werden soll. Sie müssen also diese Funktion innerhalb einer Skalierungstransformation aufrufen. Achten Sie darauf, Implementierungen vonIndicationrufen irgendwann immerdrawContent()auf. Andernfalls wird die Komponente, auf die SieIndicationanwenden, 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.clickableverwendetModifier.indicationintern. Komponente mitScaleIndicationklicken, müssen Sie nur die SchaltflächeIndicationals 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 erstellte Schaltfläche.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:
Indication gezeichnet wurde.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