Stato e animazioni in Stili

L'API Styles offre un approccio dichiarativo e semplificato alla gestione delle modifiche all'interfaccia utente durante gli stati di interazione come hovered, focused e pressed. Con questa API, puoi ridurre notevolmente il codice boilerplate in genere richiesto quando utilizzi i modificatori.

Per facilitare lo stile reattivo, StyleState funge da interfaccia stabile e di sola lettura che tiene traccia dello stato attivo di un elemento (ad esempio il suo stato abilitato, premuto o selezionato). All'interno di un StyleScope, puoi accedere a questa funzionalità tramite la proprietà state per implementare la logica condizionale direttamente nelle definizioni di stile.

Interazione basata sullo stato: al passaggio del mouse, stato attivo, premuto, selezionato, attivato, attivato/disattivato

Gli stili sono dotati di supporto integrato per le interazioni comuni:

  • Premuti
  • Al passaggio del mouse
  • Selezionato
  • Abilitati
  • Attivato/disattivato

È anche possibile supportare stati personalizzati. Per saperne di più, consulta la sezione Stili personalizzati degli stati con StyleState.

Gestire gli stati di interazione con i parametri di stile

L'esempio seguente mostra la modifica di background e borderColor in risposta agli stati di interazione, in particolare il passaggio al colore viola quando viene passato il mouse sopra e al blu quando viene selezionato:

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

Figura 1. Modifica del colore dello sfondo in base agli stati di passaggio del mouse e messa a fuoco.

Puoi anche creare definizioni di stato nidificate. Ad esempio, puoi definire uno stile specifico per quando un pulsante viene premuto e passato con il mouse contemporaneamente:

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

Figura 2. Stato di passaggio del mouse e pressione insieme su un pulsante.

Composable personalizzabili con Modifier.styleable

Quando crei i tuoi componenti styleable, devi collegare un interactionSource a un styleState. Poi, passa questo stato a Modifier.styleable per utilizzarlo.

Considera uno scenario in cui il tuo sistema di progettazione include un GradientButton. Potresti voler creare un LoginButton che eredita da GradientButton, ma che cambia colore durante le interazioni, ad esempio quando viene premuto.

  • Per attivare gli aggiornamenti dello stile interactionSource, includi un interactionSource come parametro all'interno del tuo elemento componibile. Utilizza il parametro fornito o, se non ne viene fornito uno, inizializza un nuovo MutableInteractionSource.
  • Inizializza styleState fornendo interactionSource. Assicurati che lo stato attivato di styleState rifletta il valore del parametro attivato fornito.
  • Assegna interactionSource ai modificatori focusable e clickable. Infine, applica styleState al parametro styleable del modificatore.

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

Ora puoi utilizzare lo stato interactionSource per apportare modifiche allo stile con le opzioni premuto, selezionato e al passaggio del mouse all'interno del blocco di stile:

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

Figura 3. Modifica dello stato di un elemento componibile personalizzato in base a interactionSource.

Animare le modifiche dello stile

Le modifiche dello stato degli stili sono dotate di supporto integrato per le animazioni. Puoi racchiudere la nuova proprietà all'interno di qualsiasi blocco di cambio di stato con animate per aggiungere automaticamente animazioni tra i diversi stati. Questo scenario è simile a quello delle API animate*AsState. L'esempio seguente anima borderColor da nero ad azzurro quando lo stato cambia in attivo:

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

    }
}

Figura 4. Animazione dei cambi di colore alla pressione.

L'API animate accetta un animationSpec per modificare la durata o la forma della curva di animazione. L'esempio seguente anima le dimensioni della casella con una specifica 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))
}

Figura 5. Animazione delle modifiche di dimensioni e colore alla pressione.

Stili personalizzati degli stati con StyleState

A seconda del caso d'uso componibile, potresti avere stili diversi supportati da stati personalizzati. Ad esempio, se hai un'app multimediale, potresti voler avere uno stile diverso per i pulsanti nel composable MediaPlayer a seconda dello stato di riproduzione del player. Per creare e utilizzare il tuo stato personalizzato:

  1. Definisci una chiave personalizzata
  2. Crea estensione StyleState
  3. Link allo stato personalizzato

Definisci una chiave personalizzata

Per creare uno stile personalizzato basato sullo stato, crea prima un StyleStateKey e passa il valore di stato predefinito. Quando l'app viene avviata, il lettore multimediale è nello stato Stopped, quindi viene inizializzato in questo modo:

enum class PlayerState {
    Stopped,
    Playing,
    Paused
}

val playerStateKey = StyleStateKey(PlayerState.Stopped)

Crea funzioni di estensione StyleState

Definisci una funzione di estensione su StyleState per eseguire query sul playState corrente. Quindi, crea funzioni di estensione su StyleScope con i tuoi stati personalizzati che passano in playStateKey, una lambda con lo stato specifico e lo stile.

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

Definisci styleState nel componente e imposta styleState.playState uguale allo stato in entrata. Passa styleState alla funzione styleable nel modificatore.

All'interno della lambda style, puoi applicare uno stile basato sullo stato per gli stati personalizzati, utilizzando le funzioni di estensione definite in precedenza.

Il seguente codice è lo snippet completo per questo esempio:

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