Umgang mit Nutzerinteraktionen

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>
Abbildung 1: Schaltflächen, die immer aktiviert sind, ohne dass das Drücken erfolgt.
<ph type="x-smartling-placeholder">
</ph>
Abbildung 2. Schaltflächen mit Ripples, die den aktivierten Status entsprechend widerspiegeln.

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">
</ph> Animation einer Schaltfläche, über die beim Anklicken dynamisch ein Einkaufswagensymbol hinzugefügt wird
Abbildung 3: Eine Schaltfläche, die beim Anklicken dynamisch ein Symbol hinzufügt.

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, die Modifier.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 Komponente Modifier.Node.

  • Modifier.indication: Ein Modifikator, mit dem Indication für ein Komponente. Modifier.clickable und andere allgemeine Interaktionsmodifikatoren akzeptieren einen Indication-Parameter direkt, sodass nicht nur Daten Interactions, kann aber auch visuelle Effekte für Interactions zeichnen, die sie emit aus. In einfachen Fällen können Sie also Modifier.clickable ohne der Modifier.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:

  1. 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 kann ContentDrawScope#draw() erstellen und mit derselben Zeichnung einen Skalierungseffekt rendern wie bei allen anderen Grafik-APIs in Compose.

    Wenn du drawContent() über den ContentDrawScope-Empfänger anrufen kannst, wird die eigentliche Komponente, auf die die Indication angewendet werden soll. Sie müssen also diese Funktion innerhalb einer Skalierungstransformation aufrufen. Achten Sie darauf, Implementierungen von Indication rufen irgendwann immer drawContent() auf. Andernfalls wird die Komponente, auf die Sie Indication 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()
            }
        }
    }

  2. 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
    }

  3. Modifier.clickable verwendet Modifier.indication intern. Komponente mit ScaleIndication klicken, müssen Sie nur die Schaltfläche Indication als Parameter für clickable:

    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!")
}

<ph type="x-smartling-placeholder">
</ph> Animation einer Schaltfläche mit einem Einkaufswagensymbol, das beim Drücken kleiner wird
Abbildung 4: Eine mit einem benutzerdefinierten 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:

<ph type="x-smartling-placeholder">
</ph> Eine Schaltfläche mit einem fantasievollen Regenbogeneffekt beim Drücken
Abbildung 5: Ein animierter Rahmeneffekt, der mit 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.