Status und Animationen in Stilen

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

Zur Erleichterung des reaktiven Stylings dient StyleState als stabile, schreibgeschützte Schnittstelle, die den aktiven Status eines Elements verfolgt (z. B. den Status „Aktiviert“, „Gedrückt“ oder „Fokussiert“). In einem StyleScope können Sie über die Eigenschaft state darauf zugreifen, um bedingte Logik direkt in Ihren Stildefinitionen zu implementieren.

Statusbasierte Interaktion: „Hovered“, „Focused“, „Pressed“, „Selected“, „Enabled“, „Toggled“

Stile bieten integrierte Unterstützung für häufige Interaktionen:

  • Gedrückt
  • Mauszeiger über bewegt
  • Ausgewählt
  • Aktiviert
  • Festgelegt

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

Interaktionsstatus mit Stilparametern verarbeiten

Im folgenden Beispiel wird gezeigt, wie background und borderColor als Reaktion auf Interaktionsstatus geändert werden. Insbesondere wird zu Lila gewechselt, wenn der Mauszeiger darüber 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 den Status „Hovered“ und „Focused“ ändern

Sie können auch verschachtelte Statusdefinitionen erstellen. Sie können beispielsweise einen bestimmten Stil definieren, wenn eine Schaltfläche gleichzeitig gedrückt und der Mauszeiger darüber bewegt 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 Status „Hovered“ und „Pressed“ zusammen auf einer Schaltfläche

Benutzerdefinierte zusammensetzbare Elemente mit Modifier.styleable

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

Angenommen, Ihr Designsystem enthält eine GradientButton. Möglicherweise möchten Sie eine LoginButton erstellen, die von GradientButton erbt, aber ihre Farben bei Interaktionen wie dem Drücken ändert.

  • Wenn Sie Stilaktualisierungen für interactionSource aktivieren möchten, fügen Sie eine interactionSource als Parameter in Ihr zusammensetzbares Element ein. Verwenden Sie den angegebenen Parameter oder initialisieren Sie eine neue MutableInteractionSource, falls keine angegeben ist.
  • Initialisieren Sie styleState, indem Sie interactionSource angeben. Der aktivierte Status von styleState muss den Wert des angegebenen aktivierten Parameters widerspiegeln.
  • Weisen Sie interactionSource den Modifikatoren focusable und clickable zu. Wenden Sie schließlich 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 = rememberUpdatedStyleState(interactionSource) {
        it.isEnabled = enabled
    }
    Row(
        modifier =
            modifier
                .clickable(
                    onClick = onClick,
                    enabled = enabled,
                    interactionSource = interactionSource,
                    indication = null,
                )
                .styleable(styleState, baseGradientButtonStyle then style),
        content = content,
    )
}

Sie können jetzt den Status interactionSource verwenden, um Stiländerungen mit den Optionen „Pressed“, „Focused“ und „Hovered“ 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 Status eines benutzerdefinierten zusammensetzbaren Elements basierend auf interactionSource ändern

Stiländerungen animieren

Statusänderungen von Stilen bieten integrierte Unterstützung für Animationen. Sie können die neue Eigenschaft in einen beliebigen Statusänderungsblock mit animate einschließen, um automatisch Animationen zwischen verschiedenen Status hinzuzufügen. Dies ähnelt den animate*AsState-APIs. Im folgenden Beispiel wird borderColor von Schwarz zu Blau animiert, wenn sich der Status in „Focused“ ä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 eine 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

Benutzerdefinierte Statusstile mit StyleState

Je nach Anwendungsfall für zusammensetzbare Elemente können Sie unterschiedliche Stile haben, die von benutzerdefinierten Status unterstützt werden. Wenn Sie beispielsweise eine Media-App haben, möchten Sie möglicherweise unterschiedliche Stile für die Schaltflächen in Ihrem zusammensetzbaren Element MediaPlayer haben, je nach Wiedergabestatus des Players. Führen Sie die folgenden Schritte aus, um einen eigenen benutzerdefinierten Status zu erstellen und zu verwenden:

  1. Benutzerdefinierten Schlüssel definieren
  2. StyleState-Erweiterung erstellen
  3. Mit benutzerdefiniertem Status verknüpfen

Benutzerdefinierten Schlüssel definieren

Wenn Sie einen benutzerdefinierten statusbasierten Stil erstellen möchten, erstellen Sie zuerst einen StyleStateKey und übergeben Sie den Standardstatuswert. Beim Starten der App 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 den aktuellen playState abzufragen. Erstellen Sie dann Erweiterungsfunktionen für StyleScope mit Ihren benutzerdefinierten Status, indem Sie playStateKey, ein Lambda mit dem spezifischen Status und den Stil übergeben.

// 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 das styleState in Ihrem zusammensetzbaren Element und setzen Sie das styleState.playState gleich dem eingehenden Status. Übergeben Sie styleState an die Funktion styleable für den Modifikator.

Im style-Lambda können Sie statusbasiertes Styling für benutzerdefinierte Status mithilfe der zuvor definierten Erweiterungsfunktionen 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)
}