1. Introduzione
Le animazioni sono un ottimo modo per migliorare l'esperienza utente della tua app, comunicare informazioni importanti all'utente e rendere l'app più curata e piacevole da usare.
Panoramica del framework di animazione di Flutter
Flutter mostra gli effetti di animazione ricostruendo una parte dell'albero dei widget su ogni frame. Fornisce effetti di animazione predefiniti e altre API per semplificare la creazione e la composizione di animazioni.
- Le animazioni implicite sono effetti di animazione predefiniti che vengono eseguiti automaticamente. Quando il valore target dell'animazione cambia, l'animazione viene eseguita dal valore corrente al valore target e vengono visualizzati tutti i valori intermedi in modo che il widget si animi in modo fluido. Esempi di animazioni implicite includono
AnimatedSize,AnimatedScaleeAnimatedPositioned. - Le animazioni esplicite sono effetti di animazione predefiniti, ma richiedono un oggetto
Animationper funzionare. Alcuni esempi sonoSizeTransition,ScaleTransitionoPositionedTransition. - Animation è una classe che rappresenta un'animazione in esecuzione o interrotta ed è composta da un valore che rappresenta il valore target a cui l'animazione è in esecuzione e dallo stato, che rappresenta il valore corrente che l'animazione visualizza sullo schermo in un determinato momento. È una sottoclasse di
Listenablee notifica ai suoi listener quando lo stato cambia durante l'esecuzione dell'animazione. - AnimationController è un modo per creare un'animazione e controllarne lo stato. I suoi metodi, come
forward(),reset(),stop()erepeat(), possono essere utilizzati per controllare l'animazione senza la necessità di definire l'effetto di animazione visualizzato, ad esempio la scala, le dimensioni o la posizione. - I tween vengono utilizzati per interpolare i valori tra un valore iniziale e uno finale e possono rappresentare qualsiasi tipo, ad esempio un valore double,
OffsetoColor. - Le curve vengono utilizzate per regolare la velocità di variazione di un parametro nel tempo. Quando viene eseguita un'animazione, è comune applicare una curva di accelerazione per rendere la velocità di variazione più rapida o più lenta all'inizio o alla fine dell'animazione. Le curve accettano un valore di input compreso tra 0,0 e 1,0 e restituiscono un valore di output compreso tra 0,0 e 1,0.
Cosa creerai
In questo codelab, creerai un quiz a scelta multipla con varie tecniche ed effetti di animazione.

Vedrai come…
- Creare un widget che anima le dimensioni e il colore
- Creare un effetto di rotazione della carta 3D
- Utilizzare effetti di animazione predefiniti e particolari dal pacchetto di animazioni
- Aggiungi il supporto del gesto Indietro predittivo disponibile nell'ultima versione di Android
Obiettivi didattici
In questo codelab imparerai:
- Come utilizzare gli effetti animati impliciti per ottenere animazioni dall'aspetto fantastico senza richiedere molto codice.
- Come utilizzare gli effetti animati espliciti per configurare i tuoi effetti utilizzando widget animati predefiniti come
AnimatedSwitchero unAnimationController. - Come utilizzare
AnimationControllerper definire il tuo widget che mostra un effetto 3D. - Come utilizzare il
animationspacchetto per visualizzare effetti di animazione particolari con una configurazione minima.
Che cosa ti serve
- SDK Flutter
- Un IDE, ad esempio VSCode o Android Studio / IntelliJ
2. Configura l'ambiente di sviluppo Flutter
Per completare questo lab, hai bisogno di due software: l'SDK Flutter e un editor.
Puoi eseguire il codelab utilizzando uno qualsiasi di questi dispositivi:
- Un dispositivo fisico Android (consigliato per l'implementazione della navigazione predittiva indietro nel passaggio 7) o iOS connesso al computer e impostato sulla modalità sviluppatore.
- Il simulatore iOS (richiede l'installazione degli strumenti Xcode).
- L'emulatore Android (richiede la configurazione in Android Studio).
- Un browser (Chrome è necessario per il debug).
- Un computer desktop Windows, Linux o macOS. Devi sviluppare sulla piattaforma in cui prevedi di eseguire il deployment. Pertanto, se vuoi sviluppare un'app desktop Windows, devi svilupparla su Windows per accedere alla catena di build appropriata. Esistono requisiti specifici del sistema operativo trattati in dettaglio su docs.flutter.dev/desktop.
Verificare l'installazione
Per verificare che l'SDK Flutter sia configurato correttamente e che sia installata almeno una delle piattaforme di destinazione sopra indicate, utilizza lo strumento 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. Esegui l'app di base
Scarica l'app iniziale
Utilizza git per clonare l'app iniziale dal repository flutter/samples su GitHub.
git clone https://github.com/flutter/codelabs.git cd codelabs/animations/step_01/
In alternativa, puoi scaricare il codice sorgente come file ZIP.
Esegui l'app
Per eseguire l'app, utilizza il comando flutter run e specifica un dispositivo di destinazione, ad esempio android, ios o chrome. Per l'elenco completo delle piattaforme supportate, consulta la pagina Piattaforme supportate.
flutter run -d android
Puoi anche eseguire il debug dell'app utilizzando l'IDE che preferisci. Per saperne di più, consulta la documentazione ufficiale di Flutter.
Esplorare il codice
L'app iniziale è un quiz a scelta multipla composto da due schermate che seguono il pattern di progettazione Model-View-ViewModel (MVVM). La QuestionScreen (View) utilizza la classe QuizViewModel (View-Model) per porre all'utente domande a scelta multipla dalla classe QuestionBank (Model).
- home_screen.dart: mostra una schermata con un pulsante Nuova partita
- main.dart: configura
MaterialAppper utilizzare Material 3 e mostrare la schermata Home - model.dart: definisce le classi principali utilizzate in tutta l'app
- question_screen.dart: mostra l'interfaccia utente per il gioco a quiz
- view_model.dart: memorizza lo stato e la logica del gioco a quiz, visualizzati da
QuestionScreen

L'app non supporta ancora effetti animati, ad eccezione della transizione di visualizzazione predefinita visualizzata dalla classe Navigator di Flutter quando l'utente preme il pulsante Nuova partita.
4. Utilizzare effetti di animazione impliciti
Le animazioni implicite sono un'ottima scelta in molte situazioni, poiché non richiedono alcuna configurazione speciale. In questa sezione aggiornerai il widget StatusBar in modo che visualizzi un tabellone animato. Per trovare gli effetti di animazione impliciti più comuni, consulta la documentazione dell'API ImplicitlyAnimatedWidget.

Crea il widget del tabellone non animato
Crea un nuovo file, lib/scoreboard.dart, con il seguente codice:
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,
),
],
),
);
}
}
Quindi, aggiungi il widget Scoreboard ai figli del widget StatusBar, sostituendo i widget Text che in precedenza mostravano il punteggio e il numero totale di domande. L'editor dovrebbe aggiungere automaticamente il import "scoreboard.dart" richiesto nella parte superiore del file.
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
),
],
),
),
);
}
}
Questo widget mostra un'icona a forma di stella per ogni domanda. Quando una domanda riceve una risposta corretta, un'altra stella si accende immediatamente senza alcuna animazione. Nei passaggi successivi, aiuterai a informare l'utente che il suo punteggio è cambiato animando le dimensioni e il colore.
Utilizzare un effetto di animazione implicito
Crea un nuovo widget chiamato AnimatedStar che utilizza un widget AnimatedScale per modificare l'importo scale da 0.5 a 1.0 quando la stella diventa attiva:
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(isActive: score > i), // Edit this line.
],
),
);
}
}
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.
Ora, quando l'utente risponde correttamente a una domanda, il widget AnimatedStar aggiorna le sue dimensioni utilizzando un'animazione implicita. Il Icon's color non è animato qui, solo il scale, che viene eseguito dal widget AnimatedScale.

Utilizzare un tween per interpolare tra due valori
Nota che il colore del widget AnimatedStar cambia immediatamente dopo che il campo isActive diventa true.
Per ottenere un effetto di colore animato, puoi provare a utilizzare un widget AnimatedContainer (che è un'altra sottoclasse di ImplicitlyAnimatedWidget), perché può animare automaticamente tutti i suoi attributi, incluso il colore. Purtroppo, il nostro widget deve mostrare un'icona, non un contenitore.
Puoi anche provare AnimatedIcon, che implementa effetti di transizione tra le forme delle icone. Tuttavia, non esiste un'implementazione predefinita di un'icona a forma di stella nella classe AnimatedIcons.
Utilizzeremo invece un'altra sottoclasse di ImplicitlyAnimatedWidget chiamata TweenAnimationBuilder, che accetta un Tween come parametro. Un tween è una classe che accetta due valori (begin e end) e calcola i valori intermedi, in modo che un'animazione possa visualizzarli. In questo esempio, utilizzeremo un ColorTween, che soddisfa l'interfaccia Tween richiesta per creare il nostro effetto di animazione.
Seleziona il widget Icon e utilizza l'azione rapida "Wrap with Builder" (Esegui wrapping con Builder) nel tuo IDE, quindi cambia il nome in TweenAnimationBuilder. Quindi fornisci la durata e 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); // And modify this line.
},
),
);
}
}
Ora, ricarica a caldo l'app per visualizzare la nuova animazione.

Tieni presente che il valore end di ColorTween cambia in base al valore del parametro isActive. Questo perché TweenAnimationBuilder esegue nuovamente l'animazione ogni volta che il valore di Tween.end cambia. In questo caso, la nuova animazione viene eseguita dal valore corrente dell'animazione al nuovo valore finale, il che ti consente di cambiare il colore in qualsiasi momento (anche durante l'esecuzione dell'animazione) e di visualizzare un effetto di animazione uniforme con i valori intermedi corretti.
Applicare una curva
Entrambi questi effetti di animazione vengono eseguiti a una velocità costante, ma le animazioni sono spesso più interessanti e informative dal punto di vista visivo quando accelerano o rallentano.
Una Curve applica una funzione di interpolazione, che definisce il tasso di variazione di un parametro nel tempo. Flutter viene fornito con una raccolta di curve di decelerazione predefinite nella classe Curves, come easeIn o easeOut.


Questi diagrammi (disponibili nella pagina della documentazione dell'API Curves) forniscono un'idea di come funzionano le curve. Le curve convertono un valore di input compreso tra 0,0 e 1,0 (visualizzato sull'asse x) in un valore di output compreso tra 0,0 e 1,0 (visualizzato sull'asse y). Questi diagrammi mostrano anche un'anteprima dell'aspetto dei vari effetti di animazione quando utilizzano una curva di decelerazione.
Crea un nuovo campo in AnimatedStar chiamato _curve e passalo come parametro ai widget AnimatedScale e 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);
},
),
);
}
}
In questo esempio, la curva elasticOut fornisce un effetto molla esagerato che inizia con un movimento a molla e si bilancia verso la fine.

Ricarica a caldo l'app per vedere questa curva applicata a AnimatedSize e TweenAnimationBuilder.

Utilizzare DevTools per attivare le animazioni lente
Per eseguire il debug di qualsiasi effetto di animazione, Flutter DevTools offre un modo per rallentare tutte le animazioni nella tua app, in modo da poterle vedere più chiaramente.
Per aprire DevTools, assicurati che l'app sia in esecuzione in modalità di debug e apri Widget Inspector selezionandolo nella barra degli strumenti di debug in VSCode o selezionando il pulsante Apri Flutter DevTools nella finestra degli strumenti di debug in IntelliJ / Android Studio.


Una volta aperto lo strumento di ispezione dei widget, fai clic sul pulsante Animazioni lente nella barra degli strumenti.

5. Utilizzare effetti di animazione espliciti
Come le animazioni implicite, le animazioni esplicite sono effetti di animazione predefiniti, ma anziché un valore di destinazione, prendono come parametro un oggetto Animation. Ciò le rende utili in situazioni in cui l'animazione è già definita da una transizione di navigazione, AnimatedSwitcher o AnimationController, ad esempio.
Utilizzare un effetto di animazione esplicito
Per iniziare a utilizzare un effetto di animazione esplicito, racchiudi il widget Card con un widget 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 utilizza un effetto dissolvenza incrociata per impostazione predefinita, ma puoi ignorarlo utilizzando il parametro transitionBuilder. Il generatore di transizioni fornisce il widget secondario passato a AnimatedSwitcher e un oggetto Animation. Questa è un'ottima opportunità per utilizzare un'animazione esplicita.
Per questo codelab, la prima animazione esplicita che utilizzeremo è SlideTransition, che accetta un Animation<Offset> che definisce l'offset iniziale e finale tra cui si sposteranno i widget in entrata e in uscita.
I tween hanno una funzione di supporto, animate(), che converte qualsiasi Animation in un altro Animation con il tween applicato. Ciò significa che un Tween può essere utilizzato per convertire il Animation fornito da AnimatedSwitcher in un Animation, da fornire al 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,
),
),
),
);
}
}
Tieni presente che questa operazione utilizza Tween.animate per applicare un Curve a Animation e poi per convertirlo da un Tween compreso tra 0,0 e 1,0 a un Tween che passa da -0,1 a 0,0 sull'asse x.
In alternativa, la classe Animation ha una funzione drive() che accetta qualsiasi Tween (o Animatable) e lo converte in un nuovo Animation. In questo modo, i tween possono essere "concatenati", rendendo il codice risultante più conciso:
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 altro vantaggio dell'utilizzo di animazioni esplicite è che possono essere composte insieme. Aggiungi un'altra animazione esplicita, FadeTransition, che utilizza la stessa animazione curva eseguendo il wrapping del 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
},
Personalizzare layoutBuilder
Potresti notare un piccolo problema con AnimationSwitcher. Quando un QuestionCard passa a una nuova domanda, la dispone al centro dello spazio disponibile mentre l'animazione è in esecuzione, ma quando l'animazione viene interrotta, il widget si sposta nella parte superiore dello schermo. Ciò causa un'animazione instabile perché la posizione finale della scheda della domanda non corrisponde alla posizione durante l'esecuzione dell'animazione.

Per risolvere il problema, AnimatedSwitcher dispone anche di un parametro layoutBuilder, che può essere utilizzato per definire il layout. Utilizza questa funzione per configurare il generatore di layout in modo da allineare la scheda alla parte superiore dello schermo:
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,
],
);
},
Questo codice è una versione modificata di defaultLayoutBuilder della classe AnimatedSwitcher, ma utilizza Alignment.topCenter anziché Alignment.center.
Riepilogo
- Le animazioni esplicite sono effetti di animazione che richiedono un oggetto
Animation(al contrario delleImplicitlyAnimatedWidgets, che richiedono unvaluee undurationdi destinazione) - La classe
Animationrappresenta un'animazione in esecuzione, ma non definisce un effetto specifico. - Utilizza
Tween().animateoAnimation.drive()per applicareTweenseCurves(utilizzandoCurveTween) a un'animazione. - Utilizza il parametro
layoutBuilderdiAnimatedSwitcherper regolare la disposizione dei relativi elementi secondari.
6. Controllare lo stato di un'animazione
Finora, ogni animazione è stata eseguita automaticamente dal framework. Le animazioni implicite vengono eseguite automaticamente, mentre gli effetti di animazione espliciti richiedono un Animation per funzionare correttamente. In questa sezione imparerai a creare i tuoi oggetti Animation utilizzando un AnimationController e a utilizzare un TweenSequence per combinare i Tween.
Eseguire un'animazione utilizzando un AnimationController
Per creare un'animazione utilizzando un AnimationController, segui questi passaggi:
- Crea un
StatefulWidget - Utilizza il mixin
SingleTickerProviderStateMixinnella classeStateper fornire unTickeralAnimationController - Inizializza
AnimationControllernel metodo del ciclo di vitainitState, fornendo l'oggettoStatecorrente al parametrovsync(TickerProvider). - Assicurati che il widget venga ricompilato ogni volta che
AnimationControllerinvia una notifica ai suoi listener, utilizzandoAnimatedBuildero chiamando manualmentelisten()esetState.
Crea un nuovo file, flip_effect.dart, e copia e incolla il seguente codice:
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,
);
}
}
Questa classe configura un AnimationController e riavvia l'animazione ogni volta che il framework chiama didUpdateWidget per comunicare che la configurazione del widget è cambiata e potrebbe esserci un nuovo widget figlio.
AnimatedBuilder garantisce che l'albero dei widget venga ricostruito ogni volta che AnimationController invia una notifica ai suoi listener e il widget Transform viene utilizzato per applicare un effetto di rotazione 3D per simulare il ribaltamento di una carta.
Per utilizzare questo widget, racchiudi ogni scheda di risposta in un widget CardFlipEffect. Assicurati di fornire un key al widget Card:
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
);
}),
);
}
Ora ricarica a caldo l'app per vedere le schede delle risposte capovolgersi utilizzando il widget CardFlipEffect.

Potresti notare che questa classe assomiglia molto a un effetto di animazione esplicito. Infatti, spesso è una buona idea estendere direttamente la classe AnimatedWidget per implementare la tua versione. Purtroppo, poiché questa classe deve memorizzare il widget precedente nel relativo State, deve utilizzare un StatefulWidget. Per scoprire di più sulla creazione di effetti di animazione espliciti, consulta la documentazione dell'API per AnimatedWidget.
Aggiungere un ritardo utilizzando TweenSequence
In questa sezione, aggiungerai un ritardo al widget CardFlipEffect in modo che ogni scheda si capovolga una alla volta. Per iniziare, aggiungi un nuovo campo chiamato 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();
}
Poi aggiungi delayAmount al metodo di build 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]),
Poi, in _CardFlipEffectState, crea un nuovo Animation che applichi il ritardo utilizzando un TweenSequence. Tieni presente che questo non utilizza alcuna utilità della libreria dart:async, come Future.delayed. Questo perché il ritardo fa parte dell'animazione e non è qualcosa che il widget controlla esplicitamente quando utilizza AnimationController. In questo modo, l'effetto di animazione è più facile da eseguire il debug quando si attivano le animazioni lente in DevTools, poiché utilizza lo stesso TickerProvider.
Per utilizzare un TweenSequence, crea due TweenSequenceItem, uno contenente un ConstantTween che mantiene l'animazione a 0 per una durata relativa e un Tween normale che va da 0.0 a 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>([ // Add from here...
if (widget.delayAmount > 0)
TweenSequenceItem(
tween: ConstantTween<double>(0.0),
weight: widget.delayAmount,
),
TweenSequenceItem(tween: Tween(begin: 0.0, end: 1.0), weight: 1.0),
]).animate(_animationController); // To here.
}
Infine, sostituisci l'animazione di AnimationController con la nuova animazione ritardata nel metodo build.
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,
);
}
Ora ricarica a caldo l'app e guarda le carte che si girano una alla volta. Per una sfida, prova a cambiare la prospettiva dell'effetto 3D fornito dal widget Transform.

7. Utilizzare transizioni di navigazione personalizzate
Finora abbiamo visto come personalizzare gli effetti su una singola schermata, ma un altro modo per utilizzare le animazioni è quello di usarle per la transizione tra le schermate. In questa sezione imparerai ad applicare effetti di animazione alle transizioni tra schermate utilizzando effetti di animazione integrati ed effetti di animazione predefiniti avanzati forniti dal pacchetto animations ufficiale su pub.dev.
Animare una transizione di navigazione
La classe PageRouteBuilder è un Route che ti consente di personalizzare l'animazione di transizione. Ti consente di eseguire l'override del relativo callback transitionBuilder, che fornisce due oggetti Animation, che rappresentano l'animazione in entrata e in uscita eseguita dal navigatore.
Per personalizzare l'animazione di transizione, sostituisci MaterialPageRoute con PageRouteBuilder e per personalizzare l'animazione di transizione quando l'utente passa da HomeScreen a QuestionScreen. Utilizza una FadeTransition (un widget animato in modo esplicito) per far apparire la nuova schermata sopra quella precedente.
lib/home_screen.dart
ElevatedButton(
onPressed: () {
// Show the question screen to start the game
Navigator.push(
context,
PageRouteBuilder( // Add from here...
pageBuilder: (context, animation, secondaryAnimation) {
return const QuestionScreen();
},
transitionsBuilder:
(context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: child,
);
},
), // To here.
);
},
child: Text('New Game'),
),
Il pacchetto di animazioni fornisce effetti di animazione predefiniti, come FadeThroughTransition. Importa il pacchetto di animazioni e sostituisci FadeTransition con il 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( // Add from here...
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
); // To here.
},
),
);
},
child: Text('New Game'),
),
Personalizzare l'animazione del gesto Indietro predittivo

Indietro predittivo è una nuova funzionalità di Android che consente all'utente di dare un'occhiata dietro l'itinerario o l'app corrente per vedere cosa c'è dietro prima di navigare. L'animazione di sbirciata è determinata dalla posizione del dito dell'utente mentre lo trascina indietro sullo schermo.
Flutter supporta la funzionalità di indietro predittivo del sistema attivandola a livello di sistema quando Flutter non ha route da estrarre dallo stack di navigazione o, in altre parole, quando un indietro uscirebbe dall'app. Questa animazione viene gestita dal sistema e non da Flutter stesso.
Flutter supporta anche il gesto Indietro predittivo durante la navigazione tra le route all'interno di un'app Flutter. Un PageTransitionsBuilder speciale chiamato PredictiveBackPageTransitionsBuilder rileva i gesti Indietro predittivo del sistema e gestisce la transizione di pagina in base all'avanzamento del gesto.
La navigazione predittiva è supportata solo in Android U e versioni successive, ma Flutter tornerà al comportamento originale del gesto Indietro e a ZoomPageTransitionBuilder. Consulta il nostro post del blog per saperne di più, inclusa una sezione su come configurarlo nella tua app.
Nella configurazione ThemeData per la tua app, configura PageTransitionsTheme in modo che utilizzi PredictiveBack su Android e l'effetto di transizione dissolvenza incrociata dal pacchetto di animazioni su altre piattaforme:
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),
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(),
);
}
}
Ora puoi modificare il richiamo Navigator.push() in MaterialPageRoute.
lib/home_screen.dart
ElevatedButton(
onPressed: () {
// Show the question screen to start the game
Navigator.push(
context,
MaterialPageRoute( // Add from here...
builder: (context) {
return const QuestionScreen();
},
), // To here.
);
},
child: Text('New Game'),
),
Utilizza FadeThroughTransition per cambiare la domanda corrente
Il widget AnimatedSwitcher fornisce un solo Animation nel callback del builder. Per risolvere questo problema, il pacchetto animations fornisce 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( // Add from here...
layoutBuilder: (entries) {
return Stack(alignment: Alignment.topCenter, children: entries);
},
transitionBuilder: (child, animation, secondaryAnimation) {
return FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
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,
),
),
),
);
}
}
Utilizzare OpenContainer

Il widget OpenContainer del pacchetto animations fornisce un effetto di animazione di trasformazione del contenitore che si espande per creare una connessione visiva tra due widget.
Il widget restituito da closedBuilder viene visualizzato inizialmente e si espande al widget restituito da openBuilder quando viene toccato il contenitore o quando viene chiamato il callback openContainer.
Per connettere il callback openContainer al view model, aggiungi un nuovo passaggio di viewModel al widget QuestionCard e memorizza un callback che verrà utilizzato per mostrare la schermata "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
}
Aggiungi un nuovo 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);
},
),
],
),
),
);
}
}
Nel widget QuestionCard, sostituisci Card con un widget OpenContainer del pacchetto animations, aggiungendo due nuovi campi per il callback viewModel e del contenitore aperto:
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. Complimenti
Congratulazioni, hai aggiunto correttamente effetti di animazione a un'app Flutter e hai imparato a conoscere i componenti principali del sistema di animazione di Flutter. Nello specifico, hai imparato:
- Come usare un
ImplicitlyAnimatedWidget - Come usare un
ExplicitlyAnimatedWidget - Come applicare
CurveseTweensa un'animazione - Come utilizzare i widget di transizione predefiniti come
AnimatedSwitcheroPageRouteBuilder - Come utilizzare gli effetti di animazione predefiniti avanzati del pacchetto
animations, ad esempioFadeThroughTransitioneOpenContainer - Come personalizzare l'animazione di transizione predefinita, inclusa l'aggiunta del supporto per Indietro predittivo su Android.

Passaggi successivi
Dai un'occhiata ad alcuni di questi codelab:
- Creare un layout per app adattabile e animato con Material 3
- Creazione di transizioni accattivanti con Material Motion per Flutter
- Trasforma la tua app Flutter da noiosa a bellissima
In alternativa, scarica l'app di esempio per le animazioni, che mostra varie tecniche di animazione.
Per approfondire
Puoi trovare altre risorse sulle animazioni su flutter.dev:
- Introduzione alle animazioni
- Tutorial sulle animazioni (tutorial)
- Animazioni implicite (tutorial)
- Animare le proprietà di un contenitore (cookbook)
- Dissolvenza in entrata e in uscita di un widget (cookbook)
- Animazioni hero
- Animare una transizione di percorso di pagina (cookbook)
- Animare un widget utilizzando una simulazione fisica (cookbook)
- Animazioni scaglionate
- Widget di animazione e movimento (catalogo dei widget)
In alternativa, consulta questi articoli su Medium:
- Approfondimento sull'animazione
- Animazioni implicite personalizzate in Flutter
- Gestione delle animazioni con Flutter e Flux / Redux
- Come scegliere il widget di animazione Flutter più adatto alle tue esigenze?
- Animazioni direzionali con animazioni esplicite integrate
- Nozioni di base sull'animazione Flutter con animazioni implicite
- Quando devo utilizzare AnimatedBuilder o AnimatedWidget?