Status und Animationen in Stilen

Die Styles API bietet einen deklarativen und optimierten Ansatz zum Verwalten von Änderungen an der Benutzeroberfläche während Interaktionsstatus wie hovered, focused und pressed. Mit dieser API können Sie den Boilerplate-Code, der normalerweise bei der Verwendung von Modifizierern erforderlich ist, erheblich reduzieren.

Um reaktive Formatierung zu ermöglichen, fungiert StyleState als stabile, schreibgeschützte Schnittstelle, die den aktiven Status eines Elements (z. B. den Status „Aktiviert“, „Gedrückt“ oder „Fokus“) verfolgt. Innerhalb eines StyleScope können Sie über die state-Eigenschaft darauf zugreifen, um bedingte Logik direkt in Ihre Stildefinitionen einzufügen.

Zustandsbasierte Interaktion: Mauszeiger darauf bewegt, fokussiert, gedrückt, ausgewählt, aktiviert, umgeschaltet

Stile unterstützen standardmäßig häufige Interaktionen:

  • Gedrückt
  • Hovered
  • Ausgewählt
  • Aktiviert
  • Ein-/Aus-Button

Es ist auch möglich, benutzerdefinierte Status zu unterstützen. Weitere Informationen finden Sie im Abschnitt Benutzerdefinierte Statusformatierung mit StyleState.

Interaktionsstatus mit Stilparametern verarbeiten

Im folgenden Beispiel wird gezeigt, wie die background und borderColor in Reaktion auf Interaktionsstatus geändert werden. Genauer gesagt wird die Farbe zu Lila geändert, wenn der Mauszeiger darauf bewegt wird, und zu Blau, wenn der Fokus darauf liegt:

@Preview
@Composable
private fun OpenButton() {
    BaseButton(
        style = outlinedButtonStyle then {
            background(Color.White)
            hovered {
                background(lightPurple)
                border(2.dp, lightPurple)
            }
            focused {
                background(lightBlue)
            }
        },
        onClick = {  },
        content = {
            BaseText("Open in Studio", style = {
                contentColor(Color.Black)
                fontSize(26.sp)
                textAlign(TextAlign.Center)
            })
        }
    )
}

Abbildung 1. Hintergrundfarbe basierend auf dem Mauszeiger- und Fokusstatus ändern.

Sie können auch verschachtelte Zustandsdefinitionen erstellen. Sie können beispielsweise einen bestimmten Stil definieren, der angewendet wird, wenn eine Schaltfläche gleichzeitig gedrückt und mit dem Mauszeiger darauf gezeigt wird:

@Composable
private fun OpenButton_CombinedStates() {
    BaseButton(
        style = outlinedButtonStyle then {
            background(Color.White)
            hovered {
                // light purple
                background(lightPurple)
                pressed {
                    // When running on a device that can hover, whilst hovering and then pressing the button this would be invoked
                    background(lightOrange)
                }
            }
            pressed {
                // when running on a device without a mouse attached, this would be invoked as you wouldn't be in a hovered state only
                background(lightRed)
            }
            focused {
                background(lightBlue)
            }
        },
        onClick = {  },
        content = {
            BaseText("Open in Studio", style = {
                contentColor(Color.Black)
                fontSize(26.sp)
                textAlign(TextAlign.Center)
            })
        }
    )
}

Abbildung 2. Zustand „Mit dem Mauszeiger darauf zeigen“ und „Gedrückt“ auf einer Schaltfläche.

Benutzerdefinierte Composables mit Modifier.styleable

Wenn Sie eigene styleable-Komponenten erstellen, müssen Sie ein interactionSource mit einem styleState verbinden. Übergeben Sie diesen Status dann an Modifier.styleable, um ihn zu verwenden.

Stellen Sie sich ein Szenario vor, in dem Ihr Designsystem eine GradientButton enthält. Sie können ein LoginButton erstellen, das von GradientButton abgeleitet wird, aber seine Farben bei Interaktionen wie dem Drücken ändert.

  • Wenn Sie Stilaktualisierungen für interactionSource aktivieren möchten, fügen Sie ein interactionSource als Parameter in Ihre Composable-Funktion ein. Verwenden Sie den bereitgestellten Parameter oder initialisieren Sie eine neue MutableInteractionSource, falls keiner angegeben ist.
  • Initialisieren Sie styleState, indem Sie die interactionSource angeben. Der aktivierte Status von styleState muss dem Wert des bereitgestellten Parameters „enabled“ entsprechen.
  • Weisen Sie interactionSource den Modifikatoren focusable und clickable zu. Wenden Sie schließlich die styleState auf den Parameter styleable des Modifikators an.

@Composable
private fun GradientButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    style: Style = Style,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource? = null,
    content: @Composable RowScope.() -> Unit,
) {
    val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
    val styleState = remember(interactionSource) { MutableStyleState(interactionSource) }
    styleState.isEnabled = enabled
    Row(
        modifier =
            modifier
                .clickable(
                    onClick = onClick,
                    enabled = enabled,
                    interactionSource = interactionSource,
                    indication = null,
                )
                .styleable(styleState, baseGradientButtonStyle then style),
        content = content,
    )
}

Sie können den Status interactionSource jetzt verwenden, um Stiländerungen mit den Optionen „Gedrückt“, „Fokus“ und „Mauszeiger darauf“ im Stilblock zu steuern:

@Preview
@Composable
fun LoginButton() {
    val loginButtonStyle = Style {
        pressed {
            background(
                Brush.linearGradient(
                    listOf(Color.Magenta, Color.Red)
                )
            )
        }
    }
    GradientButton(onClick = {
        // Login logic
    }, style = loginButtonStyle) {
        BaseText("Login")
    }
}

Abbildung 3. Ändern des Status einer benutzerdefinierten Composable-Funktion basierend auf interactionSource.

Stiländerungen animieren

Statusänderungen von Stilen werden mit integrierter Animationsunterstützung ausgeliefert. Sie können die neue Eigenschaft in einen beliebigen Block für Zustandsänderungen mit animate einfügen, um automatisch Animationen zwischen verschiedenen Zuständen hinzuzufügen. Dies ähnelt den animate*AsState-APIs. Im folgenden Beispiel wird die borderColor von Schwarz zu Blau animiert, wenn sich der Status in „Fokus“ ändert:

val animatingStyle = Style {
    externalPadding(48.dp)
    border(3.dp, Color.Black)
    background(Color.White)
    size(100.dp)

    pressed {
        animate {
            borderColor(Color.Magenta)
            background(Color(0xFFB39DDB))
        }
    }
}

@Preview
@Composable
private fun AnimatingStyleChanges() {
    val interactionSource = remember { MutableInteractionSource() }
    val styleState = remember(interactionSource) { MutableStyleState(interactionSource) }
    Box(modifier = Modifier
        .clickable(
            interactionSource,
            enabled = true,
            indication = null,
            onClick = {

            }
        )
        .styleable(styleState, animatingStyle)) {

    }
}

Abbildung 4. Farbänderungen beim Drücken animieren

Die animate API akzeptiert einen animationSpec, um die Dauer oder Form der Animationskurve zu ändern. Im folgenden Beispiel wird die Größe des Felds mit einer spring-Spezifikation animiert:

val animatingStyleSpec = Style {
    externalPadding(48.dp)
    border(3.dp, Color.Black)
    background(Color.White)
    size(100.dp)
    transformOrigin(TransformOrigin.Center)
    pressed {
        animate {
            borderColor(Color.Magenta)
            background(Color(0xFFB39DDB))
        }
        animate(spring(dampingRatio = Spring.DampingRatioMediumBouncy)) {
            scale(1.2f)
        }
    }
}

@Preview(showBackground = true)
@Composable
fun AnimatingStyleChangesSpec() {
    val interactionSource = remember { MutableInteractionSource() }
    val styleState = remember(interactionSource) { MutableStyleState(interactionSource) }
    Box(modifier = Modifier
        .clickable(
            interactionSource,
            enabled = true,
            indication = null,
            onClick = {

            }
        )
        .styleable(styleState, animatingStyleSpec))
}

Abbildung 5. Größen- und Farbänderungen beim Drücken animieren

Benutzerdefiniertes Styling von Status mit StyleState

Je nach Ihrem zusammensetzbaren Anwendungsfall haben Sie möglicherweise verschiedene Stile, die von benutzerdefinierten Status unterstützt werden. Wenn Sie beispielsweise eine Media-App haben, möchten Sie möglicherweise je nach Wiedergabestatus des Players unterschiedliche Formatierungen für die Schaltflächen in Ihrem MediaPlayer-Composable verwenden. So erstellen und verwenden Sie einen eigenen benutzerdefinierten Status:

  1. Benutzerdefinierten Schlüssel definieren
  2. Erstellen Sie die Erweiterung StyleState.
  3. Link zum benutzerdefinierten Status

Benutzerdefinierten Schlüssel definieren

Wenn Sie einen benutzerdefinierten zustandsbasierten Stil erstellen möchten, müssen Sie zuerst ein StyleStateKey erstellen und den Standardstatuswert übergeben. Wenn die App gestartet wird, befindet sich der Media Player im Status Stopped. Er wird also so initialisiert:

enum class PlayerState {
    Stopped,
    Playing,
    Paused
}

val playerStateKey = StyleStateKey(PlayerState.Stopped)

StyleState-Erweiterungsfunktionen erstellen

Definieren Sie eine Erweiterungsfunktion für StyleState, um die aktuelle playState abzufragen. Erstellen Sie dann Erweiterungsfunktionen für StyleScope, indem Sie Ihre benutzerdefinierten Status übergeben. Dazu verwenden Sie playStateKey, eine Lambda-Funktion mit dem jeweiligen Status und dem Stil.

// Extension Function on MutableStyleState to query and set the current playState
var MutableStyleState.playerState
    get() = this[playerStateKey]
    set(value) { this[playerStateKey] = value }

fun StyleScope.playerPlaying(value: Style) {
    state(playerStateKey, value, { key, state -> state[key] == PlayerState.Playing })
}
fun StyleScope.playerPaused(value: Style) {
    state(playerStateKey, value, { key, state -> state[key] == PlayerState.Paused })
}

Definieren Sie styleState in Ihrem Composable und legen Sie styleState.playState auf den eingehenden Status fest. Übergeben Sie styleState an die Funktion styleable für den Modifier.

Innerhalb des style-Lambdas können Sie mithilfe der zuvor definierten Erweiterungsfunktionen statusbasiertes Styling für benutzerdefinierte Status anwenden.

Der folgende Code ist das vollständige Snippet für dieses Beispiel:

enum class PlayerState {
    Stopped,
    Playing,
    Paused
}
val playerStateKey = StyleStateKey<PlayerState>(PlayerState.Stopped)
var MutableStyleState.playerState
    get() = this[playerStateKey]
    set(value) { this[playerStateKey] = value }

fun StyleScope.playerPlaying(value: Style) {
    state(playerStateKey, value, { key, state -> state[key] == PlayerState.Playing })
}
fun StyleScope.playerPaused(value: Style) {
    state(playerStateKey, value, { key, state -> state[key] == PlayerState.Paused })

}

@Composable
fun MediaPlayer(
    url: String,
    modifier: Modifier = Modifier,
    style: Style = Style,
    state: PlayerState = remember { PlayerState.Paused }
) {
    // Hoist style state, set playstate as a parameter,
    val styleState = remember { MutableStyleState(null) }
    // Set equal to incoming state to link the two together
    styleState.playerState = state
    Box(
        modifier = modifier.styleable(styleState, Style {
            size(100.dp)
            border(2.dp, Color.Red)

        }, style, )) {

        ///..
    }
}
@Composable
fun StyleStateKeySample() {
    // Using the extension function to change the border color to green while playing
    val style = Style {
        borderColor(Color.Gray)
        playerPlaying {
            animate {
                borderColor(Color.Green)
            }
        }
        playerPaused {
            animate {
                borderColor(Color.Blue)
            }
        }
    }
    val styleState = remember { MutableStyleState(null) }
    styleState[playerStateKey] = PlayerState.Playing

    // Using the style in a composable that sets the state -> notice if you change the state parameter, the style changes. You can link this up to an ViewModel and change the state from there too.
    MediaPlayer(url = "https://example.com/media/video",
        style = style,
        state = PlayerState.Stopped)
}