Animer une seule valeur avec animate*AsState
Les fonctions animate*AsState
comptent parmi les API d'animation les plus simples dans Compose. Elles permettent d'animer une seule valeur. Vous fournissez uniquement la valeur finale (ou la valeur cible), et l'API lance une animation menant de la valeur actuelle à la valeur spécifiée.
L'exemple ci-dessous montre comment animer le niveau alpha avec cette API. En encapsulant simplement la valeur cible dans animateFloatAsState
, la valeur alpha devient une valeur d'animation entre les valeurs fournies (1f
ou 0.5f
dans cet exemple).
var enabled by remember { mutableStateOf(true) } val alpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f) Box( Modifier.fillMaxSize() .graphicsLayer(alpha = alpha) .background(Color.Red) )
Notez qu'il n'est pas nécessaire de créer une instance de classe d'animation ni de gérer l'interruption. En arrière-plan, un objet d'animation (à savoir, une instance Animatable
) sera créé et mémorisé sur le site d'appel, avec comme valeur initiale la première valeur cible. À partir de là, chaque fois que vous fournissez une valeur cible différente à ce composable, une animation est automatiquement lancée vers cette valeur. Si une autre animation est déjà en cours, la nouvelle animation démarre à partir de la valeur actuelle (et avec la vitesse actuelle) et se poursuit vers la valeur cible. Pendant l'animation, ce composable est recomposé et renvoie une valeur d'animation mise à jour pour chaque frame.
Compose fournit directement des fonctions animate*AsState
pour Float
, Color
, Dp
, Size
, Offset
, Rect
, Int
, IntOffset
et IntSize
. Vous pouvez facilement prendre en charge d'autres types de données en fournissant un TwoWayConverter
à animateValueAsState
, qui accepte un type générique.
Il est possible de personnaliser les spécifications de l'animation en fournissant un AnimationSpec
.
Pour en savoir plus, consultez la section AnimationSpec.
Animer plusieurs propriétés simultanément avec une transition
Transition
gère une ou plusieurs animations en tant qu'enfants et les exécute simultanément entre plusieurs états.
Tous les types de données sont acceptés pour les états. Bien souvent, vous pouvez utiliser un type enum
personnalisé pour assurer la sûreté du typage, comme dans cet exemple :
enum class BoxState { Collapsed, Expanded }
updateTransition
crée et mémorise une instance de Transition
et met à jour son état.
var currentState by remember { mutableStateOf(BoxState.Collapsed) } val transition = updateTransition(currentState, label = "box state")
Vous pouvez ensuite utiliser une fonction d'extension animate*
pour définir une animation enfant dans cette transition. Spécifiez les valeurs cibles pour chaque état.
Ces fonctions animate*
renvoient une valeur d'animation qui est mise à jour pour chaque frame de l'animation lorsque l'état de transition est actualisé avec updateTransition
.
val rect by transition.animateRect(label = "rectangle") { state -> when (state) { BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f) BoxState.Expanded -> Rect(100f, 100f, 300f, 300f) } } val borderWidth by transition.animateDp(label = "border width") { state -> when (state) { BoxState.Collapsed -> 1.dp BoxState.Expanded -> 0.dp } }
Vous pouvez éventuellement transmettre un paramètre transitionSpec
afin de spécifier un AnimationSpec
différent pour chaque combinaison de changements d'état de transition. Pour en savoir plus, consultez la section AnimationSpec.
val color by transition.animateColor( transitionSpec = { when { BoxState.Expanded isTransitioningTo BoxState.Collapsed -> spring(stiffness = 50f) else -> tween(durationMillis = 500) } }, label = "color" ) { state -> when (state) { BoxState.Collapsed -> MaterialTheme.colorScheme.primary BoxState.Expanded -> MaterialTheme.colorScheme.background } }
Lorsqu'une transition atteint l'état cible, Transition.currentState
est identique à Transition.targetState
. Cela permet de déterminer si la transition est terminée.
Parfois, nous avons besoin que l'état initial soit différent du premier état cible. Pour cela, nous pouvons utiliser updateTransition
avec MutableTransitionState
. Cela nous permet par exemple de démarrer l'animation dès que le code entre dans la composition.
// Start in collapsed state and immediately animate to expanded var currentState = remember { MutableTransitionState(BoxState.Collapsed) } currentState.targetState = BoxState.Expanded val transition = updateTransition(currentState, label = "box state") // ……
Pour une transition plus complexe impliquant plusieurs fonctions modulables, vous pouvez créer une transition enfant en utilisant createChildTransition
. Cette technique permet de séparer les problèmes entre plusieurs sous-composants dans un composable complexe. La transition parente connaît alors toutes les valeurs d'animation des transitions enfants.
enum class DialerState { DialerMinimized, NumberPad } @Composable fun DialerButton(isVisibleTransition: Transition<Boolean>) { // `isVisibleTransition` spares the need for the content to know // about other DialerStates. Instead, the content can focus on // animating the state change between visible and not visible. } @Composable fun NumberPad(isVisibleTransition: Transition<Boolean>) { // `isVisibleTransition` spares the need for the content to know // about other DialerStates. Instead, the content can focus on // animating the state change between visible and not visible. } @Composable fun Dialer(dialerState: DialerState) { val transition = updateTransition(dialerState, label = "dialer state") Box { // Creates separate child transitions of Boolean type for NumberPad // and DialerButton for any content animation between visible and // not visible NumberPad( transition.createChildTransition { it == DialerState.NumberPad } ) DialerButton( transition.createChildTransition { it == DialerState.DialerMinimized } ) } }
Utiliser la transition avec AnimatedVisibility
et AnimatedContent
AnimatedVisibility
et AnimatedContent
sont disponibles en tant que fonctions d'extension de Transition
. Le targetState
de Transition.AnimatedVisibility
et Transition.AnimatedContent
, dérivé de Transition
, déclenche au besoin des transitions d'entrée et de sortie lorsque le targetState
de Transition
change. Ces fonctions d'extension permettent de hisser dans Transition
toutes les animations enter/exit/sizeTransform qui seraient autrement contenues dans AnimatedVisibility
/AnimatedContent
.
Grâce à ces fonctions d'extension, le changement d'état de AnimatedVisibility
/AnimatedContent
peut être observé de l'extérieur. Au lieu d'un paramètre visible
booléen, cette version de AnimatedVisibility
accepte un lambda qui convertit l'état cible de la transition parente en booléen.
Pour en savoir plus, consultez les sections AnimatedVisibility et AnimatedContent.
var selected by remember { mutableStateOf(false) } // Animates changes when `selected` is changed. val transition = updateTransition(selected, label = "selected state") val borderColor by transition.animateColor(label = "border color") { isSelected -> if (isSelected) Color.Magenta else Color.White } val elevation by transition.animateDp(label = "elevation") { isSelected -> if (isSelected) 10.dp else 2.dp } Surface( onClick = { selected = !selected }, shape = RoundedCornerShape(8.dp), border = BorderStroke(2.dp, borderColor), elevation = elevation ) { Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) { Text(text = "Hello, world!") // AnimatedVisibility as a part of the transition. transition.AnimatedVisibility( visible = { targetSelected -> targetSelected }, enter = expandVertically(), exit = shrinkVertically() ) { Text(text = "It is fine today.") } // AnimatedContent as a part of the transition. transition.AnimatedContent { targetState -> if (targetState) { Text(text = "Selected") } else { Icon(imageVector = Icons.Default.Phone, contentDescription = "Phone") } } } }
Encapsuler une transition et la rendre réutilisable
Pour les cas d'utilisation simples, définir des animations de transition dans le même composable que l'interface utilisateur est une option parfaitement valide. Toutefois, lorsque vous travaillez sur un composant complexe comportant plusieurs valeurs animées, il peut être utile de séparer l'implémentation des animations du composable de l'UI.
Pour ce faire, vous devez créer une classe contenant toutes les valeurs d'animation et une fonction "update" renvoyant une instance de cette classe. L'implémentation de la transition peut être extraite dans cette nouvelle fonction. Ce modèle est utile lorsqu'il est nécessaire de centraliser la logique de l'animation ou de rendre des animations complexes réutilisables.
enum class BoxState { Collapsed, Expanded } @Composable fun AnimatingBox(boxState: BoxState) { val transitionData = updateTransitionData(boxState) // UI tree Box( modifier = Modifier .background(transitionData.color) .size(transitionData.size) ) } // Holds the animation values. private class TransitionData( color: State<Color>, size: State<Dp> ) { val color by color val size by size } // Create a Transition and return its animation values. @Composable private fun updateTransitionData(boxState: BoxState): TransitionData { val transition = updateTransition(boxState, label = "box state") val color = transition.animateColor(label = "color") { state -> when (state) { BoxState.Collapsed -> Color.Gray BoxState.Expanded -> Color.Red } } val size = transition.animateDp(label = "size") { state -> when (state) { BoxState.Collapsed -> 64.dp BoxState.Expanded -> 128.dp } } return remember(transition) { TransitionData(color, size) } }
Créer une animation en boucle avec rememberInfiniteTransition
InfiniteTransition
contient une ou plusieurs animations enfants, telles que Transition
, mais elles s'exécutent dès qu'elles entrent dans la composition et ne s'arrêtent que si elles sont supprimées. Vous pouvez créer une instance de InfiniteTransition
avec rememberInfiniteTransition
. Les animations enfants peuvent être ajoutées avec animateColor
, animatedFloat
ou animatedValue
. Vous devez également ajouter un infiniteRepeatable pour définir les spécifications de l'animation.
val infiniteTransition = rememberInfiniteTransition() val color by infiniteTransition.animateColor( initialValue = Color.Red, targetValue = Color.Green, animationSpec = infiniteRepeatable( animation = tween(1000, easing = LinearEasing), repeatMode = RepeatMode.Reverse ) ) Box(Modifier.fillMaxSize().background(color))
API d'animation de niveau inférieur
Toutes les API d'animation de niveau supérieur mentionnées dans la section précédente reposent sur les API d'animation de niveau inférieur.
Les fonctions animate*AsState
comptent parmi les API les plus simples. Elles permettent de rendre un changement de valeur instantané sous la forme d'une valeur d'animation. Elles reposent sur Animatable
, une API basée sur des coroutines permettant d'animer une seule valeur. updateTransition
crée un objet de transition capable de gérer plusieurs valeurs d'animation et de les exécuter en fonction d'un changement d'état. La fonction rememberInfiniteTransition
est similaire, mais elle crée une transition infinie qui peut gérer plusieurs animations s'exécutant indéfiniment. Ces API sont toutes des composables (sauf Animatable
), ce qui signifie que ces animations peuvent être créées en dehors de la composition.
Toutes ces API reposent sur l'API Animation
, plus basique. Bien que la plupart des applications n'interagissent pas directement avec Animation
, certaines fonctionnalités de personnalisation de Animation
sont disponibles via des API de niveau supérieur. Pour en savoir plus sur AnimationVector
et AnimationSpec
, consultez la section Personnaliser les animations.
Animatable
: animation à valeur unique basée sur une coroutine
Animatable
est un conteneur de valeur qui peut animer la valeur lorsqu'elle est modifiée via animateTo
. Cette API sauvegarde l'implémentation de animate*AsState
.
Cela garantit une continuité cohérente et l'exclusivité mutuelle, ce qui signifie que le changement de valeur est toujours continu et que toute animation en cours sera annulée.
De nombreuses fonctionnalités de Animatable
, y compris animateTo
, sont fournies en tant que fonctions de suspension. Par conséquent, elles doivent être encapsulées dans une portée de coroutine appropriée. Par exemple, vous pouvez utiliser le composable LaunchedEffect
pour créer une portée spécifique pour la durée de la valeur de clé spécifiée.
// Start out gray and animate to green/red based on `ok` val color = remember { Animatable(Color.Gray) } LaunchedEffect(ok) { color.animateTo(if (ok) Color.Green else Color.Red) } Box(Modifier.fillMaxSize().background(color.value))
Dans l'exemple ci-dessus, nous créons et mémorisons une instance de Animatable
avec la valeur initiale Color.Gray
. En fonction de la valeur de l'indicateur booléen ok
, la couleur s'anime pour devenir Color.Green
ou Color.Red
. Toute modification ultérieure de la valeur booléenne lance l'animation vers l'autre couleur. Si une animation est en cours au moment du changement de valeur, elle est annulée et la nouvelle animation démarre à partir de la valeur d'instantané actuelle, avec la vitesse actuelle.
Cette implémentation de l'animation constitue la base de l'API animate*AsState
, comme mentionné dans la section précédente. Comparé à animate*AsState
, en utilisant directement Animatable
, nous pouvons contrôler précisément plusieurs aspects. Tout d'abord, Animatable
peut avoir une valeur initiale différente de sa première valeur cible.
Par exemple, l'exemple de code ci-dessus affiche une zone grise au début, qui commence immédiatement à s'animer en vert ou en rouge. Ensuite, Animatable
permet de réaliser des opérations supplémentaires sur la valeur du contenu, à savoir snapTo
et animateDecay
. snapTo
définit immédiatement la valeur actuelle sur la valeur cible. Cette option est utile lorsque l'animation elle-même n'est pas la seule source fiable et doit être synchronisée avec d'autres états, tels que des événements tactiles. animateDecay
démarre une animation qui ralentit à partir d'une vitesse donnée. Cette opération permet d'implémenter le comportement de glissement d'un geste vif. Pour en savoir plus, consultez la section Gestes et animations.
Animatable
est directement compatible avec Float
et Color
, mais vous pouvez utiliser n'importe quel type de données en fournissant un TwoWayConverter
. Pour en savoir plus, consultez la section AnimationVector.
Il est possible de personnaliser les spécifications de l'animation en fournissant un AnimationSpec
.
Pour en savoir plus, consultez la section AnimationSpec.
Animation
: animation contrôlée manuellement
Animation
est l'API d'animation de niveau le plus bas. La plupart des animations que nous avons vues jusqu'à présent s'appuient sur Animation. Animation
propose deux sous-types :
TargetBasedAnimation
et DecayAnimation
.
Animation
doit uniquement servir à contrôler manuellement le timing de l'animation.
Animation
est sans état et n'est associé à aucun cycle de vie. Il sert de moteur de calcul des animations pour les API de niveau supérieur.
TargetBasedAnimation
Les autres API couvrent la plupart des cas d'utilisation, mais en utilisant directement TargetBasedAnimation
, vous pouvez contrôler vous-même le temps de lecture de l'animation. Dans l'exemple ci-dessous, le temps de lecture de TargetAnimation
est contrôlé manuellement en fonction du temps de rendu fourni par withFrameNanos
.
val anim = remember { TargetBasedAnimation( animationSpec = tween(200), typeConverter = Float.VectorConverter, initialValue = 200f, targetValue = 1000f ) } var playTime by remember { mutableStateOf(0L) } LaunchedEffect(anim) { val startTime = withFrameNanos { it } do { playTime = withFrameNanos { it } - startTime val animationValue = anim.getValueFromNanos(playTime) } while (someCustomCondition()) }
DecayAnimation
Contrairement à TargetBasedAnimation
, DecayAnimation
ne nécessite pas de targetValue
. Il calcule en effet sa valeur targetValue
en fonction des conditions de départ, telles que définies par initialVelocity
et initialValue
et par la valeur DecayAnimationSpec
fournie.
Les animations de décomposition sont souvent utilisées après un glissement d'un geste vif afin de ralentir le mouvement des éléments jusqu'à leur arrêt. La vitesse d'animation commence à la valeur définie par initialVelocityVector
et ralentit au fil du temps.
Recommandations personnalisées
- Remarque : Le texte du lien s'affiche lorsque JavaScript est désactivé.
- Personnaliser des animations {:#customize-animations}
- Animations dans Compose
- Modificateurs d'animation et composables