États et animations dans les styles

L'API Styles offre une approche déclarative et simplifiée pour gérer les modifications de l'interface utilisateur lors d'états d'interaction tels que hovered, focused et pressed. Avec cette API, vous pouvez réduire considérablement le code récurrent généralement requis lors de l'utilisation de modificateurs.

Pour faciliter l'application de styles réactifs, StyleState fait office d'interface stable en lecture seule qui suit l'état actif d'un élément (par exemple, son état activé, appuyé ou sélectionné). Dans un StyleScope, vous pouvez y accéder via la propriété state pour implémenter une logique conditionnelle directement dans vos définitions de style.

Interaction basée sur l'état : survolé, sélectionné, appuyé, activé, désactivé

Les styles sont compatibles avec les interactions courantes :

  • Appuyé
  • Survolé
  • Sélectionné
  • Activé
  • Désactivé

Il est également possible de prendre en charge des états personnalisés. Pour en savoir plus, consultez la section Application de styles personnalisés avec StyleState.

Gérer les états d'interaction avec les paramètres de style

L'exemple suivant montre comment modifier background et borderColor en réponse aux états d'interaction, en passant au violet lorsque l'élément est survolé et au bleu lorsqu'il est sélectionné :

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

Figure 1. Modification de la couleur d'arrière-plan en fonction des états survolé et sélectionné.

Vous pouvez également créer des définitions d'état imbriquées. Par exemple, vous pouvez définir un style spécifique lorsqu'un bouton est à la fois appuyé et survolé :

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

Figure 2. États survolé et appuyé combinés sur un bouton.

Composables personnalisés avec Modifier.styleable

Lorsque vous créez vos propres composants styleable, vous devez connecter un interactionSource à un styleState. Transmettez ensuite cet état à Modifier.styleable pour l'utiliser.

Prenons l'exemple d'un système de conception qui inclut un GradientButton. Vous pouvez créer un LoginButton qui hérite de GradientButton, mais qui modifie ses couleurs lors des interactions, par exemple lorsqu'il est appuyé.

  • Pour activer les mises à jour de style interactionSource, incluez un interactionSource en tant que paramètre dans votre composable. Utilisez le paramètre fourni ou, si aucun n'est fourni, initialisez un nouveau MutableInteractionSource.
  • Initialisez le styleState en fournissant le interactionSource. Assurez-vous que l'état activé du styleState reflète la valeur du paramètre activé fourni.
  • Attribuez le interactionSource aux modificateurs focusable et clickable. Enfin, appliquez le styleState au paramètre styleable du modificateur.

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

Vous pouvez maintenant utiliser l'état interactionSource pour piloter les modifications de style avec les options appuyé, sélectionné et survolé dans le bloc de style :

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

Figure 3. Modification d'un état composable personnalisé en fonction de interactionSource.

Animer les modifications de style

Les modifications d'état des styles sont compatibles avec l'animation intégrée. Vous pouvez encapsuler la nouvelle propriété dans n'importe quel bloc de modification d'état avec animate pour ajouter automatiquement des animations entre différents états. Cela est semblable aux API animate*AsState. L'exemple suivant anime le borderColor du noir au bleu lorsque l'état passe à sélectionné :

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

    }
}

Figure 4. Animation des modifications de couleur lors de l'appui.

L'API animate accepte un animationSpec pour modifier la durée ou la forme de la courbe d'animation. L'exemple suivant anime la taille de la zone avec une spécification 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))
}

Figure 5. Animation des modifications de taille et de couleur lors de l'appui.

Application de styles personnalisés avec StyleState

Selon votre cas d'utilisation composable, vous pouvez avoir différents styles basés sur des états personnalisés. Par exemple, si vous disposez d'une application multimédia, vous pouvez avoir différents styles pour les boutons de votre composable MediaPlayer en fonction de l'état de lecture du lecteur. Procédez comme suit pour créer et utiliser votre propre état personnalisé :

  1. Définir une clé personnalisée
  2. Créer une extension StyleState
  3. Lier à un état personnalisé

Définir une clé personnalisée

Pour créer un style personnalisé basé sur l'état, commencez par créer un StyleStateKey et transmettez la valeur d'état par défaut. Lorsque l'application est lancée, le lecteur multimédia est à l'état Stopped. Il est donc initialisé de la manière suivante :

enum class PlayerState {
    Stopped,
    Playing,
    Paused
}

val playerStateKey = StyleStateKey(PlayerState.Stopped)

Créer des fonctions d'extension StyleState

Définissez une fonction d'extension sur StyleState pour interroger le playState actuel. Créez ensuite des fonctions d'extension sur StyleScope avec vos états personnalisés en transmettant le playStateKey, un lambda avec l'état spécifique et le style.

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

Définissez le styleState dans votre composable et définissez le styleState.playState sur l'état entrant. Transmettez styleState à la fonction styleable sur le modificateur.

Dans le lambda style, vous pouvez appliquer un style basé sur l'état pour les états personnalisés à l'aide des fonctions d'extension définies précédemment.

Voici l'extrait de code complet pour cet exemple :

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