Stato e animazioni in Stili

L'API Styles offre un approccio dichiarativo e semplificato alla gestione delle modifiche dell'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 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 accedervi tramite la proprietà state per implementare la logica condizionale direttamente nelle definizioni di stile.

Interazione basata sullo stato: puntatore su, selezionato, premuto, abilitato, attivato/disattivato

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

  • Premuto
  • Puntatore su
  • Selezionato
  • Abilitato
  • Attivato/disattivato

È anche possibile supportare stati personalizzati. Per ulteriori informazioni, consulta la sezione Stile dello stato personalizzato con StyleState.

Gestire gli stati di interazione con i parametri di stile

L'esempio seguente mostra come modificare background e borderColor in risposta agli stati di interazione, in particolare passando al viola quando il puntatore è sopra e al blu quando è 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 di sfondo in base agli stati puntatore su e selezionato.

Puoi anche creare definizioni di stato nidificate. Ad esempio, puoi definire uno stile specifico per quando un pulsante viene premuto e il puntatore è sopra 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 puntatore su e premuto insieme su un pulsante.

Componibili personalizzati con Modifier.styleable

Quando crei i tuoi componenti styleable, devi collegare un interactionSource a un styleState. Quindi, 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 ne modifica i colori durante le interazioni, ad esempio quando viene premuto.

  • Per abilitare gli aggiornamenti dello stile interactionSource, includi un interactionSource come parametro all'interno del tuo componibile. Utilizza il parametro fornito o, se non ne viene fornito uno, inizializza un nuovo MutableInteractionSource.
  • Inizializza styleState fornendo interactionSource. Assicurati che lo stato abilitato di styleState rifletta il valore del parametro abilitato 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 = rememberUpdatedStyleState(interactionSource) {
        it.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 guidare le modifiche dello stile con le opzioni premuto, selezionato e puntatore su 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 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 modifica dello stato con animate per aggiungere automaticamente le animazioni tra stati diversi. È simile alle API animate*AsState. L'esempio seguente anima borderColor da nero a blu quando lo stato passa a selezionato:

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 delle modifiche del colore quando si preme.

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 quando si preme.

Stile dello stato personalizzato con StyleState

A seconda del caso d'uso del 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 tuo componibile MediaPlayer a seconda dello stato di riproduzione del player. Segui questi passaggi per creare e utilizzare il tuo stato personalizzato:

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

Definisci 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 media player è 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 una query su playState corrente. Quindi, crea funzioni di estensione su StyleScope con i tuoi stati personalizzati passando playStateKey, un'espressione 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 il styleState nel tuo componibile e imposta il styleState.playState uguale allo stato in entrata. Passa styleState alla funzione styleable sul modificatore.

All'interno dell'espressione lambda style, puoi applicare lo 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)
}