Stany i animacje w stylach

Interfejs Styles API oferuje deklaratywne i uproszczone podejście do zarządzania zmianami interfejsu podczas stanów interakcji, takich jak hovered, focusedpressed. Ten interfejs API pozwala znacznie ograniczyć 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 zaznaczony). 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, zaznaczono, naciśnięto, wybrano, włączono, przełączono

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

  • Naciśnięty
  • Najechano
  • Wybrane
  • Włączono
  • Przełączono

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

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

Poniższy przykład pokazuje, jak zmodyfikować background i borderColor w odpowiedzi na stany interakcji, w szczególności zmienić kolor na fioletowy po najechaniu kursorem i na niebieski po zaznaczeniu:

@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 najechanej i aktywnej pozycji.

Możesz też tworzyć zagnieżdżone definicje stanów. Możesz na przykład zdefiniować konkretny styl, który będzie stosowany, gdy przycisk jest jednocześnie naciskany 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 najechany i naciśnięty na przycisku.

Komponenty z możliwością dostosowania za pomocą Modifier.styleable

Podczas tworzenia własnych styleable komponentów musisz połączyć interactionSourcestyleState. Następnie przekaż ten stan do funkcji Modifier.styleable, aby go wykorzystać.

Rozważmy scenariusz, w którym system projektowania zawiera element 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, w funkcji kompozycyjnej umieść parametr interactionSource. Użyj podanego parametru lub, jeśli nie został on podany, zainicjuj nowy obiekt MutableInteractionSource.
  • Zainicjuj obiekt styleState, podając interactionSource. Upewnij się, że stan włączony styleState odzwierciedla wartość podanego parametru enabled.
  • Przypisz interactionSource do modyfikatorów focusableclickable. 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 = 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,
    )
}

Możesz teraz używać stanu interactionSource do wprowadzania zmian w stylu za pomocą opcji naciśnięty, zaznaczony i najechany 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 komponentu opartego na interactionSource.

Animowanie zmian stylu

Zmiany stanu stylów mają wbudowaną obsługę animacji. Nową właściwość możesz umieścić w dowolnym bloku zmiany stanu za pomocą funkcji animate, aby automatycznie dodawać animacje między różnymi stanami. Działa to podobnie jak interfejsy animate*AsState. W tym przykładzie kolor borderColor zmienia się 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 kolorów po naciśnięciu.

Interfejs API animate akceptuje animationSpec, aby zmienić czas trwania lub kształt krzywej animacji. W tym przykładzie animujemy 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.

Dostosowywanie stylu stanu za pomocą StyleState

W zależności od przypadku użycia komponentu 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 chcieć zastosować różne style przycisków w MediaPlayer komponencie w zależności od stanu odtwarzania odtwarzacza. Aby utworzyć i używać własnego stanu niestandardowego:

  1. Definiowanie klucza niestandardowego
  2. Tworzenie rozszerzenia StyleState
  3. Link do stanu niestandardowego

Definiowanie klucza niestandardowego

Aby utworzyć niestandardowy styl oparty na stanie, najpierw utwórz StyleStateKey i przekaż domyślną wartość stanu. Po uruchomieniu aplikacji odtwarzacz multimediów jest w stanie Stopped, 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 do bieżącego playState. Następnie utwórz funkcje rozszerzenia w StyleScope z własnymi stanami przekazywanymi w playStateKey, lambdę z określonym 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 funkcji kompozycyjnej i ustaw styleState.playState równą przychodzącemu stanowi. Przekaż styleState do funkcji styleable w modyfikatorze.

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

Oto pełny fragment kodu w tym przykładzie:

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