États et animations dans les styles

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

Pour faciliter le style réactif, StyleState sert 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é
  • Pointeur sur
  • Sélectionné
  • Activé
  • Activé

Il est également possible de prendre en charge des états personnalisés. Pour en savoir plus, consultez la section Personnaliser le style des états 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 plus précisément au violet en cas de pointeur et au bleu en cas de sélection :

@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 de survol et de sélection

Vous pouvez également créer des définitions d'état imbriquées. Par exemple, vous pouvez définir un style spécifique pour un bouton lorsque l'utilisateur appuie dessus et le survole simultanément :

@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. État "Pointeur et bouton enfoncé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. Ensuite, transmettez cet état à Modifier.styleable pour l'utiliser.

Imaginons que votre système de conception inclue un GradientButton. Vous pouvez créer un LoginButton qui hérite de GradientButton, mais qui modifie ses couleurs lors des interactions, comme lorsqu'il est enfoncé.

  • 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 styleState en fournissant interactionSource. Assurez-vous que l'état activé de styleState reflète la valeur du paramètre activé fourni.
  • Attribuez le interactionSource aux modificateurs focusable et clickable. Enfin, appliquez 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 = 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,
    )
}

Vous pouvez désormais utiliser l'état interactionSource pour modifier le style à l'aide des 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. Modifier l'état d'un composable personnalisé en fonction de interactionSource.

Animer les modifications de style

Les changements d'état des styles sont fournis avec une compatibilité d'animation intégrée. Vous pouvez encapsuler la nouvelle propriété dans n'importe quel bloc de changement d'état avec animate pour ajouter automatiquement des animations entre différents états. Cela est semblable aux API animate*AsState. L'exemple suivant anime borderColor du noir au bleu lorsque l'état passe à "focused" (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. Animer les changements 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 boîte 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 de la taille et de la couleur au moment de l'appui.

Personnaliser le style d'état avec StyleState

Selon votre cas d'utilisation composable, vous pouvez avoir différents styles qui sont soutenus par des états personnalisés. Par exemple, si vous avez une application multimédia, vous pouvez souhaiter avoir un style différent pour les boutons de votre composable MediaPlayer en fonction de l'état de lecture du lecteur. Pour créer et utiliser votre propre état personnalisé, procédez comme suit :

  1. Définir une clé personnalisée
  2. Créer l'extension StyleState
  3. Lien vers 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 se lance, le lecteur multimédia est dans l'état Stopped. Il est donc initialisé de cette manière :

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 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 styleState dans votre composable et définissez 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)
}