Stany i animacje w stylach

Interfejs Styles API oferuje deklaratywne i uproszczone podejście do zarządzania zmianami interfejsu podczas interakcji, takich jak hovered (najechano), focused (skupiono) i pressed (naciśnięto). Dzięki temu interfejsowi API możesz znacznie zmniejszyć ilość powtarzalnego kodu, który jest zwykle wymagany podczas korzystania z modyfikatorów.

Aby ułatwić stylizację reaktywną, StyleState działa jako stabilny interfejs tylko do odczytu, który śledzi aktywny stan elementu (np. jego stan włączony, naciśnięty lub skupiony). W StyleScope możesz uzyskać do niego dostęp za pomocą właściwości state, aby zaimplementować logikę warunkową bezpośrednio w definicjach stylu.

Interakcja oparta na stanie: najechano, skupiono, naciśnięto, wybrano, włączono, przełączono

Style mają wbudowaną obsługę typowych interakcji:

  • Naciśnięto
  • Najechano
  • Wybrano
  • Włączono
  • Przełączono

Możesz też obsługiwać stany niestandardowe. Więcej informacji znajdziesz w sekcji Stylizacja stanu niestandardowego za pomocą StyleState.

Obsługa stanów interakcji za pomocą parametrów stylu

Poniższy przykład pokazuje, jak modyfikować background (tło) i borderColor (kolor obramowania) w odpowiedzi na stany interakcji, w szczególności przełączanie na kolor fioletowy po najechaniu i niebieski po skupieniu:

@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)
            })
        }
    )
}

Rysunek 1. Zmiana koloru tła w zależności od stanu najechano i skupiono.

Możesz też tworzyć zagnieżdżone definicje stanu. Możesz na przykład zdefiniować konkretny styl, gdy przycisk jest jednocześnie naciśnięty i najechany:

@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)
            })
        }
    )
}

Rysunek 2. Stan najechano i naciśnięto na przycisku.

Niestandardowe elementy kompozycyjne z Modifier.styleable

Podczas tworzenia własnych komponentów styleable musisz połączyć interactionSource z styleState. Następnie przekaż ten stan do Modifier.styleable, aby go użyć.

Rozważmy sytuację, w której system projektowania zawiera GradientButton. Możesz utworzyć LoginButton, który dziedziczy po GradientButton, ale zmienia kolory podczas interakcji, np. po naciśnięciu.

  • Aby włączyć aktualizacje stylu interactionSource, dodaj interactionSource jako parametr w elemencie kompozycyjnym. Użyj podanego parametru lub, jeśli nie został podany, zainicjuj nowy MutableInteractionSource.
  • Zainicjuj styleState, podając interactionSource. Upewnij się, że stan włączony styleState odzwierciedla wartość podanego parametru włączonego.
  • Przypisz interactionSource do modyfikatorów focusable i clickable. Na koniec zastosuj styleState do parametru styleable modyfikatora.

@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,
    )
}

Teraz możesz użyć stanu interactionSource, aby sterować modyfikacjami stylu za pomocą opcji naciśnięto, skupiono i najechano w bloku stylu:

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

Rysunek 3. Zmiana stanu niestandardowego elementu kompozycyjnego na podstawie interactionSource.

Animowanie zmian stylu

Zmiany stanu stylu mają wbudowaną obsługę animacji. Aby automatycznie dodawać animacje między różnymi stanami, możesz opakować nową właściwość w dowolnym bloku zmiany stanu za pomocą animate. Jest to podobne do interfejsów API animate*AsState. Poniższy przykład animuje borderColor z czarnego na niebieski, gdy stan zmieni się na skupiony:

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)) {

    }
}

Rysunek 4. Animowanie zmian koloru po naciśnięciu.

Interfejs API animate akceptuje animationSpec, aby zmienić czas trwania lub kształt krzywej animacji. Poniższy przykład animuje rozmiar pola za pomocą specyfikacji spring:

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

Rysunek 5. Animowanie zmian rozmiaru i koloru po naciśnięciu.

Stylizacja stanu niestandardowego za pomocą StyleState

W zależności od przypadku użycia elementu kompozycyjnego możesz mieć różne style, które są oparte na stanach niestandardowych. Jeśli na przykład masz aplikację do multimediów, możesz mieć różne style przycisków w elemencie kompozycyjnym MediaPlayer w zależności od stanu odtwarzania odtwarzacza. Aby utworzyć i używać własnego stanu niestandardowego, wykonaj te czynności:

  1. Zdefiniuj klucz niestandardowy
  2. Utwórz rozszerzenie StyleState
  3. Połącz ze stanem niestandardowym

Zdefiniuj klucz niestandardowy

Aby utworzyć styl oparty na stanie niestandardowym, najpierw utwórz StyleStateKey i przekaż domyślną wartość stanu. Po uruchomieniu aplikacji odtwarzacz multimediów jest w stanie Stopped (Zatrzymany), więc jest inicjowany w ten sposób:

enum class PlayerState {
    Stopped,
    Playing,
    Paused
}

val playerStateKey = StyleStateKey(PlayerState.Stopped)

Tworzenie funkcji rozszerzenia StyleState

Zdefiniuj funkcję rozszerzenia w StyleState, aby wysyłać zapytania o bieżący playState. Następnie utwórz funkcje rozszerzenia w StyleScope ze stanami niestandardowymi, przekazując playStateKey, lambdę z konkretnym stanem i stylem.

// 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 })
}

Zdefiniuj styleState w elemencie kompozycyjnym i ustaw styleState.playState na stan przychodzący. Przekaż styleState do funkcji styleable w modyfikatorze.

W lambdzie style możesz zastosować stylizację opartą na stanie w przypadku stanów niestandardowych, używając wcześniej zdefiniowanych funkcji rozszerzenia.

Poniżej znajdziesz pełny fragment kodu tego przykładu:

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