1. Introduction
Les animations sont un excellent moyen d'améliorer l'expérience utilisateur de votre application, de lui communiquer des informations importantes et de la rendre plus soignée et agréable à utiliser.
Présentation du framework d'animation de Flutter
Flutter affiche des effets d'animation en reconstruisant une partie de l'arborescence des widgets à chaque frame. Il fournit des effets d'animation prédéfinis et d'autres API pour faciliter la création et la composition d'animations.
- Les animations implicites sont des effets d'animation prédéfinis qui exécutent automatiquement l'ensemble de l'animation. Lorsque la valeur cible de l'animation change, l'animation s'exécute de la valeur actuelle à la valeur cible, et affiche chaque valeur entre les deux afin que le widget s'anime de manière fluide.
AnimatedSize
,AnimatedScale
etAnimatedPositioned
sont des exemples d'animations implicites. - Les animations explicites sont également des effets d'animation prédéfinis, mais elles nécessitent un objet
Animation
pour fonctionner. Par exemple,SizeTransition
,ScaleTransition
ouPositionedTransition
. - Animation est une classe qui représente une animation en cours d'exécution ou arrêtée. Elle se compose d'une valeur représentant la valeur cible vers laquelle l'animation s'exécute et de l'état, qui représente la valeur actuelle que l'animation affiche à l'écran à un moment donné. Il s'agit d'une sous-classe de
Listenable
et il avertit ses écouteurs lorsque l'état change pendant l'exécution de l'animation. - AnimationController permet de créer une animation et de contrôler son état. Ses méthodes telles que
forward()
,reset()
,stop()
etrepeat()
peuvent être utilisées pour contrôler l'animation sans avoir à définir l'effet d'animation affiché, comme l'échelle, la taille ou la position. - Les animations de transition permettent d'interpoler des valeurs entre une valeur de début et une valeur de fin. Elles peuvent représenter n'importe quel type, comme un double,
Offset
ouColor
. - Les courbes permettent d'ajuster le taux de variation d'un paramètre au fil du temps. Lorsqu'une animation s'exécute, il est courant d'appliquer une courbe d'accélération pour accélérer ou ralentir la fréquence de changement au début ou à la fin de l'animation. Les courbes acceptent une valeur d'entrée comprise entre 0,0 et 1,0 et renvoient une valeur de sortie comprise entre 0,0 et 1,0.
Objectifs de l'atelier
Dans cet atelier de programmation, vous allez créer un jeu de quiz à choix multiples qui présente différents effets et techniques d'animation.
Vous allez découvrir comment :
- Créer un widget qui anime sa taille et sa couleur
- Créer un effet de retournement de carte en 3D
- Utiliser des effets d'animation prédéfinis sophistiqués du package d'animations
- Ajouter la prise en charge de la prévisualisation du geste Retour disponible sur la dernière version d'Android
Points abordés
Dans cet atelier de programmation, vous allez apprendre:
- Utiliser des effets d'animation implicites pour créer des animations de qualité sans avoir à écrire beaucoup de code
- Utiliser des effets animés explicites pour configurer vos propres effets à l'aide de widgets animés prédéfinis tels que
AnimatedSwitcher
ouAnimationController
. - Utiliser
AnimationController
pour définir votre propre widget qui affiche un effet 3D - Utiliser le package
animations
pour afficher des effets d'animation sophistiqués avec une configuration minimale
Prérequis
- Le SDK Flutter
- Un IDE, tel que VSCode ou Android Studio / IntelliJ
2. Configurer votre environnement de développement Flutter
Pour cet atelier, vous avez besoin de deux logiciels : le SDK Flutter et un éditeur.
Vous pouvez exécuter l'atelier de programmation sur l'un des appareils suivants :
- Un appareil Android (recommandé pour implémenter la prévisualisation du Retour à l'étape 7) ou iOS physique connecté à votre ordinateur et réglé en mode développeur
- Le simulateur iOS (les outils Xcode doivent être installés)
- Android Emulator (à configurer dans Android Studio)
- Un navigateur (Chrome est requis pour le débogage)
- Un ordinateur de bureau Windows, Linux ou macOS Vous devez développer votre application sur la plate-forme où vous comptez la déployer. Par exemple, si vous voulez développer une application de bureau Windows, vous devez le faire sous Windows pour accéder à la chaîne de compilation appropriée. Les exigences spécifiques aux systèmes d'exploitation sont détaillées sur docs.flutter.dev/desktop.
Vérifier votre installation
Pour vérifier que votre SDK Flutter est correctement configuré et que vous avez installé au moins l'une des plates-formes cibles ci-dessus, utilisez l'outil Flutter Doctor:
$ flutter doctor Doctor summary (to see all details, run flutter doctor -v): [✓] Flutter (Channel stable, 3.24.2, on macOS 14.6.1 23G93 darwin-arm64, locale en) [✓] Android toolchain - develop for Android devices [✓] Xcode - develop for iOS and macOS [✓] Chrome - develop for the web [✓] Android Studio [✓] IntelliJ IDEA Ultimate Edition [✓] VS Code [✓] Connected device (4 available) [✓] Network resources • No issues found!
3. Exécuter l'application de démarrage
Télécharger l'application de démarrage
Utilisez git
pour cloner l'application de démarrage à partir du dépôt flutter/samples sur GitHub.
$ git clone https://github.com/flutter/codelabs.git $ cd codelabs/animations/step_01/
Vous pouvez également télécharger le code source au format ZIP.
Exécuter l'application
Pour exécuter l'application, utilisez la commande flutter run
et spécifiez un appareil cible, tel que android
, ios
ou chrome
. Pour obtenir la liste complète des plates-formes compatibles, consultez la page Plates-formes compatibles.
$ flutter run -d android
Vous pouvez également exécuter et déboguer l'application à l'aide de l'IDE de votre choix. Pour en savoir plus, consultez la documentation officielle de Flutter.
À la découverte du code
L'application de démarrage est un jeu de quiz à choix multiples qui se compose de deux écrans suivant le modèle de conception MVVM (Model-View-View-Model). QuestionScreen
(vue) utilise la classe QuizViewModel
(view-model) pour poser à l'utilisateur des questions à choix multiples à partir de la classe QuestionBank
(modèle).
- home_screen.dart : affiche un écran avec un bouton Nouveau jeu
- main.dart : configure
MaterialApp
pour utiliser Material 3 et afficher l'écran d'accueil - model.dart : définit les classes principales utilisées dans l'application
- question_screen.dart : affiche l'UI du jeu-questionnaire
- view_model.dart : stocke l'état et la logique du jeu de quiz, affichés par
QuestionScreen
.
L'application n'est pas encore compatible avec les effets animés, à l'exception de la transition de vue par défaut affichée par la classe Navigator
de Flutter lorsque l'utilisateur appuie sur le bouton Nouveau jeu.
4. Utiliser des effets d'animation implicites
Les animations implicites sont un excellent choix dans de nombreuses situations, car elles ne nécessitent aucune configuration spéciale. Dans cette section, vous allez mettre à jour le widget StatusBar
pour qu'il affiche un tableau de scores animé. Pour trouver des effets d'animation implicites courants, consultez la documentation de l'API ImplicitlyAnimatedWidget.
Créer le widget de tableau de scores non animé
Créez un fichier lib/scoreboard.dart
avec le code suivant:
lib/scoreboard.dart
import 'package:flutter/material.dart';
class Scoreboard extends StatelessWidget {
final int score;
final int totalQuestions;
const Scoreboard({
super.key,
required this.score,
required this.totalQuestions,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (var i = 0; i < totalQuestions; i++)
Icon(
Icons.star,
size: 50,
color:
score < i + 1 ? Colors.grey.shade400 : Colors.yellow.shade700,
)
],
),
);
}
}
Ajoutez ensuite le widget Scoreboard
dans les enfants du widget StatusBar
, en remplaçant les widgets Text
qui affichaient auparavant le score et le nombre total de questions. Votre éditeur doit ajouter automatiquement les import "scoreboard.dart"
requises en haut du fichier.
lib/question_screen.dart
class StatusBar extends StatelessWidget {
final QuizViewModel viewModel;
const StatusBar({required this.viewModel, super.key});
@override
Widget build(BuildContext context) {
return Card(
elevation: 4,
child: Padding(
padding: EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Scoreboard( // NEW
score: viewModel.score, // NEW
totalQuestions: viewModel.totalQuestions, // NEW
),
],
),
),
);
}
}
Ce widget affiche une icône en forme d'étoile pour chaque question. Lorsqu'une question est répondue correctement, une autre étoile s'allume instantanément, sans animation. Dans les étapes suivantes, vous allez indiquer à l'utilisateur que son score a changé en animant sa taille et sa couleur.
Utiliser un effet d'animation implicite
Créez un widget appelé AnimatedStar
qui utilise un widget AnimatedScale
pour modifier la valeur scale
de 0.5
à 1.0
lorsque l'étoile devient active:
lib/scoreboard.dart
import 'package:flutter/material.dart';
class Scoreboard extends StatelessWidget {
final int score;
final int totalQuestions;
const Scoreboard({
super.key,
required this.score,
required this.totalQuestions,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (var i = 0; i < totalQuestions; i++)
AnimatedStar( // NEW
isActive: score > i, // NEW
) // NEW
],
),
);
}
}
class AnimatedStar extends StatelessWidget { // Add from here...
final bool isActive;
final Duration _duration = const Duration(milliseconds: 1000);
final Color _deactivatedColor = Colors.grey.shade400;
final Color _activatedColor = Colors.yellow.shade700;
AnimatedStar({super.key, required this.isActive});
@override
Widget build(BuildContext context) {
return AnimatedScale(
scale: isActive ? 1.0 : 0.5,
duration: _duration,
child: Icon(
Icons.star,
size: 50,
color: isActive ? _activatedColor : _deactivatedColor,
),
);
}
} // To here.
Désormais, lorsque l'utilisateur répond correctement à une question, le widget AnimatedStar
met à jour sa taille à l'aide d'une animation implicite. Le color
de Icon
n'est pas animé ici, mais uniquement le scale
, ce qui est effectué par le widget AnimatedScale
.
Utiliser un Tween pour interpoler entre deux valeurs
Notez que la couleur du widget AnimatedStar
change immédiatement après que le champ isActive
est défini sur "true".
Pour obtenir un effet de couleur animé, vous pouvez essayer d'utiliser un widget AnimatedContainer
(qui est une autre sous-classe de ImplicitlyAnimatedWidget
), car il peut animer automatiquement tous ses attributs, y compris la couleur. Malheureusement, notre widget doit afficher une icône, et non un conteneur.
Vous pouvez également essayer AnimatedIcon
, qui implémente des effets de transition entre les formes des icônes. Toutefois, il n'existe pas d'implémentation par défaut d'une icône en forme d'étoile dans la classe AnimatedIcons
.
Nous allons plutôt utiliser une autre sous-classe de ImplicitlyAnimatedWidget
appelée TweenAnimationBuilder
, qui utilise un Tween
comme paramètre. Un tween est une classe qui prend deux valeurs (begin
et end
) et calcule les valeurs intermédiaires afin qu'une animation puisse les afficher. Dans cet exemple, nous allons utiliser un ColorTween
, qui répond à l'interface Tween<Color>
requise pour créer notre effet d'animation.
Sélectionnez le widget Icon
et utilisez l'action rapide "Encapsuler avec un générateur" dans votre IDE. Remplacez le nom par TweenAnimationBuilder
. Indiquez ensuite la durée et ColorTween
.
lib/scoreboard.dart
class AnimatedStar extends StatelessWidget {
final bool isActive;
final Duration _duration = const Duration(milliseconds: 1000);
final Color _deactivatedColor = Colors.grey.shade400;
final Color _activatedColor = Colors.yellow.shade700;
AnimatedStar({super.key, required this.isActive});
@override
Widget build(BuildContext context) {
return AnimatedScale(
scale: isActive ? 1.0 : 0.5,
duration: _duration,
child: TweenAnimationBuilder( // Add from here...
duration: _duration,
tween: ColorTween(
begin: _deactivatedColor,
end: isActive ? _activatedColor : _deactivatedColor,
),
builder: (context, value, child) { // To here.
return Icon(
Icons.star,
size: 50,
color: value, // Modify from here...
);
}, // To here.
),
);
}
}
Effectuez maintenant un hot reload de l'application pour voir la nouvelle animation.
Notez que la valeur end
de notre ColorTween
change en fonction de la valeur du paramètre isActive
. En effet, TweenAnimationBuilder
réexécute son animation chaque fois que la valeur Tween.end
change. Dans ce cas, la nouvelle animation s'exécute de la valeur d'animation actuelle à la nouvelle valeur de fin, ce qui vous permet de modifier la couleur à tout moment (même pendant l'exécution de l'animation) et d'afficher un effet d'animation fluide avec les valeurs intermédiaires correctes.
Appliquer une courbe
Ces deux effets d'animation s'exécutent à une vitesse constante, mais les animations sont souvent plus intéressantes et informatives visuellement lorsqu'elles s'accélèrent ou se ralentissent.
Un Curve
applique une fonction d'atténuation, qui définit le taux de variation d'un paramètre au fil du temps. Flutter est fourni avec une collection de courbes d'atténuation prédéfinies dans la classe Curves
, telles que easeIn
ou easeOut
.
Ces diagrammes (disponibles sur la page de documentation de l'API Curves
) donnent un indice sur le fonctionnement des courbes. Les courbes convertissent une valeur d'entrée comprise entre 0,0 et 1,0 (affichée sur l'axe X) en une valeur de sortie comprise entre 0,0 et 1,0 (affichée sur l'axe Y). Ces diagrammes montrent également un aperçu de l'apparence des différents effets d'animation lorsqu'ils utilisent une courbe d'atténuation.
Créez un champ nommé _curve
dans AnimatedStar et transmettez-le en tant que paramètre aux widgets AnimatedScale
et TweenAnimationBuilder
.
lib/scoreboard.dart
class AnimatedStar extends StatelessWidget {
final bool isActive;
final Duration _duration = const Duration(milliseconds: 1000);
final Color _deactivatedColor = Colors.grey.shade400;
final Color _activatedColor = Colors.yellow.shade700;
final Curve _curve = Curves.elasticOut; // NEW
AnimatedStar({super.key, required this.isActive});
@override
Widget build(BuildContext context) {
return AnimatedScale(
scale: isActive ? 1.0 : 0.5,
curve: _curve, // NEW
duration: _duration,
child: TweenAnimationBuilder(
curve: _curve, // NEW
duration: _duration,
tween: ColorTween(
begin: _deactivatedColor,
end: isActive ? _activatedColor : _deactivatedColor,
),
builder: (context, value, child) {
return Icon(
Icons.star,
size: 50,
color: value,
);
},
),
);
}
}
Dans cet exemple, la courbe elasticOut
fournit un effet de rétroaction exagéré qui commence par un mouvement de rétroaction et s'équilibre vers la fin.
Effectuez un hot reload de l'application pour voir cette courbe appliquée à AnimatedSize
et TweenAnimationBuilder
.
Activer les animations lentes à l'aide des outils pour les développeurs
Pour déboguer un effet d'animation, les outils de développement Flutter permettent de ralentir toutes les animations de votre application afin que vous puissiez les voir plus clairement.
Pour ouvrir DevTools, assurez-vous que l'application s'exécute en mode débogage, puis ouvrez l'inspecteur de widgets en le sélectionnant dans la barre d'outils de débogage de VSCode ou en sélectionnant le bouton Open Flutter DevTools (Ouvrir DevTools Flutter) dans la fenêtre d'outil de débogage d'IntelliJ / Android Studio.
Une fois l'inspecteur de widget ouvert, cliquez sur le bouton Animations lentes dans la barre d'outils.
5. Utiliser des effets d'animation explicites
Comme les animations implicites, les animations explicites sont des effets d'animation prédéfinis, mais au lieu d'utiliser une valeur cible, elles utilisent un objet Animation
comme paramètre. Ils sont donc utiles dans les situations où l'animation est déjà définie par une transition de navigation, AnimatedSwitcher
ou AnimationController
, par exemple.
Utiliser un effet d'animation explicite
Pour commencer à utiliser un effet d'animation explicite, encapsulez le widget Card
avec un AnimatedSwitcher
.
lib/question_screen.dart
class QuestionCard extends StatelessWidget {
final String? question;
const QuestionCard({
required this.question,
super.key,
});
@override
Widget build(BuildContext context) {
return AnimatedSwitcher( // NEW
duration: const Duration(milliseconds: 300), // NEW
child: Card(
key: ValueKey(question),
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
question ?? '',
style: Theme.of(context).textTheme.displaySmall,
),
),
), // NEW
);
}
}
AnimatedSwitcher
utilise un effet de fondu par défaut, mais vous pouvez le remplacer à l'aide du paramètre transitionBuilder
. Le compilateur de transition fournit le widget enfant qui a été transmis à AnimatedSwitcher
et un objet Animation
. C'est une excellente occasion d'utiliser une animation explicite.
Pour cet atelier de programmation, la première animation explicite que nous utiliserons est SlideTransition
, qui prend un Animation<Offset>
qui définit le décalage de début et de fin entre lequel les widgets entrants et sortants se déplaceront.
Les tweens disposent d'une fonction d'assistance, animate()
, qui convertit n'importe quel Animation
en un autre Animation
avec le tween appliqué. Cela signifie qu'un Tween<Offset>
peut être utilisé pour convertir le Animation<double>
fourni par le AnimatedSwitcher
en Animation<Offset>
, à fournir au widget SlideTransition
.
lib/question_screen.dart
class QuestionCard extends StatelessWidget {
final String? question;
const QuestionCard({
required this.question,
super.key,
});
@override
Widget build(BuildContext context) {
return AnimatedSwitcher(
transitionBuilder: (child, animation) { // Add from here...
final curveAnimation =
CurveTween(curve: Curves.easeInCubic).animate(animation);
final offsetAnimation =
Tween<Offset>(begin: Offset(-0.1, 0.0), end: Offset.zero)
.animate(curveAnimation);
return SlideTransition(position: offsetAnimation, child: child);
}, // To here.
duration: const Duration(milliseconds: 300),
child: Card(
key: ValueKey(question),
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
question ?? '',
style: Theme.of(context).textTheme.displaySmall,
),
),
),
);
}
}
Notez que Tween.animate
est utilisé pour appliquer un Curve
au Animation
, puis pour le convertir d'un Tween<double>
compris entre 0,0 et 1,0 en Tween<Offset>
qui passe de -0,1 à 0,0 sur l'axe X.
La classe Animation comporte également une fonction drive()
qui prend n'importe quel Tween
(ou Animatable
) et le convertit en nouveau Animation
. Cela permet de "enchaîner" les tweens, ce qui rend le code obtenu plus concis:
lib/question_screen.dart
transitionBuilder: (child, animation) {
var offsetAnimation = animation
.drive(CurveTween(curve: Curves.easeInCubic))
.drive(Tween<Offset>(begin: Offset(-0.1, 0.0), end: Offset.zero));
return SlideTransition(position: offsetAnimation, child: child);
},
Un autre avantage de l'utilisation d'animations explicites est qu'elles peuvent être facilement composées ensemble. Ajoutez une autre animation explicite, FadeTransition, qui utilise la même animation incurvée en encapsulant le widget SlideTransition.
lib/question_screen.dart
return AnimatedSwitcher(
transitionBuilder: (child, animation) {
final curveAnimation =
CurveTween(curve: Curves.easeInCubic).animate(animation);
final offsetAnimation =
Tween<Offset>(begin: Offset(-0.1, 0.0), end: Offset.zero)
.animate(curveAnimation);
final fadeInAnimation = curveAnimation; // NEW
return FadeTransition( // NEW
opacity: fadeInAnimation, // NEW
child: SlideTransition(position: offsetAnimation, child: child), // NEW
); // NEW
},
Personnaliser layoutBuilder
Vous remarquerez peut-être un petit problème avec AnimationSwitcher. Lorsqu'une fiche de question passe à une nouvelle question, elle s'affiche au centre de l'espace disponible pendant l'animation, mais lorsque l'animation est arrêtée, le widget se fixe en haut de l'écran. Cela entraîne une animation saccadée, car la position finale de la fiche de question ne correspond pas à celle pendant l'exécution de l'animation.
Pour résoudre ce problème, AnimatedSwitcher dispose également d'un paramètre layoutBuilder, qui permet de définir la mise en page. Utilisez cette fonction pour configurer l'outil de création de mise en page afin d'aligner la fiche en haut de l'écran:
lib/question_screen.dart
@override
Widget build(BuildContext context) {
return AnimatedSwitcher(
layoutBuilder: (currentChild, previousChildren) {
return Stack(
alignment: Alignment.topCenter,
children: <Widget>[
...previousChildren,
if (currentChild != null) currentChild,
],
);
},
Ce code est une version modifiée du defaultLayoutBuilder de la classe AnimatedSwitcher, mais utilise Alignment.topCenter
au lieu de Alignment.center
.
Résumé
- Les animations explicites sont des effets d'animation qui utilisent un objet Animation (contrairement aux ImplicitlyAnimatedWidgets, qui utilisent une valeur cible et une durée).
- La classe Animation représente une animation en cours d'exécution, mais ne définit pas d'effet spécifique.
- Utilisez Tween().animate ou Animation.drive() pour appliquer des Tweens et des courbes (à l'aide de CurveTween) à une animation.
- Utilisez le paramètre layoutBuilder d'AnimatedSwitcher pour ajuster la mise en page de ses enfants.
6. Contrôler l'état d'une animation
Jusqu'à présent, chaque animation a été exécutée automatiquement par le framework. Les animations implicites s'exécutent automatiquement, et les effets d'animation explicites nécessitent une animation pour fonctionner correctement. Dans cette section, vous allez apprendre à créer vos propres objets Animation à l'aide d'un AnimationController et à utiliser une TweenSequence pour combiner des Tweens.
Exécuter une animation à l'aide d'un AnimationController
Pour créer une animation à l'aide d'un AnimationController, procédez comme suit:
- Créer un StatefulWidget
- Utilisez le mixin SingleTickerProviderStateMixin dans votre classe State pour fournir un Ticker à votre AnimationController.
- Initialisez l'AnimationController dans la méthode de cycle de vie initState, en fournissant l'objet State actuel au paramètre
vsync
(TickerProvider). - Assurez-vous que votre widget est recompilé chaque fois que l'AnimationController avertit ses écouteurs, soit à l'aide d'AnimatedBuilder, soit en appelant listen() et setState manuellement.
Créez un fichier nommé flip_effect.dart et copiez-collez le code suivant:
lib/flip_effect.dart
import 'dart:math' as math;
import 'package:flutter/widgets.dart';
class CardFlipEffect extends StatefulWidget {
final Widget child;
final Duration duration;
const CardFlipEffect({
super.key,
required this.child,
required this.duration,
});
@override
State<CardFlipEffect> createState() => _CardFlipEffectState();
}
class _CardFlipEffectState extends State<CardFlipEffect>
with SingleTickerProviderStateMixin {
late final AnimationController _animationController;
Widget? _previousChild;
@override
void initState() {
super.initState();
_animationController =
AnimationController(vsync: this, duration: widget.duration);
_animationController.addListener(() {
if (_animationController.value == 1) {
_animationController.reset();
}
});
}
@override
void didUpdateWidget(covariant CardFlipEffect oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.child.key != oldWidget.child.key) {
_handleChildChanged(widget.child, oldWidget.child);
}
}
void _handleChildChanged(Widget newChild, Widget previousChild) {
_previousChild = previousChild;
_animationController.forward();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform(
alignment: Alignment.center,
transform: Matrix4.identity()
..rotateX(_animationController.value * math.pi),
child: _animationController.isAnimating
? _animationController.value < 0.5
? _previousChild
: Transform.flip(flipY: true, child: child)
: child,
);
},
child: widget.child,
);
}
}
Cette classe configure un AnimationController et réexécute l'animation chaque fois que le framework appelle didUpdateWidget pour l'informer que la configuration du widget a changé et qu'il peut y avoir un nouveau widget enfant.
AnimatedBuilder s'assure que l'arborescence de widgets est recréée chaque fois que l'AnimationController informe ses écouteurs. Le widget Transform permet d'appliquer un effet de rotation 3D pour simuler le retournement d'une carte.
Pour utiliser ce widget, encapsulez chaque fiche de réponse dans un widget CardFlipEffect. Assurez-vous de fournir un key
au widget de carte:
lib/question_screen.dart
@override
Widget build(BuildContext context) {
return GridView.count(
shrinkWrap: true,
crossAxisCount: 2,
childAspectRatio: 5 / 2,
children: List.generate(answers.length, (index) {
var color = Theme.of(context).colorScheme.primaryContainer;
if (correctAnswer == index) {
color = Theme.of(context).colorScheme.tertiaryContainer;
}
return CardFlipEffect( // NEW
duration: const Duration(milliseconds: 300), // NEW
child: Card.filled( // NEW
key: ValueKey(answers[index]), // NEW
color: color,
elevation: 2,
margin: EdgeInsets.all(8),
clipBehavior: Clip.hardEdge,
child: InkWell(
onTap: () => onTapped(index),
child: Padding(
padding: EdgeInsets.all(16.0),
child: Center(
child: Text(
answers.length > index ? answers[index] : '',
style: Theme.of(context).textTheme.titleMedium,
overflow: TextOverflow.clip,
),
),
),
),
), // NEW
);
}),
);
}
Effectuez maintenant une actualisation à chaud de l'application pour voir les fiches de réponse se retourner à l'aide du widget CardFlipEffect.
Vous remarquerez peut-être que cette classe ressemble beaucoup à un effet d'animation explicite. En fait, il est souvent judicieux d'étendre directement la classe AnimatedWidget pour implémenter votre propre version. Malheureusement, comme cette classe doit stocker le widget précédent dans son état, elle doit utiliser un StatefulWidget. Pour en savoir plus sur la création de vos propres effets d'animation explicites, consultez la documentation de l'API AnimatedWidget.
Ajouter un délai à l'aide de TweenSequence
Dans cette section, vous allez ajouter un délai au widget CardFlipEffect afin que chaque carte se retourne une à une. Pour commencer, ajoutez un champ appelé delayAmount
.
lib/flip_effect.dart
class CardFlipEffect extends StatefulWidget {
final Widget child;
final Duration duration;
final double delayAmount; // NEW
const CardFlipEffect({
super.key,
required this.child,
required this.duration,
required this.delayAmount, // NEW
});
@override
State<CardFlipEffect> createState() => _CardFlipEffectState();
}
Ajoutez ensuite le delayAmount
à la méthode de compilation AnswerCards
.
lib/question_screen.dart
@override
Widget build(BuildContext context) {
return GridView.count(
shrinkWrap: true,
crossAxisCount: 2,
childAspectRatio: 5 / 2,
children: List.generate(answers.length, (index) {
var color = Theme.of(context).colorScheme.primaryContainer;
if (correctAnswer == index) {
color = Theme.of(context).colorScheme.tertiaryContainer;
}
return CardFlipEffect(
delayAmount: index.toDouble() / 2, // NEW
duration: const Duration(milliseconds: 300),
child: Card.filled(
key: ValueKey(answers[index]),
Ensuite, dans _CardFlipEffectState
, créez une animation qui applique le délai à l'aide d'un TweenSequence
. Notez que cette méthode n'utilise pas d'utilitaires de la bibliothèque dart:async
, comme Future.delayed
. En effet, le délai fait partie de l'animation et n'est pas contrôlé explicitement par le widget lorsqu'il utilise AnimationController. Cela facilite le débogage de l'effet d'animation lorsque vous activez les animations lentes dans DevTools, car il utilise le même TickerProvider.
Pour utiliser un TweenSequence
, créez deux TweenSequenceItem
, l'un contenant un ConstantTween
qui maintient l'animation à 0 pendant une durée relative et un Tween
standard qui va de 0.0
à 1.0
.
lib/flip_effect.dart
class _CardFlipEffectState extends State<CardFlipEffect>
with SingleTickerProviderStateMixin {
late final AnimationController _animationController;
Widget? _previousChild;
late final Animation<double> _animationWithDelay; // NEW
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this, duration: widget.duration * (widget.delayAmount + 1));
_animationController.addListener(() {
if (_animationController.value == 1) {
_animationController.reset();
}
});
_animationWithDelay = TweenSequence<double>([ // NEW
if (widget.delayAmount > 0) // NEW
TweenSequenceItem( // NEW
tween: ConstantTween<double>(0.0), // NEW
weight: widget.delayAmount, // NEW
), // NEW
TweenSequenceItem( // NEW
tween: Tween(begin: 0.0, end: 1.0), // NEW
weight: 1.0, // NEW
), // NEW
]).animate(_animationController); // NEW
}
Enfin, remplacez l'animation de l'AnimationController par la nouvelle animation retardée dans la méthode de compilation.
lib/flip_effect.dart
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationWithDelay, // Modify this line
builder: (context, child) {
return Transform(
alignment: Alignment.center,
transform: Matrix4.identity()
..rotateX(_animationWithDelay.value * math.pi), // And this line
child: _animationController.isAnimating
? _animationWithDelay.value < 0.5 // And this one.
? _previousChild
: Transform.flip(flipY: true, child: child)
: child,
);
},
child: widget.child,
);
}
Effectuez maintenant un hot reload de l'application et regardez les cartes se retourner une par une. Pour vous lancer un défi, essayez de modifier la perspective de l'effet 3D fourni par le widget Transform
.
7. Utiliser des transitions de navigation personnalisées
Jusqu'à présent, nous avons vu comment personnaliser des effets sur un seul écran. Mais vous pouvez également utiliser des animations pour effectuer des transitions entre les écrans. Dans cette section, vous allez apprendre à appliquer des effets d'animation aux transitions d'écran à l'aide d'effets d'animation intégrés et d'effets d'animation prédéfinis sophistiqués fournis par le package officiel animations sur pub.dev.
Animer une transition de navigation
La classe PageRouteBuilder
est un Route
qui vous permet de personnaliser l'animation de transition. Il vous permet de remplacer son rappel transitionBuilder
, qui fournit deux objets Animation, représentant l'animation entrante et sortante exécutée par le navigateur.
Pour personnaliser l'animation de transition, remplacez MaterialPageRoute
par PageRouteBuilder
et pour personnaliser l'animation de transition lorsque l'utilisateur passe de HomeScreen
à QuestionScreen
. Utilisez un FadeTransition (un widget animé explicitement) pour faire apparaître le nouvel écran par-dessus l'écran précédent.
lib/home_screen.dart
ElevatedButton(
onPressed: () {
// Show the question screen to start the game
Navigator.push(
context,
PageRouteBuilder( // NEW
pageBuilder: (context, animation, secondaryAnimation) { // NEW
return QuestionScreen(); // NEW
}, // NEW
transitionsBuilder: // NEW
(context, animation, secondaryAnimation, child) { // NEW
return FadeTransition( // NEW
opacity: animation, // NEW
child: child, // NEW
); // NEW
}, // NEW
), // NEW
);
},
child: Text('New Game'),
),
Le package d'animations fournit des effets d'animation prédéfinis sophistiqués, comme FadeThroughTransition. Importez le package d'animations et remplacez FadeTransition par le widget FadeThroughTransition:
lib/home_screen.dart
import 'package;animations/animations.dart';
ElevatedButton(
onPressed: () {
// Show the question screen to start the game
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) {
return const QuestionScreen();
},
transitionsBuilder:
(context, animation, secondaryAnimation, child) {
return FadeThroughTransition( // NEW
animation: animation, // NEW
secondaryAnimation: secondaryAnimation, // NEW
child: child, // NEW
); // NEW
},
),
);
},
child: Text('New Game'),
),
Personnaliser l'animation de la prévisualisation du Retour
La prévisualisation du Retour est une nouvelle fonctionnalité Android qui permet à l'utilisateur de voir ce qui se cache derrière l'itinéraire ou l'application en cours avant de naviguer. L'animation d'aperçu est déterminée par l'emplacement du doigt de l'utilisateur lorsqu'il le fait glisser vers l'arrière sur l'écran.
Flutter est compatible avec la prévisualisation du Retour du système en activant la fonctionnalité au niveau du système lorsque Flutter n'a aucune route à afficher dans sa pile de navigation, ou en d'autres termes, lorsqu'un Retour quitterait l'application. Cette animation est gérée par le système et non par Flutter lui-même.
Flutter est également compatible avec la prévisualisation du Retour lorsque vous naviguez entre les routes d'une application Flutter. Un PageTransitionsBuilder spécial appelé PredictiveBackPageTransitionsBuilder
écoute les gestes de prévisualisation du Retour du système et gère sa transition de page en fonction de la progression du geste.
La prévisualisation du Retour n'est compatible qu'avec Android U ou version ultérieure, mais Flutter revient automatiquement au comportement d'origine du geste Retour et à ZoomPageTransitionBuilder. Pour en savoir plus, consultez notre article de blog, qui comprend une section sur la configuration de cette fonctionnalité dans votre propre application.
Dans la configuration ThemeData de votre application, configurez PageTransitionsTheme pour utiliser PredictiveBack sur Android et l'effet de transition de fondu du package d'animations sur d'autres plates-formes:
lib/main.dart
import 'package:animations/animations.dart'; // NEW
import 'package:flutter/material.dart';
import 'home_screen.dart';
void main() {
runApp(MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
pageTransitionsTheme: PageTransitionsTheme(
builders: {
TargetPlatform.android: PredictiveBackPageTransitionsBuilder(), // NEW
TargetPlatform.iOS: FadeThroughPageTransitionsBuilder(), // NEW
TargetPlatform.macOS: FadeThroughPageTransitionsBuilder(), // NEW
TargetPlatform.windows: FadeThroughPageTransitionsBuilder(), // NEW
TargetPlatform.linux: FadeThroughPageTransitionsBuilder(), // NEW
},
),
),
home: HomeScreen(),
);
}
}
Vous pouvez maintenant remplacer l'appel Navigator.push() par un MaterialPageRoute.
lib/home_screen.dart
ElevatedButton(
onPressed: () {
// Show the question screen to start the game
Navigator.push(
context,
MaterialPageRoute(builder: (context) { // NEW
return const QuestionScreen(); // NEW
}), // NEW
);
},
child: Text('New Game'),
),
Utiliser FadeThroughTransition pour modifier la question actuelle
Le widget AnimatedSwitcher ne fournit qu'une seule animation dans son rappel du générateur. Pour y remédier, le package animations
fournit un PageTransitionSwitcher.
lib/question_screen.dart
class QuestionCard extends StatelessWidget {
final String? question;
const QuestionCard({
required this.question,
super.key,
});
@override
Widget build(BuildContext context) {
return PageTransitionSwitcher( // NEW
layoutBuilder: (entries) { // NEW
return Stack( // NEW
alignment: Alignment.topCenter, // NEW
children: entries, // NEW
); // NEW
}, // NEW
transitionBuilder: (child, animation, secondaryAnimation) { // NEW
return FadeThroughTransition( // NEW
animation: animation, // NEW
secondaryAnimation: secondaryAnimation, // NEW
child: child, // NEW
); // NEW
}, // NEW
duration: const Duration(milliseconds: 300),
child: Card(
key: ValueKey(question),
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
question ?? '',
style: Theme.of(context).textTheme.displaySmall,
),
),
),
);
}
}
Utiliser OpenContainer
Le widget OpenContainer du package animations
fournit un effet d'animation de transformation de conteneur qui se développe pour créer une connexion visuelle entre deux widgets.
Le widget renvoyé par closedBuilder
s'affiche initialement et se développe pour afficher le widget renvoyé par openBuilder
lorsque l'utilisateur appuie sur le conteneur ou lorsque le rappel openContainer
est appelé.
Pour connecter le rappel openContainer
au modèle de vue, ajoutez un nouveau pass dans le widget QuestionCard et stockez un rappel qui sera utilisé pour afficher l'écran "Game Over" :
lib/question_screen.dart
class QuestionScreen extends StatefulWidget {
const QuestionScreen({super.key});
@override
State<QuestionScreen> createState() => _QuestionScreenState();
}
class _QuestionScreenState extends State<QuestionScreen> {
late final QuizViewModel viewModel =
QuizViewModel(onGameOver: _handleGameOver);
VoidCallback? _showGameOverScreen; // NEW
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: viewModel,
builder: (context, child) {
return Scaffold(
appBar: AppBar(
actions: [
TextButton(
onPressed:
viewModel.hasNextQuestion && viewModel.didAnswerQuestion
? () {
viewModel.getNextQuestion();
}
: null,
child: const Text('Next'),
)
],
),
body: Center(
child: Column(
children: [
QuestionCard( // NEW
onChangeOpenContainer: _handleChangeOpenContainer, // NEW
question: viewModel.currentQuestion?.question, // NEW
viewModel: viewModel, // NEW
), // NEW
Spacer(),
AnswerCards(
onTapped: (index) {
viewModel.checkAnswer(index);
},
answers: viewModel.currentQuestion?.possibleAnswers ?? [],
correctAnswer: viewModel.didAnswerQuestion
? viewModel.currentQuestion?.correctAnswer
: null,
),
StatusBar(viewModel: viewModel),
],
),
),
);
},
);
}
void _handleChangeOpenContainer(VoidCallback openContainer) { // NEW
_showGameOverScreen = openContainer; // NEW
} // NEW
void _handleGameOver() { // NEW
if (_showGameOverScreen != null) { // NEW
_showGameOverScreen!(); // NEW
} // NEW
} // NEW
}
Ajoutez un widget, GameOverScreen:
lib/question_screen.dart
class GameOverScreen extends StatelessWidget {
final QuizViewModel viewModel;
const GameOverScreen({required this.viewModel, super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Scoreboard(
score: viewModel.score,
totalQuestions: viewModel.totalQuestions,
),
Text(
'You Win!',
style: Theme.of(context).textTheme.displayLarge,
),
Text(
'Score: ${viewModel.score} / ${viewModel.totalQuestions}',
style: Theme.of(context).textTheme.displaySmall,
),
ElevatedButton(
child: Text('OK'),
onPressed: () {
Navigator.popUntil(context, (route) => route.isFirst);
},
),
],
),
),
);
}
}
Dans le widget QuestionCard, remplacez la carte par un widget OpenContainer du package d'animations, en ajoutant deux nouveaux champs pour le ViewModel et le rappel du conteneur ouvert:
lib/question_screen.dart
class QuestionCard extends StatelessWidget {
final String? question;
const QuestionCard({
required this.onChangeOpenContainer,
required this.question,
required this.viewModel,
super.key,
});
final ValueChanged<VoidCallback> onChangeOpenContainer;
final QuizViewModel viewModel;
static const _backgroundColor = Color(0xfff2f3fa);
@override
Widget build(BuildContext context) {
return PageTransitionSwitcher(
duration: const Duration(milliseconds: 200),
transitionBuilder: (child, animation, secondaryAnimation) {
return FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
);
},
child: OpenContainer( // NEW
key: ValueKey(question), // NEW
tappable: false, // NEW
closedColor: _backgroundColor, // NEW
closedShape: const RoundedRectangleBorder( // NEW
borderRadius: BorderRadius.all(Radius.circular(12.0)), // NEW
), // NEW
closedElevation: 4, // NEW
closedBuilder: (context, openContainer) { // NEW
onChangeOpenContainer(openContainer); // NEW
return ColoredBox( // NEW
color: _backgroundColor, // NEW
child: Padding( // NEW
padding: const EdgeInsets.all(16.0), // NEW
child: Text(
question ?? '',
style: Theme.of(context).textTheme.displaySmall,
),
),
);
},
openBuilder: (context, closeContainer) { // NEW
return GameOverScreen(viewModel: viewModel); // NEW
}, // NEW
),
);
}
}
8. Félicitations
Félicitations, vous avez ajouté des effets d'animation à une application Flutter et vous avez découvert les composants principaux du système d'animation de Flutter. Plus précisément, vous avez appris:
- Utiliser un ImplicitlyAnimatedWidget
- Utiliser un ExplicitlyAnimatedWidget
- Appliquer des courbes et des splines à une animation
- Utiliser des widgets de transition prédéfinis tels que AnimatedSwitcher ou PageRouteBuilder
- Utiliser des effets d'animation prédéfinis sophistiqués du package
animations
, tels que FadeThroughTransition et OpenContainer - Personnaliser l'animation de transition par défaut, y compris en ajoutant la prise en charge de la prévisualisation du Retour sur Android
Et ensuite ?
Découvrez quelques-uns de ces ateliers de programmation:
- Créer une mise en page d'application animée et responsive avec Material 3
- Créer des transitions esthétiques avec le système de mouvement de Material Design pour Flutter
- Mettre en valeur son application Flutter
Vous pouvez également télécharger l'exemple d'application d'animation, qui présente différentes techniques d'animation.
Complément d'informations
Vous trouverez d'autres ressources sur les animations sur flutter.dev:
- Présentation des animations
- Tutoriel sur les animations (tutoriel)
- Animations implicites (tutoriel)
- Animer les propriétés d'un conteneur (guide)
- Faire apparaître et disparaître un widget (guide)
- Animations principales
- Animer une transition de parcours de page (guide de recettes)
- Animer un widget à l'aide d'une simulation physique (guide)
- Animations décalées
- Widgets d'animation et de mouvement (catalogue de widgets)
Vous pouvez également consulter ces articles sur Medium:
- Présentation détaillée de l'animation
- Animations implicites personnalisées dans Flutter
- Gestion des animations avec Flutter et Flux / Redux
- Choisir le widget d'animation Flutter qui vous convient
- Animations directionnelles avec animations explicites intégrées
- Principes de base de l'animation Flutter avec des animations implicites
- Quand dois-je utiliser AnimatedBuilder ou AnimatedWidget ?