Jetpack Compose fournit des API puissantes et extensibles qui facilitent l'implémentation de diverses animations dans l'interface utilisateur de votre application. Ce document explique comment utiliser ces API et laquelle choisir en fonction de votre scénario d'animation.
Présentation
Dans une application mobile moderne, les animations sont essentielles pour offrir une expérience utilisateur fluide et compréhensible. De nombreuses API Animation Jetpack Compose sont disponibles en tant que fonctions modulables, tout comme les mises en page et d'autres éléments d'interface. Elles reposent sur des API de niveau inférieur intégrant des fonctions de suspension de coroutines Kotlin. Ce guide présente d'abord les API de niveau supérieur utiles dans de nombreux cas pratiques, puis décrit les API de niveau inférieur qui vous offrent davantage de contrôle et une personnalisation plus avancée.
Le diagramme ci-dessous vous aide à décider quelle API utiliser pour implémenter votre animation.
- Si vous animez un changement de contenu dans la mise en page :
- Si vous animez l'apparition et la disparition du contenu :
- Utilisez
AnimatedVisibility
.
- Utilisez
- Si le contenu est remplacé en fonction de l'état :
- Si vous effectuez un fondu enchaîné :
- Utilisez
Crossfade
.
- Utilisez
- Sinon, utilisez
AnimatedContent
.
- Si vous effectuez un fondu enchaîné :
- Sinon, utilisez
Modifier.animateContentSize
.
- Si vous animez l'apparition et la disparition du contenu :
- Si l'animation est basée sur l'état :
- Si l'animation se produit pendant la composition :
- Si l'animation est infinie :
- Utilisez
rememberInfiniteTransition
.
- Utilisez
- Si vous animez plusieurs valeurs simultanément :
- Utilisez
updateTransition
.
- Utilisez
- Sinon, utilisez
animate*AsState
.
- Si l'animation est infinie :
- Si l'animation se produit pendant la composition :
- Si vous souhaitez contrôler précisément le timing de l'animation :
- Utilisez
Animation
, par exempleTargetBasedAnimation
ouDecayAnimation
.
- Utilisez
- Si l'animation est la seule source fiable :
- Utilisez
Animatable
.
- Utilisez
- Sinon, utilisez
AnimationState
ouanimate
.
API d'animation de niveau supérieur
Compose propose des API d'animation de niveau supérieur pour plusieurs modèles d'animation couramment utilisés dans de nombreuses applications. Ces API sont conçues pour s'aligner sur les bonnes pratiques du système Material Design Motion.
AnimatedVisibility
Le composable AnimatedVisibility
anime l'apparition et la disparition de son contenu.
var editable by remember { mutableStateOf(true) }
AnimatedVisibility(visible = editable) {
Text(text = "Edit")
}
Par défaut, le contenu apparaît en fondu et en s'agrandissant, et disparaît en fondu et en se rétrécissant. Vous pouvez personnaliser la transition en spécifiant EnterTransition
et ExitTransition
.
var visible by remember { mutableStateOf(true) }
val density = LocalDensity.current
AnimatedVisibility(
visible = visible,
enter = slideInVertically {
// Slide in from 40 dp from the top.
with(density) { -40.dp.roundToPx() }
} + expandVertically(
// Expand from the top.
expandFrom = Alignment.Top
) + fadeIn(
// Fade in with the initial alpha of 0.3f.
initialAlpha = 0.3f
),
exit = slideOutVertically() + shrinkVertically() + fadeOut()
) {
Text("Hello", Modifier.fillMaxWidth().height(200.dp))
}
Comme vous pouvez le voir dans l'exemple ci-dessus, il est possible de combiner plusieurs objets EnterTransition
ou ExitTransition
avec un opérateur +
. Chaque objet accepte des paramètres facultatifs permettant de personnaliser son comportement. Consultez la documentation de référence pour en savoir plus.
Exemples d'objets EnterTransition
et ExitTransition
AnimatedVisibility
propose également une variante qui accepte un MutableTransitionState
. Cela vous permet de déclencher une animation dès que AnimatedVisibility
est ajouté à l'arborescence de composition, et aussi d'observer l'état de l'animation.
// Create a MutableTransitionState<Boolean> for the AnimatedVisibility.
val state = remember {
MutableTransitionState(false).apply {
// Start the animation immediately.
targetState = true
}
}
Column {
AnimatedVisibility(visibleState = state) {
Text(text = "Hello, world!")
}
// Use the MutableTransitionState to know the current animation state
// of the AnimatedVisibility.
Text(
text = when {
state.isIdle && state.currentState -> "Visible"
!state.isIdle && state.currentState -> "Disappearing"
state.isIdle && !state.currentState -> "Invisible"
else -> "Appearing"
}
)
}
Animer l'entrée et la sortie des enfants
Le contenu dans AnimatedVisibility
(enfants directs ou indirects) peut utiliser le modificateur animateEnterExit
afin de spécifier un comportement d'animation différent pour chacun d'eux. L'effet visuel pour chacun de ces enfants combine les animations définies dans le composable AnimatedVisibility
et les animations d'entrée et de sortie de l'enfant.
AnimatedVisibility(
visible = visible,
enter = fadeIn(),
exit = fadeOut()
) {
// Fade in/out the background and the foreground.
Box(Modifier.fillMaxSize().background(Color.DarkGray)) {
Box(
Modifier
.align(Alignment.Center)
.animateEnterExit(
// Slide in/out the inner box.
enter = slideInVertically(),
exit = slideOutVertically()
)
.sizeIn(minWidth = 256.dp, minHeight = 64.dp)
.background(Color.Red)
) {
// Content of the notification…
}
}
}
Dans certains cas, vous souhaiterez peut-être que AnimatedVisibility
n'applique aucune animation afin que les enfants puissent avoir leurs propres animations avec animateEnterExit
. Pour ce faire, spécifiez EnterTransition.None
et ExitTransition.None
dans le composable AnimatedVisibility
.
Ajouter une animation personnalisée
Si vous souhaitez ajouter des effets d'animation personnalisés en plus des animations d'entrée et de sortie intégrées, accédez à l'instance Transition
sous-jacente via la propriété transition
dans le lambda de contenu de AnimatedVisibility
. Tous les états d'animation ajoutés à l'instance Transition s'exécuteront simultanément avec les animations d'entrée et de sortie de AnimatedVisibility
. AnimatedVisibility
attend que toutes les animations de Transition
soient terminées avant de supprimer son contenu.
Si des animations de sortie sont créées indépendamment de Transition
(par exemple, à l'aide de animate*AsState
), AnimatedVisibility
ne pourra pas les prendre en compte. Il risque donc de supprimer le composable de contenu avant la fin de ces animations.
AnimatedVisibility(
visible = visible,
enter = fadeIn(),
exit = fadeOut()
) { // this: AnimatedVisibilityScope
// Use AnimatedVisibilityScope#transition to add a custom animation
// to the AnimatedVisibility.
val background by transition.animateColor { state ->
if (state == EnterExitState.Visible) Color.Blue else Color.Gray
}
Box(modifier = Modifier.size(128.dp).background(background))
}
Pour en savoir plus sur Transition
, consultez la section updateTransition.
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).
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.
AnimatedContent (expérimental)
Le composable AnimatedContent
anime son contenu à mesure qu'il change en fonction d'un état cible.
Row {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Add")
}
AnimatedContent(targetState = count) { targetCount ->
// Make sure to use `targetCount`, not `count`.
Text(text = "Count: $targetCount")
}
}
Notez que vous devez toujours utiliser le paramètre lambda et l'appliquer au contenu. L'API utilise cette valeur comme clé pour identifier le contenu actuellement affiché.
Par défaut, le contenu initial disparaît en fondu, puis le contenu cible apparaît en fondu (ce comportement est appelé fondu total). Vous pouvez personnaliser ce comportement d'animation en spécifiant un objet ContentTransform
pour le paramètre transitionSpec
. Vous pouvez créer ContentTransform
en associant un EnterTransition
à un ExitTransition
à l'aide de la fonction infixe with
. Vous pouvez appliquer SizeTransform
au ContentTransform
en l'associant avec la fonction infixe using
.
AnimatedContent(
targetState = count,
transitionSpec = {
// Compare the incoming number with the previous number.
if (targetState > initialState) {
// If the target number is larger, it slides up and fades in
// while the initial (smaller) number slides up and fades out.
slideInVertically { height -> height } + fadeIn() with
slideOutVertically { height -> -height } + fadeOut()
} else {
// If the target number is smaller, it slides down and fades in
// while the initial number slides down and fades out.
slideInVertically { height -> -height } + fadeIn() with
slideOutVertically { height -> height } + fadeOut()
}.using(
// Disable clipping since the faded slide-in/out should
// be displayed out of bounds.
SizeTransform(clip = false)
)
}
) { targetCount ->
Text(text = "$targetCount")
}
EnterTransition
définit la façon dont le contenu cible doit apparaître, et ExitTransition
la manière dont le contenu initial doit disparaître. En plus de toutes les fonctions EnterTransition
et ExitTransition
disponibles pour AnimatedVisibility
, AnimatedContent
propose slideIntoContainer
et slideOutOfContainer
.
Il s'agit d'alternatives pratiques à slideInHorizontally/Vertically
et slideOutHorizontally/Vertically
pour calculer la distance de glissement en fonction de la taille du contenu initial et du contenu cible de AnimatedContent
.
SizeTransform
définit la manière dont la taille doit être animée entre le contenu initial et le contenu cible. Vous avez accès à la taille initiale et à la taille cible lorsque vous créez l'animation. SizeTransform
contrôle également si le contenu doit être rogné et adapté à la taille du composant durant les animations.
var expanded by remember { mutableStateOf(false) }
Surface(
color = MaterialTheme.colors.primary,
onClick = { expanded = !expanded }
) {
AnimatedContent(
targetState = expanded,
transitionSpec = {
fadeIn(animationSpec = tween(150, 150)) with
fadeOut(animationSpec = tween(150)) using
SizeTransform { initialSize, targetSize ->
if (targetState) {
keyframes {
// Expand horizontally first.
IntSize(targetSize.width, initialSize.height) at 150
durationMillis = 300
}
} else {
keyframes {
// Shrink vertically first.
IntSize(initialSize.width, targetSize.height) at 150
durationMillis = 300
}
}
}
}
) { targetExpanded ->
if (targetExpanded) {
Expanded()
} else {
ContentIcon()
}
}
}
Animer l'entrée et la sortie des enfants
Tout comme AnimatedVisibility
, le modificateur animateEnterExit
est disponible dans le lambda de contenu de AnimatedContent
. Il vous permet d'appliquer séparément EnterAnimation
et ExitAnimation
à chaque enfant direct ou indirect.
Ajouter une animation personnalisée
Tout comme AnimatedVisibility
, le champ transition
est disponible dans le lambda de contenu de AnimatedContent
. Il permet de créer un effet d'animation personnalisé qui s'exécute simultanément avec la transition AnimatedContent
. Pour en savoir plus, consultez la section updateTransition.
animateContentSize
Le modificateur animateContentSize
anime un changement de taille.
var message by remember { mutableStateOf("Hello") }
Box(
modifier = Modifier.background(Color.Blue).animateContentSize()
) {
Text(text = message)
}
Crossfade
Crossfade
exécute une animation de fondu enchaîné entre deux mises en page. Il permet de remplacer le contenu avec une animation de fondu enchaîné en basculant la valeur transmise au paramètre current
.
var currentPage by remember { mutableStateOf("A") }
Crossfade(targetState = currentPage) { screen ->
when (screen) {
"A" -> Text("Page A")
"B" -> Text("Page B")
}
}
updateTransition
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)
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 { state ->
when (state) {
BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f)
BoxState.Expanded -> Rect(100f, 100f, 300f, 300f)
}
}
val borderWidth by transition.animateDp { 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)
}
}
) { state ->
when (state) {
BoxState.Collapsed -> MaterialTheme.colors.primary
BoxState.Expanded -> MaterialTheme.colors.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)
// ...
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)
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 AnimatedVisibility et AnimatedContent pour une transition
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)
val borderColor by transition.animateColor { isSelected ->
if (isSelected) Color.Magenta else Color.White
}
val elevation by transition.animateDp { 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)
val color = transition.animateColor { state ->
when (state) {
BoxState.Collapsed -> Color.Gray
BoxState.Expanded -> Color.Red
}
}
val size = transition.animateDp { state ->
when (state) {
BoxState.Collapsed -> 64.dp
BoxState.Expanded -> 128.dp
}
}
return remember(transition) { TransitionData(color, size) }
}
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
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
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.
Personnaliser les animations
Bon nombre des API Animation acceptent des paramètres qui permettent de personnaliser leur comportement.
AnimationSpec
La plupart des API d'animation permettent aux développeurs de personnaliser les spécifications d'animation à l'aide d'un paramètre AnimationSpec
facultatif.
val alpha: Float by animateFloatAsState(
targetValue = if (enabled) 1f else 0.5f,
// Configure the animation duration and easing.
animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
)
Il existe plusieurs types de AnimationSpec
, qui permettent de créer autant d'animations différentes.
spring
spring
crée une animation basée sur des mécanismes physiques entre les valeurs de début et de fin. Deux paramètres sont acceptés : dampingRatio
et stiffness
.
dampingRatio
définit la force du rebond. La valeur par défaut est Spring.DampingRatioNoBouncy
.
stiffness
définit la vitesse à laquelle le ressort doit se déplacer vers la valeur de fin. La valeur par défaut est Spring.StiffnessMedium
.
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioHighBouncy,
stiffness = Spring.StiffnessMedium
)
)
spring
permet de gérer les interruptions plus en douceur par rapport aux types AnimationSpec
basés sur la durée, car il maintient la vitesse lorsque la valeur cible change entre les animations. De nombreuses API d'animation, dont animate*AsState
et updateTransition
, utilisent spring
comme AnimationSpec par défaut.
tween
tween
crée une animation entre les valeurs de début et de fin durant la période durationMillis
spécifiée en suivant une courbe de lissage de vitesse. Pour en savoir plus, consultez la section Easing. Vous pouvez également spécifier delayMillis
pour reporter le début de l'animation.
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = tween(
durationMillis = 300,
delayMillis = 50,
easing = LinearOutSlowInEasing
)
)
keyframes
keyframes
crée une animation en fonction des valeurs d'instantané spécifiées à différents horodatages pendant la durée de l'animation. À un moment donné, la valeur d'animation est interpolée entre deux valeurs d'image clé. Vous pouvez spécifier la courbe d'interpolation de chacune de ces images clés en utilisant Easing.
De manière facultative, vous pouvez spécifier les valeurs à 0 ms et pour toute la durée. Dans le cas contraire, elles seront définies par défaut sur les valeurs de début et de fin de l'animation, respectivement.
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = keyframes {
durationMillis = 375
0.0f at 0 with LinearOutSlowInEasing // for 0-15 ms
0.2f at 15 with FastOutLinearInEasing // for 15-75 ms
0.4f at 75 // ms
0.4f at 225 // ms
}
)
repeatable
repeatable
exécute plusieurs fois une animation basée sur la durée (par exemple, tween
ou keyframes
) jusqu'à atteindre le nombre d'itérations spécifié. Vous pouvez transmettre le paramètre repeatMode
pour indiquer si l'animation doit être répétée en commençant par le début (RepeatMode.Restart
) ou par la fin (RepeatMode.Reverse
).
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = repeatable(
iterations = 3,
animation = tween(durationMillis = 300),
repeatMode = RepeatMode.Reverse
)
)
infiniteRepeatable
infiniteRepeatable
est semblable à repeatable
, mais l'animation se répète indéfiniment.
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 300),
repeatMode = RepeatMode.Reverse
)
)
Dans les tests avec ComposeTestRule
, les animations qui utilisent infiniteRepeatable
ne sont pas exécutées. La valeur initiale de chaque valeur animée sera utilisée pour afficher le composant.
snap
snap
est un AnimationSpec
spécial qui remplace immédiatement la valeur par la valeur de fin. Vous pouvez spécifier delayMillis
pour retarder le début de l'animation.
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = snap(delayMillis = 50)
)
Easing
Les opérations AnimationSpec
basées sur la durée (telles que tween
ou keyframes
) utilisent Easing
pour ajuster une fraction d'une animation. Cela permet à la valeur d'animation d'accélérer et de ralentir plutôt que de se déplacer à une vitesse constante. La fraction est une valeur comprise entre 0 (début) et 1.0 (fin). Elle indique le point actuellement atteint dans l'animation.
En réalité, Easing est une fonction qui accepte une valeur de fraction comprise entre 0 et 1.0 et renvoie un float. La valeur renvoyée peut se trouver en dehors des limites pour représenter un dépassement vers le haut ou vers le bas. Vous pouvez créer un Easing personnalisé comme dans le code ci-dessous.
val CustomEasing = Easing { fraction -> fraction * fraction }
@Composable
fun EasingUsage() {
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = tween(
durationMillis = 300,
easing = CustomEasing
)
)
// ...
}
Compose intègre plusieurs fonctions Easing
qui couvrent la plupart des cas d'utilisation.
Pour déterminer quel lissage de vitesse utiliser selon votre scénario, consultez la section sur la vitesse dans Material Design.
FastOutSlowInEasing
LinearOutSlowInEasing
FastOutLinearEasing
LinearEasing
CubicBezierEasing
- Autres fonctions
AnimationVector
La plupart des API d'animation Compose acceptent les types Float
, Color
et Dp
, ainsi que d'autres types de données de base en tant que valeurs d'animation. Toutefois, vous devez parfois animer d'autres types de données, y compris des types personnalisés. Pendant l'animation, les valeurs d'animation sont représentées par un AnimationVector
. Elles sont converties en AnimationVector
et inversement par un TwoWayConverter
associé afin que le système d'animation principal puisse les gérer de manière uniforme. Par exemple, Int
est représenté par un AnimationVector1D
contenant une seule valeur flottante.
Le TwoWayConverter
d'un Int
se présente comme suit :
val IntToVector: TwoWayConverter<Int, AnimationVector1D> =
TwoWayConverter({ AnimationVector1D(it.toFloat()) }, { it.value.toInt() })
Color
est essentiellement un ensemble de quatre valeurs (rouge, vert, bleu et alpha). Color
est donc converti en un AnimationVector4D
qui contient quatre floats. Ainsi, chaque type de données utilisé dans les animations est converti en AnimationVector1D
, AnimationVector2D
, AnimationVector3D
ou AnimationVector4D
en fonction de sa dimensionnalité. Cela permet d'animer indépendamment les différents composants de l'objet, chacun avec son propre suivi de la vitesse. Vous pouvez accéder aux convertisseurs intégrés des types de données de base à l'aide de Color.VectorConverter
, Dp.VectorConverter
, etc.
Lorsque vous souhaitez proposer un nouveau type de données en tant que valeur d'animation, créez votre TwoWayConverter
et fournissez-le à l'API. Par exemple, vous pouvez utiliser animateValueAsState
pour animer votre type de données personnalisé comme suit :
data class MySize(val width: Dp, val height: Dp)
@Composable
fun MyAnimation(targetSize: MySize) {
val animSize: MySize by animateValueAsState<MySize, AnimationVector2D>(
targetSize,
TwoWayConverter(
convertToVector = { size: MySize ->
// Extract a float value from each of the `Dp` fields.
AnimationVector2D(size.width.value, size.height.value)
},
convertFromVector = { vector: AnimationVector2D ->
MySize(vector.v1.dp, vector.v2.dp)
}
)
)
}
Ressources de vecteur animé (expérimental)
Pour utiliser une ressource AnimatedVectorDrawable
, chargez le fichier drawable à l'aide de animatedVectorResource
et transmettez un boolean
pour basculer entre les états de début et de fin de votre drawable.
@Composable
fun AnimatedVectorDrawable() {
val image = AnimatedImageVector.animatedVectorResource(R.drawable.ic_hourglass_animated)
var atEnd by remember { mutableStateOf(false) }
Image(
painter = rememberAnimatedVectorPainter(image, atEnd),
contentDescription = "Timer",
modifier = Modifier.clickable {
atEnd = !atEnd
},
contentScale = ContentScale.Crop
)
}
Pour en savoir plus sur le format de votre fichier drawable, consultez Animer des images Drawable.
Animer des éléments de liste
Si vous souhaitez animer la réorganisation des éléments d'une liste ou d'une grille différée, consultez la documentation sur l'animation des éléments d'une mise en page différée.
Gestes et animations (avancé)
Plusieurs points sont à prendre en compte lorsque nous utilisons des événements tactiles et des animations, et pas seulement des animations. Tout d'abord, nous devrons peut-être interrompre une animation en cours lorsque les événements tactiles commencent, car la priorité doit être donnée à l'interaction de l'utilisateur.
Dans l'exemple ci-dessous, nous utilisons un Animatable
pour représenter la position décalée d'un composant de cercle. Les événements tactiles sont traités avec le modificateur pointerInput
. Lorsque nous détectons un nouvel événement d'appui, nous appelons animateTo
pour animer la valeur de décalage jusqu'à la position de l'appui. Un événement d'appui peut également se produire pendant l'animation. Dans ce cas, animateTo
interrompt l'animation en cours et la lance à la nouvelle position cible, tout en maintenant la vitesse de l'animation interrompue.
@Composable
fun Gesture() {
val offset = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) }
Box(
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
coroutineScope {
while (true) {
// Detect a tap event and obtain its position.
val position = awaitPointerEventScope {
awaitFirstDown().position
}
launch {
// Animate to the tap position.
offset.animateTo(position)
}
}
}
}
) {
Circle(modifier = Modifier.offset { offset.value.toIntOffset() })
}
}
private fun Offset.toIntOffset() = IntOffset(x.roundToInt(), y.roundToInt())
Il est aussi courant de devoir synchroniser les valeurs d'animation avec les valeurs issues d'événements tactiles, tels qu'un déplacement. Dans l'exemple ci-dessous, l'animation "Balayer pour fermer la vue" est implémentée en tant que Modifier
(au lieu d'utiliser le composable SwipeToDismiss
). Le décalage horizontal de l'élément est représenté par un Animatable
. Cette API présente une caractéristique utile dans les animations par gestes. Sa valeur peut être modifiée par les événements tactiles et l'animation. Lorsque nous recevons un événement tactile, nous arrêtons Animatable
par la méthode stop
afin d'interrompre toute animation en cours.
Lors d'un événement de déplacement, nous utilisons snapTo
pour mettre à jour la valeur Animatable
avec la valeur calculée à partir des événements tactiles. Pour le glissement d'un geste vif, Compose fournit VelocityTracker
afin d'enregistrer les événements de déplacement et de calculer la vitesse. La vitesse peut être transmise directement à animateDecay
pour cette animation de glissement. Lorsque nous voulons faire glisser à nouveau la valeur décalée vers sa position d'origine, nous spécifions la valeur décalée cible de 0f
avec la méthode animateTo
.
fun Modifier.swipeToDismiss(
onDismissed: () -> Unit
): Modifier = composed {
val offsetX = remember { Animatable(0f) }
pointerInput(Unit) {
// Used to calculate fling decay.
val decay = splineBasedDecay<Float>(this)
// Use suspend functions for touch events and the Animatable.
coroutineScope {
while (true) {
// Detect a touch down event.
val pointerId = awaitPointerEventScope { awaitFirstDown().id }
val velocityTracker = VelocityTracker()
// Stop any ongoing animation.
offsetX.stop()
awaitPointerEventScope {
horizontalDrag(pointerId) { change ->
// Update the animation value with touch events.
launch {
offsetX.snapTo(
offsetX.value + change.positionChange().x
)
}
velocityTracker.addPosition(
change.uptimeMillis,
change.position
)
}
}
// No longer receiving touch events. Prepare the animation.
val velocity = velocityTracker.calculateVelocity().x
val targetOffsetX = decay.calculateTargetValue(
offsetX.value,
velocity
)
// The animation stops when it reaches the bounds.
offsetX.updateBounds(
lowerBound = -size.width.toFloat(),
upperBound = size.width.toFloat()
)
launch {
if (targetOffsetX.absoluteValue <= size.width) {
// Not enough velocity; Slide back.
offsetX.animateTo(
targetValue = 0f,
initialVelocity = velocity
)
} else {
// The element was swiped away.
offsetX.animateDecay(velocity, decay)
onDismissed()
}
}
}
}
}
.offset { IntOffset(offsetX.value.roundToInt(), 0) }
}
Tests
Compose propose ComposeTestRule
, qui vous permet d'écrire des tests pour les animations de manière déterministe en contrôlant totalement l'horloge de test. Cela vous permet de vérifier les valeurs d'animation intermédiaires. En outre, un test peut s'exécuter plus rapidement que la durée réelle de l'animation.
ComposeTestRule
expose son horloge de test en tant que mainClock
. Vous pouvez définir la propriété autoAdvance
sur "false" pour contrôler l'horloge dans votre code de test. Après avoir lancé l'animation à tester, vous pouvez avancer l'horloge avec advanceTimeBy
.
Avec advanceTimeBy
, vous n'avancez pas exactement l'horloge selon la durée spécifiée. L'heure est arrondie à la durée la plus proche, qui est un multiplicateur de la durée de l'image.
@get:Rule
val rule = createComposeRule()
@Test
fun testAnimationWithClock() {
// Pause animations
rule.mainClock.autoAdvance = false
var enabled by mutableStateOf(false)
rule.setContent {
val color by animateColorAsState(
targetValue = if (enabled) Color.Red else Color.Green,
animationSpec = tween(durationMillis = 250)
)
Box(Modifier.size(64.dp).background(color))
}
// Initiate the animation.
enabled = true
// Let the animation proceed.
rule.mainClock.advanceTimeBy(50L)
// Compare the result with the image showing the expected result.
// `assertAgainGolden` needs to be implemented in your code.
rule.onRoot().captureToImage().assertAgainstGolden()
}
Outils compatibles
Android Studio permet d'inspecter updateTransition
et animatedVisibility
dans Aperçu de l'animation. Cela vous permet de :
- Prévisualiser une transition image par image
- Inspecter les valeurs de toutes les animations de la transition
- Obtenir un aperçu d'une transition entre les états initiaux et cibles
- Inspecter et coordonner plusieurs animations à la fois
Lorsque vous lancez l'aperçu de l'animation, le volet "Animations" s'affiche. Vous pouvez y exécuter toutes les transitions incluses dans l'aperçu. Un nom par défaut est attribué à la transition ainsi qu'à chacune de ses valeurs d'animation. Vous pouvez le personnaliser en spécifiant le paramètre label
dans updateTransition
et dans les fonctions AnimatedVisibility
. Pour en savoir plus, consultez la page Aperçu de l'animation.
En savoir plus
Pour en savoir plus sur les animations dans Jetpack Compose, consultez les ressources supplémentaires suivantes :
Exemples
Articles de blog
Ateliers de programmation
Vidéos
- Reimagine animations system (Repenser le système d'animations)