1. Introduzione
Le animazioni sono un ottimo modo per migliorare l'esperienza utente della tua app, comunicare informazioni importanti all'utente e rendere la tua app più raffinata e piacevole da usare.
Panoramica del framework di animazione di Flutter
Flutter mostra gli effetti di animazione ricostruendo una parte dell'albero dei widget in ogni frame. Fornisce effetti di animazione predefiniti e altre API per semplificare la creazione e la composizione delle animazioni.
- Le animazioni implicite sono effetti di animazione predefiniti che eseguono automaticamente l'intera animazione. Quando il valore target dell'animazione cambia, l'animazione viene eseguita dal valore corrente a quello target e vengono visualizzati tutti i valori intermedi in modo che il widget venga animato in modo fluido. Alcuni esempi di animazioni implicite sono
AnimatedSize
,AnimatedScale
eAnimatedPositioned
. - Anche le animazioni esplicite sono effetti di animazione predefiniti, ma richiedono un oggetto
Animation
per funzionare. Alcuni esempi sonoSizeTransition
,ScaleTransition
oPositionedTransition
. - Animation è una classe che rappresenta un'animazione in esecuzione o interrotta ed è composta da un value che rappresenta il valore target a cui è in esecuzione l'animazione e dallo status, che rappresenta il valore corrente visualizzato dall'animazione sullo schermo in un determinato momento. È una sottoclasse di
Listenable
e notifica i suoi ascoltatori 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 dover definire l'effetto di animazione visualizzato, ad esempio la scala, le dimensioni o la posizione. - Le interpolazione lineari vengono utilizzate per interpolare i valori tra un valore iniziale e uno finale e possono rappresentare qualsiasi tipo, ad esempio un valore doppio,
Offset
oColor
. - 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 transizione per aumentare o diminuire la velocità di variazione 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 vari effetti e tecniche di animazione.
Scoprirai come...
- Creare un widget che anima le dimensioni e il colore
- Creare un effetto di ribaltamento di una carta 3D
- Utilizzare effetti di animazione predefiniti sofisticati dal pacchetto di animazioni
- Aggiungere il supporto del gesto Indietro predittivo disponibile nell'ultima versione di Android
Cosa imparerai a fare
In questo codelab imparerai:
- Come utilizzare gli effetti animati in modo implicito per creare animazioni di grande impatto senza richiedere molto codice.
- Come utilizzare gli effetti animati espliciti per configurare i tuoi effetti utilizzando widget animati predefiniti come
AnimatedSwitcher
o unAnimationController
. - Come utilizzare
AnimationController
per definire un widget personalizzato che mostri un effetto 3D. - Come utilizzare il pacchetto
animations
per visualizzare effetti di animazione elaborati con una configurazione minima.
Che cosa ti serve
- L'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 di questi dispositivi:
- Un dispositivo Android (consigliato per l'implementazione del riavvolgimento predittivo nel passaggio 7) o iOS fisico collegato al computer e impostato sulla modalità Sviluppatore.
- Il simulatore iOS (è richiesta l'installazione degli strumenti Xcode).
- L'emulatore Android (richiede la configurazione in Android Studio).
- Un browser (è necessario Chrome 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 per Windows, devi eseguire lo sviluppo su Windows per accedere alla catena di build appropriata. Esistono requisiti specifici per il sistema operativo che sono descritti 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 avvio
Scaricare 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.
Eseguire 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 ed eseguire il debug dell'app utilizzando l'IDE che preferisci. Per ulteriori informazioni, consulta la documentazione ufficiale di Flutter.
Esplora il codice
L'app iniziale è un quiz a scelta multipla composto da due schermate che seguono il pattern di progettazione model-view-view-model o MVVM. QuestionScreen
(Visualizzazione) utilizza la classe QuizViewModel
(Modello di visualizzazione) per porre all'utente domande a scelta multipla della classe QuestionBank
(Modello).
- home_screen.dart: mostra una schermata con un pulsante Nuova partita
- main.dart: configura
MaterialApp
per utilizzare Material 3 e mostrare la schermata Home - model.dart: definisce le classi di base utilizzate nell'app
- question_screen.dart: mostra l'interfaccia utente del gioco a quiz
- view_model.dart: memorizza lo stato e la logica del quiz, visualizzato 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 mostri un tabellone animato. Per trovare gli effetti di animazione implicita più comuni, consulta la documentazione dell'API ImplicitlyAnimatedWidget.
Creare 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,
)
],
),
);
}
}
Aggiungi il widget Scoreboard
agli elementi secondari 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 viene data una risposta corretta a una domanda, si illumina immediatamente un'altra stella senza alcuna animazione. Nei passaggi successivi, aiuterai l'utente a capire che il suo punteggio è cambiato animandone le dimensioni e il colore.
Utilizzare un effetto di animazione implicito
Crea un nuovo widget denominato 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( // 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.
Ora, quando l'utente risponde correttamente a una domanda, il widget AnimatedStar
aggiorna le sue dimensioni utilizzando un'animazione implicita. Il color
di Icon
non è animato qui, ma solo il scale
, che viene eseguito dal widget AnimatedScale
.
Utilizzare un Tween per eseguire l'interpolazione tra due valori
Tieni presente 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
(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 prende 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<Color>
richiesta per creare l'effetto di animazione.
Seleziona il widget Icon
e utilizza l'azione rapida "Inserisci un riquadro con il generatore di componenti" nell'IDE, poi cambia il nome in TweenAnimationBuilder
. Poi specifica 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, // Modify from here...
);
}, // To here.
),
);
}
}
Ora ricarica l'app per vedere la nuova animazione.
Tieni presente che il valore end
di ColorTween
cambia in base al valore del parametro isActive
. Questo accade perché TweenAnimationBuilder
esegue di nuovo l'animazione ogni volta che il valore 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 fluido 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 vengono accelerate o rallentate.
Un Curve
applica una funzione di attenuazione, che definisce la velocità di variazione di un parametro nel tempo. Flutter è dotato di una raccolta di curve di transizione predefinite nella classe Curves
, ad esempio 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 viene utilizzata una curva di easing.
Crea un nuovo campo in AnimatedStar denominato _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 di molla esagerato che inizia con un movimento a molla e si bilancia verso la fine.
Esegui il ricaricamento dell'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 fornisce un modo per rallentare tutte le animazioni nell'app, in modo da poterle vedere più chiaramente.
Per aprire DevTools, assicurati che l'app sia in esecuzione in modalità di debug e apri l'ispezione dei widget selezionandola nella barra degli strumenti di debug in VSCode o selezionando il pulsante Apri Flutter DevTools nella finestra dello strumento di debug in IntelliJ / Android Studio.
Una volta aperto l'ispezionatore dei widget, fai clic sul pulsante Rallenta animazioni nella barra degli strumenti.
5. Utilizzare effetti di animazione espliciti
Come le animazioni implicite, le animazioni esplicite sono effetti di animazione predefiniti, ma invece di accettare un valore target, accettano un oggetto Animation
come parametro. Questo li rende utili in situazioni in cui l'animazione è già definita da una transizione di navigazione, ad esempio AnimatedSwitcher
o AnimationController
.
Utilizzare un effetto di animazione esplicito
Per iniziare con un effetto animazione esplicito, racchiudi il widget Card
in 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
);
}
}
Per impostazione predefinita, AnimatedSwitcher
utilizza un effetto di transizione graduale, ma puoi sostituirlo 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 muoveranno i widget in entrata e in uscita.
Le animazioni interpolazioni dispongono di una funzione di supporto, animate()
, che converte qualsiasi Animation
in un altro Animation
con l'animazione applicata. Ciò significa che un Tween<Offset>
può essere utilizzato per convertire il Animation<double>
fornito dal AnimatedSwitcher
in un Animation<Offset>
, 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 viene utilizzato Tween.animate
per applicare un Curve
a Animation
e poi per convertirlo da un Tween<double>
che va da 0,0 a 1,0 a un Tween<Offset>
che passa da -0,1 a 0,0 sull'asse x.
In alternativa, la classe Animation ha una funzione drive()
che prende qualsiasi Tween
(o Animatable
) e lo converte in un nuovo Animation
. In questo modo, i tween possono essere "incatenati", 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 facilmente combinate. Aggiungi un'altra animazione esplicita, FadeTransition, che utilizza la stessa animazione curva inserendo il 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 una scheda domanda 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 aggancia alla parte superiore dello schermo. Ciò causa un'animazione discontinua perché la posizione finale della scheda della domanda non corrisponde a quella 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 lo strumento per la creazione del layout in modo che allinei 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 (a differenza di ImplicitlyAnimatedWidgets, che richiedono un valore target e una durata).
- La classe Animation rappresenta un'animazione in esecuzione, ma non definisce un effetto specifico.
- Utilizza Tween().animate o Animation.drive() per applicare Tween e Curve (utilizzando CurveTween) a un'animazione.
- Utilizza il parametro layoutBuilder di AnimatedSwitcher per modificare il modo in cui vengono disposti i relativi elementi secondari.
6. Controllare lo stato di un'animazione
Finora, ogni animazione è stata eseguita automaticamente dal framework. Le animazioni implicite vengono eseguite automaticamente e gli effetti di animazione espliciti richiedono un'animazione per funzionare correttamente. In questa sezione imparerai a creare i tuoi oggetti Animation utilizzando un AnimationController e a utilizzare una TweenSequence per combinare i Tween.
Eseguire un'animazione utilizzando un AnimationController
Per creare un'animazione utilizzando un AnimationController, devi seguire questi passaggi:
- Creare un StatefulWidget
- Utilizza il mixin SingleTickerProviderStateMixin nella classe State per fornire un Ticker all'AnimationController
- Inizializza AnimationController nel metodo di ciclo di vita initState, fornendo l'oggetto State corrente al parametro
vsync
(TickerProvider). - Assicurati che il widget venga ricostruito ogni volta che AnimationController invia una notifica ai suoi ascoltatori, utilizzando AnimatedBuilder o chiamando listen() e setState manualmente.
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 ed esegue di nuovo l'animazione ogni volta che il framework chiama didUpdateWidget per notificare che la configurazione del widget è cambiata e che potrebbe esserci un nuovo widget secondario.
AnimatedBuilder garantisce che l'albero dei widget venga ricostruito ogni volta che AnimationController invia una notifica ai suoi ascoltatori e il widget Transform viene utilizzato per applicare un effetto di rotazione 3D per simulare la rotazione di una scheda.
Per utilizzare questo widget, inserisci ogni scheda di risposta in un widget CardFlipEffect. Assicurati di fornire un key
al widget Scheda:
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 l'app per vedere le schede delle risposte girare utilizzando il widget CardFlipEffect.
Potresti notare che questa classe assomiglia molto a un effetto di animazione esplicito. In effetti, spesso è buona norma estendere direttamente la classe AnimatedWidget per implementare la tua versione. Purtroppo, poiché questo corso deve memorizzare il widget precedente nel proprio stato, 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 venga girata una alla volta. Per iniziare, aggiungi un nuovo campo denominato 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();
}
Aggiungi delayAmount
al metodo di compilazione 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 una nuova animazione che applichi il ritardo utilizzando un TweenSequence
. Tieni presente che non vengono utilizzate 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. Ciò semplifica il debug dell'effetto animazione quando attivi 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>([ // 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
}
Infine, sostituisci l'animazione di AnimationController con la nuova animazione ritardata nel metodo di compilazione.
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 l'app e guarda le schede girare una per una. Per una sfida, prova a modificare la prospettiva dell'effetto 3D fornito dal widget Transform
.
7. Utilizzare transizioni di navigazione personalizzate
Finora abbiamo visto come personalizzare gli effetti in una singola schermata, ma un altro modo per utilizzare le animazioni è per passare da una schermata all'altra. In questa sezione imparerai ad applicare effetti di animazione alle transizioni tra schermate utilizzando gli effetti di animazione integrati e quelli predefiniti più sofisticati forniti dal pacchetto ufficiale animations 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 sostituire il relativo callback transitionBuilder
, che fornisce due oggetti Animation, che rappresentano l'animazione in entrata e in uscita eseguita dal Navigator.
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 transizione di dissolvenza (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( // 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'),
),
Il pacchetto di animazioni fornisce effetti di animazione predefiniti sofisticati, 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( // NEW
animation: animation, // NEW
secondaryAnimation: secondaryAnimation, // NEW
child: child, // NEW
); // NEW
},
),
);
},
child: Text('New Game'),
),
Personalizzare l'animazione Indietro predittivo
Indietro predittivo è una nuova funzionalità di Android che consente all'utente di dare un'occhiata dietro il percorso o l'app corrente per vedere cosa c'è dietro prima di navigare. L'animazione di anteprima è basata sulla posizione del dito dell'utente mentre trascina indietro sullo schermo.
Flutter supporta il pulsante Indietro predittivo di sistema attivando la funzionalità a livello di sistema quando Flutter non ha route da visualizzare nello stack di navigazione, in altre parole quando un pulsante Indietro fa uscire dall'app. Questa animazione è gestita dal sistema e non da Flutter stesso.
Flutter supporta anche il gesto Indietro predittivo quando si passa da una route all'altra all'interno di un'app Flutter. Un PageTransitionsBuilder speciale chiamato PredictiveBackPageTransitionsBuilder
ascolta i gesti Indietro predittivi di sistema e gestisce la transizione di pagina con l'avanzamento del gesto.
Il pulsante Indietro predittivo è supportato solo in Android U e versioni successive, ma Flutter tornerà in modo elegante al comportamento originale del gesto Indietro e a ZoomPageTransitionBuilder. Per saperne di più, consulta il nostro post del blog, che include una sezione su come configurarlo nella tua app.
Nella configurazione di ThemeData per la tua app, configura PageTransitionsTheme in modo da utilizzare la funzionalità Indietro predittiva su Android e l'effetto di transizione di dissolvenza 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),
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(),
);
}
}
Ora puoi modificare la chiamata Navigator.push() in una 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'),
),
Utilizzare FadeThroughTransition per cambiare la domanda corrente
Il widget AnimatedSwitcher fornisce una sola animazione nel suo callback del builder. Per risolvere il 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( // 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,
),
),
),
);
}
}
Utilizzare OpenContainer
Il widget OpenContainer del pacchetto animations
fornisce un effetto di animazione di trasformazione del contenitore che si espande per creare un collegamento visivo tra due widget.
Il widget restituito da closedBuilder
viene visualizzato inizialmente e si espande nel widget restituito da openBuilder
quando si tocca il contenitore o quando viene chiamato il callback openContainer
.
Per collegare il callback openContainer
al modello di visualizzazione, aggiungi un nuovo passaggio del viewModel nel 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 la scheda con un widget OpenContainer del pacchetto di animazioni, aggiungendo due nuovi campi per il callback del viewModel e dell'open container:
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 appreso:
- Come utilizzare un ImplicitlyAnimatedWidget
- Come utilizzare un widget esplicitamente animato
- Come applicare curve e tween a un'animazione
- Come utilizzare i widget di transizione predefiniti come AnimatedSwitcher o PageRouteBuilder
- Come utilizzare effetti di animazione predefiniti sofisticati del pacchetto
animations
, come FadeThroughTransition e OpenContainer - 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 dell'app adattabile animato con Material 3
- Creare transizioni straordinarie con Material Motion per Flutter
- Trasforma la tua app Flutter da noiosa a bella
In alternativa, scarica l'app di esempio di animazioni, che mostra varie tecniche di animazione.
Letture aggiuntive
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)
- Eseguire l'effetto dissolvenza su un widget (cookbook)
- Animazioni hero
- Animare una transizione di percorso pagina (cookbook)
- Animare un widget utilizzando una simulazione fisica (cookbook)
- Animazioni sfalsate
- Widget di animazione e movimento (catalogo di 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 a te?
- Animazioni direzionali con animazioni esplicite integrate
- Nozioni di base sulle animazioni Flutter con animazioni implicite
- Quando devo utilizzare AnimatedBuilder o AnimatedWidget?