Informazioni su questo codelab
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 senza interruzioni. 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
- Aggiunta del supporto del gesto di ritorno predittivo disponibile nell'ultima versione di Android
Obiettivi didattici
In questo codelab imparerai:
- Come utilizzare gli effetti animati in modo implicito per ottenere 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
oAnimationController
. - Come utilizzare
AnimationController
per definire un widget personalizzato che mostri un effetto 3D. - Come utilizzare il
animations
package 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. Configurare l'ambiente di sviluppo Flutter
Per completare questo laboratorio, 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 pulsante Indietro 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 all'indirizzo 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
Scarica l'app iniziale
Utilizza git
per clonare l'app di avvio 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 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 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 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 utilizzi 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 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
richiesta per creare l'effetto di animazione.
Seleziona il widget Icon
e utilizza l'azione rapida "Inserisci un riquadro con il generatore di widget" 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); // And modify this line.
},
),
);
}
}
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 dell'animazione corrente 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 viene fornito con 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 del funzionamento delle 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.
Dopo aver aperto l'ispezione del 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 anziché 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 a utilizzare un effetto di 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 hanno una funzione di supporto, animate()
, che converte qualsiasi Animation
in un altro Animation
con l'animazione applicata. Ciò significa che un Tween
può essere utilizzato per convertire il Animation
fornito dal 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 in questo caso viene utilizzato Tween.animate
per applicare un Curve
a Animation
e poi per convertirlo da un Tween
che va da 0,0 a 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 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 composte insieme. 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 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 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
ha anche 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 diImplicitlyAnimatedWidgets
, che richiedono unvalue
e unduration
di destinazione) - La classe
Animation
rappresenta un'animazione in esecuzione, ma non definisce un effetto specifico. - Usa
Tween().animate
oAnimation.drive()
per applicareTweens
eCurves
(utilizzandoCurveTween
) a un'animazione. - Utilizza il parametro
AnimatedSwitcher
dilayoutBuilder
per modificare il layout 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 e 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, devi seguire questi passaggi:
- Crea un
StatefulWidget
- Utilizza il mixin
SingleTickerProviderStateMixin
nella classeState
per fornire unTicker
aAnimationController
- Inizializza
AnimationController
nel metodo del ciclo di vitainitState
, fornendo l'oggettoState
corrente al parametrovsync
(TickerProvider
). - Assicurati che il widget venga ricostruito ogni volta che
AnimationController
invia una notifica ai suoi ascoltatori utilizzandoAnimatedBuilder
o chiamandolisten()
esetState
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 informarlo 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 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 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é questa classe deve memorizzare il widget precedente nel suo 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 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 un nuovo Animation
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
. In questo modo, è più facile eseguire 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>([ // 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 l'app e guarda le schede che si girano 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 le animazioni in entrata e in uscita eseguite 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 un 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 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( // Add from here...
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
); // To here.
},
),
);
},
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 o, 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 PredictiveBack
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),
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 cambiare 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( // Add from here...
builder: (context) {
return const QuestionScreen();
},
), // To here.
);
},
child: Text('New Game'),
),
Utilizza FadeThroughTransition per modificare la domanda corrente
Il widget AnimatedSwitcher
fornisce un solo Animation
nel suo callback del generatore. 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( // 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 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 Card
con un widget OpenContainer
del pacchetto animations
, aggiungendo due nuovi campi per il callback viewModel
e per l'apertura del contenitore:
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 usare un
ImplicitlyAnimatedWidget
- Come usare un
ExplicitlyAnimatedWidget
- Come applicare
Curves
eTweens
a un'animazione - Come utilizzare i widget di transizione predefiniti, ad esempio
AnimatedSwitcher
oPageRouteBuilder
- Come utilizzare effetti di animazione predefiniti sofisticati del pacchetto
animations
, comeFadeThroughTransition
eOpenContainer
- 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.
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)
- Eseguire l'animazione di 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, dai un'occhiata a 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 di Flutter con animazioni implicite
- Quando devo utilizzare AnimatedBuilder o AnimatedWidget?