Informacje o tym ćwiczeniu (w Codelabs)
1. Wprowadzenie
Animacje to świetny sposób na zwiększenie wygody użytkowników aplikacji, przekazanie im ważnych informacji oraz uatrakcyjnienie aplikacji i zwiększenie przyjemności z jej korzystania.
Omówienie ramowego systemu animacji Flutter
Flutter wyświetla efekty animacji, ponownie budując część drzewa widżetu w każdej klatce. Udostępnia on gotowe efekty animacji i inne interfejsy API, które ułatwiają tworzenie i kompozycję animacji.
- Animacje domyślne to wstępnie utworzone efekty animacji, które automatycznie odtwarzają całą animację. Gdy wartość docelowa animacji ulegnie zmianie, animacja będzie odtwarzana od bieżącej wartości do wartości docelowej i wyświetlać poszczególne wartości pośrednie, aby animacja widżetu była płynna. Przykłady animacji domyślnych to
AnimatedSize
,AnimatedScale
iAnimatedPositioned
. - Animacje jawne to również wstępnie utworzone efekty animacji, ale wymagają one obiektu
Animation
. Przykłady:SizeTransition
,ScaleTransition
lubPositionedTransition
. - Animation to klasa reprezentująca uruchomioną lub zatrzymaną animację. Składa się ona z wartości, która reprezentuje wartość docelową, do której animacja jest uruchomiona, oraz stanu, który reprezentuje bieżącą wartość wyświetlaną przez animację na ekranie w danym momencie. Jest to podklasa klasy
Listenable
, która powiadamia swoich słuchaczy o zmianie stanu podczas odtwarzania animacji. - AnimationController to sposób na tworzenie animacji i sterowanie jej stanem. Metody takie jak
forward()
,reset()
,stop()
irepeat()
mogą służyć do sterowania animacją bez konieczności definiowania wyświetlanego efektu animacji, takiego jak skala, rozmiar czy położenie. - Tweeny służą do interpolowania wartości między wartością początkową a końcową. Mogą reprezentować dowolny typ, np. podwójną,
Offset
lubColor
. - Krzywe służą do dostosowywania szybkości zmiany parametru w czasie. Podczas odtwarzania animacji często stosuje się krzywą wykładniczą, aby przyspieszyć lub spowolnić tempo zmian na początku lub na końcu animacji. Krzywe przyjmują wartość wejściową z zakresu od 0,0 do 1,0 i zwracają wartość wyjściową z zakresu od 0,0 do 1,0.
Co utworzysz
W tym ćwiczeniu z programowania utworzysz grę z quizem z pytaniami jednokrotnego wyboru, która będzie zawierać różne efekty i techniki animacji.
Zobaczysz, jak:
- Utwórz widżet, który animuje jego rozmiar i kolor
- Tworzenie efektu odwracania karty 3D
- Używaj efektownych gotowych efektów animacji z pakietu animacji.
- Dodanie obsługi gestu przewidywanego przejścia wstecz w najnowszej wersji Androida.
Czego się nauczysz
Z tego ćwiczenia dowiesz się:
- Jak używać efektów animowanych w ramach, aby uzyskać świetnie wyglądające animacje bez konieczności pisania dużej ilości kodu.
- Jak używać wyraźnie animowanych efektów do konfigurowania własnych efektów za pomocą gotowych animowanych widżetów, takich jak
AnimatedSwitcher
lubAnimationController
. - Jak za pomocą
AnimationController
zdefiniować własny widżet wyświetlający efekt 3D. - Jak za pomocą pakietu
animations
wyświetlać efektowne animacje przy minimalnej konfiguracji.
Czego potrzebujesz
- Pakiet Flutter SDK
- IDE, takie jak VSCode, Android Studio lub IntelliJ
2. Konfigurowanie środowiska programistycznego Flutter
Do wykonania tego ćwiczenia potrzebne są 2 programy: Flutter SDK i edytor.
Możesz uruchomić laboratorium programistyczne na dowolnym z tych urządzeń:
- Fizyczne urządzenie Android (zalecane do implementacji przewidywanego powrotu w kroku 7) lub iOS podłączone do komputera i ustawione w trybie programisty.
- Symulator iOS (wymaga zainstalowania narzędzi Xcode).
- Emulator Androida (wymaga skonfigurowania w Android Studio).
- przeglądarka (do debugowania wymagana jest przeglądarka Chrome);
- Komputer stacjonarny z systemem Windows, Linux lub macOS. Musisz tworzyć aplikację na platformie, na której planujesz ją wdrożyć. Jeśli więc chcesz tworzyć aplikacje na komputery z systemem Windows, musisz to robić w systemie Windows, aby mieć dostęp do odpowiedniego łańcucha kompilacji. Istnieją wymagania dotyczące poszczególnych systemów operacyjnych, które omówiono szczegółowo na stronie docs.flutter.dev/desktop.
Sprawdzanie instalacji
Aby sprawdzić, czy pakiet SDK Flutter jest prawidłowo skonfigurowany i czy masz zainstalowaną co najmniej jedną z wymienionych powyżej platform docelowych, użyj narzędzia 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. Uruchamianie aplikacji startowej
Pobieranie aplikacji startowej
Użyj git
, aby skopiować aplikację startową z repozytorium flutter/samples
na GitHubie.
git clone https://github.com/flutter/codelabs.git cd codelabs/animations/step_01/
Możesz też pobrać kod źródłowy jako plik ZIP.
Uruchamianie aplikacji
Aby uruchomić aplikację, użyj polecenia flutter run
i wskaż urządzenie docelowe, np. android
, ios
lub chrome
. Pełna lista obsługiwanych platform znajduje się na stronie Obsługiwane platformy.
flutter run -d android
Aplikację możesz też uruchomić i uruchomić debugowanie w wybranym środowisku IDE. Więcej informacji znajdziesz w oficjalnej dokumentacji Fluttera.
Omówienie kodu
Aplikacja startowa to gra z quizem z pytaniami wielokrotnego wyboru. Składa się z 2 ekranów zgodnie z wzorcem projektowania model-view-view-model (MVVM). Komponent QuestionScreen
(widok) korzysta z klasy QuizViewModel
(widok-model), aby zadawać użytkownikowi pytania wielokrotnego wyboru z klasy QuestionBank
(model).
- home_screen.dart – wyświetla ekran z przyciskiem Nowa gra.
- main.dart – konfiguruje
MaterialApp
, aby używać Material 3 i wyświetlać ekran główny. - model.dart – definiuje podstawowe klasy używane w całej aplikacji.
- question_screen.dart – wyświetla interfejs użytkownika gry typu quiz.
- view_model.dart – przechowuje stan i logikę gry w quizie, wyświetlaną przez
QuestionScreen
Aplikacja nie obsługuje jeszcze żadnych efektów animowanych, z wyjątkiem domyślnego przejścia widoku wyświetlanego przez klasę Navigator
w Flutterze, gdy użytkownik naciśnie przycisk Nowa gra.
4. Używanie efektów animacji domyślnych
Animatory niejawne są świetnym rozwiązaniem w wielu sytuacjach, ponieważ nie wymagają specjalnej konfiguracji. W tej sekcji zaktualizujesz widżet StatusBar
, aby wyświetlał animowaną tablicę wyników. Aby znaleźć typowe efekty animacji niejawnej, przejrzyj dokumentację interfejsu API ImplicitlyAnimatedWidget.
Tworzenie nieanimowanego widżetu tablicy wyników
Utwórz nowy plik lib/scoreboard.dart
z tym kodem:
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,
),
],
),
);
}
}
Następnie dodaj widżet Scoreboard
jako element potomny widżetu StatusBar
, zastępując nim widżety Text
, które wcześniej wyświetlały wynik i łączną liczbę pytań. Twój edytor powinien automatycznie dodać wymagane import "scoreboard.dart"
na początku pliku.
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
),
],
),
),
);
}
}
Ten widżet wyświetla ikonę gwiazdki dla każdego pytania. Gdy odpowiedź na pytanie jest poprawna, kolejna gwiazda zapala się natychmiast bez żadnej animacji. W następnych krokach poinformujesz użytkownika o zmianie jego wyniku, animując jego rozmiar i kolor.
Używanie efektu animacji domyślnej
Utwórz nowy widżet o nazwie AnimatedStar
, który używa widżetu AnimatedScale
do zmiany wartości scale
z 0.5
na 1.0
, gdy gwiazda stanie się aktywna:
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.
Gdy użytkownik udzieli prawidłowej odpowiedzi na pytanie, widget AnimatedStar
zmienia rozmiar za pomocą animowanej animacji. Element Icon
color
nie jest tutaj animowany, tylko element scale
, który jest animowany przez widżet AnimatedScale
.
Użycie funkcji Tween do interpolowania wartości
Zwróć uwagę, że kolor widżetu AnimatedStar
zmienia się natychmiast po zmianie wartości pola isActive
na „prawda”.
Aby uzyskać efekt animowanego koloru, możesz użyć widżetu AnimatedContainer
(który jest inną podklasą klasy ImplicitlyAnimatedWidget
), ponieważ może automatycznie animować wszystkie swoje atrybuty, w tym kolor. Widżet musi wyświetlać ikonę, a nie kontener.
Możesz też wypróbować AnimatedIcon
, który implementuje efekty przejścia między kształtami ikon. Nie ma jednak domyślnej implementacji ikony gwiazdki w klasie AnimatedIcons
.
Zamiast tego użyjemy innej podklasy klasy ImplicitlyAnimatedWidget
o nazwie TweenAnimationBuilder
, która przyjmuje jako parametr wartość Tween
. Przejście jest klasą, która przyjmuje 2 wartości (begin
i end
) i oblicza wartości pośrednie, aby można je było wyświetlić w ramach animacji. W tym przykładzie użyjemy ColorTween
, który spełnia wymagania interfejsu Tween
, niezbędne do utworzenia efektu animacji.
Wybierz widżet Icon
i użyj szybkiej czynności „Zakończ z Builderem” w swoim IDE, a potem zmień nazwę na TweenAnimationBuilder
. Następnie podaj czas trwania i 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.
},
),
);
}
}
Teraz ponownie załaduj aplikację, aby zobaczyć nową animację.
Zwróć uwagę, że wartość end
w naszym ColorTween
zmienia się w zależności od wartości parametru isActive
. Dzieje się tak, ponieważ TweenAnimationBuilder
uruchamia animację ponownie, gdy wartość Tween.end
ulegnie zmianie. W takim przypadku nowa animacja będzie działać od bieżącej wartości animacji do nowej wartości końcowej, co pozwoli Ci zmienić kolor w dowolnym momencie (nawet podczas odtwarzania animacji) i wyświetlić płynny efekt animacji z poprawnymi wartościami pośrednimi.
Stosowanie krzywej
Oba te efekty animacji działają z równą szybkością, ale animacje są często bardziej interesujące wizualnie i bardziej przydatne, gdy przyspieszają lub zwalniają.
Curve
stosuje funkcję wygaszania, która określa tempo zmian parametru w czasie. Flutter zawiera gotowe kolekcje krzywych wypełnienia w klasie Curves
, np. easeIn
lub easeOut
.
Te diagramy (dostępne na stronie dokumentacji Curves
interfejsu API) wskazują, jak działają krzywe. Krzywe przekształcają wartość wejściową z zakresu od 0,0 do 1,0 (wyświetlana na osi X) na wartość wyjściową z zakresu od 0,0 do 1,0 (wyświetlana na osi Y). Na tych schematach znajdziesz też podgląd różnych efektów animacji, które wykorzystują krzywą wygaszania.
Utwórz w animowanym widżecie nowe pole o nazwie _curve
i przekaż je jako parametr do widżetów AnimatedScale
i 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);
},
),
);
}
}
W tym przykładzie krzywa elasticOut
zapewnia wyolbrzymiony efekt sprężystości, który zaczyna się od sprężystego ruchu i zrównoważa się pod koniec.
Aby zobaczyć tę krzywą zastosowaną do AnimatedSize
i TweenAnimationBuilder
, ponownie załaduj aplikację.
Włączanie animacji w wolnym tempie za pomocą Narzędzi deweloperskich
Aby debugować dowolny efekt animacji, możesz spowolnić wszystkie animacje w aplikacji za pomocą narzędzi dewelopera Fluttera.
Aby otworzyć DevTools, upewnij się, że aplikacja działa w trybie debugowania, i otwórz Widget Inspector, klikając go na pasku narzędzi Debugowanie w VSCode lub klikając przycisk Otwórz Flutter DevTools w oknie narzędzia Debugowanie w IntelliJ / Android Studio.
Po otwarciu inspektora widżetu kliknij na pasku narzędzi przycisk Wyłącz animacje.
5. Używanie efektów animacji
Podobnie jak animacje domyślne, animacje jawne to wstępnie utworzone efekty animacji, ale zamiast wartości docelowej ich parametrem jest obiekt Animation
. Jest to przydatne w sytuacjach, gdy animacja jest już zdefiniowana przez przejście nawigacyjne, na przykład AnimatedSwitcher
lub AnimationController
.
Używanie wyraźnego efektu animacji
Aby zacząć korzystać z wyraźnego efektu animacji, owiń widżet Card
widżetem 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
);
}
}
Domyślnie AnimatedSwitcher
używa efektu przejścia, ale możesz go zastąpić za pomocą parametru transitionBuilder
. Kreator przejścia udostępnia widżet podrzędny przekazany do AnimatedSwitcher
oraz obiekt Animation
. To świetna okazja do użycia wyraźnej animacji.
W tym ćwiczeniu pierwszą animację, której użyjemy, będzie SlideTransition
. Wymaga ona parametru Animation<Offset>
, który określa przesunięcie początkowe i końcowe, między którymi będą się przemieszczać widżety przychodzące i wychodzące.
Przejścia mają funkcję pomocniczą animate()
, która zamienia dowolne Animation
na inne Animation
z zastosowaniem przejścia. Oznacza to, że za pomocą Tween
można przekonwertować Animation
z AnimatedSwitcher
na Animation
, aby przekazać go widżetowi 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,
),
),
),
);
}
}
Pamiętaj, że w tym przypadku funkcja Tween.animate
stosuje funkcję Curve
do funkcji Animation
, a następnie przekształca ją z funkcji Tween
o zakresie od 0,0 do 1,0 w funkcję Tween
, która na osi x przechodzi od -0,1 do 0,0.
Klasa Animation ma też funkcję drive()
, która przyjmuje dowolną wartość Tween
(lub Animatable
) i konwertuje ją na nową wartość Animation
. Dzięki temu możesz „łańcuchowo” łączyć animacje, co pozwoli Ci uzyskać bardziej zwięzły kod:
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);
},
Kolejną zaletą stosowania animacji wyraźnych jest to, że można je łączyć. Dodaj kolejną animację, FadeTransition
która używa tej samej wygiętej animacji, owijając widżet 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
},
Dostosowywanie kreatora układów
Możesz zauważyć niewielki problem z AnimationSwitcher
. Gdy QuestionCard
przełączy się na nowe pytanie, umieści je na środku dostępnej przestrzeni podczas animacji, ale gdy animacja zostanie zatrzymana, widget zostanie przypięty u góry ekranu. Powoduje to niepłynną animację, ponieważ końcowa pozycja karty z pytaniem nie jest zgodna z pozycją podczas odtwarzania animacji.
Aby rozwiązać ten problem, tag AnimatedSwitcher
ma też parametr layoutBuilder
, który służy do definiowania układu. Użyj tej funkcji, aby skonfigurować kreatora układu w celu wyrównania karty do górnej części ekranu:
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,
],
);
},
Ten kod jest zmodyfikowaną wersją funkcji defaultLayoutBuilder z klasy AnimatedSwitcher
, ale zamiast Alignment.center
używa Alignment.topCenter
.
Podsumowanie
- Animacje jawne to efekty animacji, które przyjmują obiekt
Animation
(w przeciwieństwie doImplicitlyAnimatedWidgets
, które przyjmują docelvalue
iduration
). - Klasa
Animation
reprezentuje uruchomioną animację, ale nie definiuje konkretnego efektu. - Użyj właściwości
Tween().animate
lubAnimation.drive()
, aby zastosować do animacji właściwościTweens
iCurves
(za pomocą właściwościCurveTween
). - Użyj parametru
AnimatedSwitcher
layoutBuilder
, aby dostosować sposób wyświetlania elementów podrzędnych.
6. Sterowanie stanem animacji
Do tej pory każda animacja była uruchamiana automatycznie przez framework. Animatory niejawne działają automatycznie, a animatory jawne wymagają Animation
. Z tej sekcji dowiesz się, jak tworzyć własne obiekty Animation
za pomocą obiektu AnimationController
, oraz jak używać obiektu TweenSequence
do łączenia obiektów Tween
.
Uruchamianie animacji za pomocą AnimationController
Aby utworzyć animację za pomocą AnimationController, wykonaj te czynności:
- Utwórz
StatefulWidget
- Użyj mixina
SingleTickerProviderStateMixin
w klasieState
, aby udostępnićTicker
klasieAnimationController
. - Zainicjuj
AnimationController
w metodzie cyklu życiainitState
, przekazując bieżący obiektState
do parametruvsync
(TickerProvider
). - Upewnij się, że widżet jest ponownie tworzony, gdy
AnimationController
powiadamia swoich słuchaczy, za pomocą funkcjiAnimatedBuilder
lub wywołując funkcjelisten()
isetState
ręcznie.
Utwórz nowy plik flip_effect.dart
i wklej ten kod:
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,
);
}
}
Ta klasa konfiguruje obiekt AnimationController
i ponowuje animację za każdym razem, gdy framework wywołuje funkcję didUpdateWidget
, aby powiadomić, że konfiguracja widgetu się zmieniła i może być nowy widget podrzędny.
Funkcja AnimatedBuilder
sprawia, że drzewo widżetów jest odtwarzane za każdym razem, gdy AnimationController
powiadamia swoich słuchaczy. Widżet Transform
jest używany do stosowania efektu obrotu 3D, aby symulować odwracanie karty.
Aby użyć tego widżetu, owiń każdą kartę odpowiedzi widżetem CardFlipEffect
. Pamiętaj, aby podać key
do widżetu 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
);
}),
);
}
Teraz ponownie załaduj aplikację, aby zobaczyć, jak karty odpowiedzi odwracają się za pomocą widżetu CardFlipEffect
.
Możesz zauważyć, że ta klasa wygląda bardzo podobnie do jawnego efektu animacji. Często warto bezpośrednio rozszerzyć klasę AnimatedWidget
, aby zaimplementować własną wersję. Ponieważ ta klasa musi przechowywać poprzedni widget w swojej State
, musi używać StatefulWidget
. Więcej informacji o tworzeniu własnych efektów animacji znajdziesz w dokumentacji interfejsu API dotyczącego AnimatedWidget.
Dodawanie opóźnienia za pomocą funkcji TweenSequence
W tej sekcji dodasz opóźnienie do widżetu CardFlipEffect
, aby każda karta odwracała się pojedynczo. Aby rozpocząć, dodaj nowe pole o nazwie 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();
}
Następnie dodaj delayAmount
do metody kompilacji 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]),
Następnie w _CardFlipEffectState
utwórz nową Animation
, która stosuje opóźnienie za pomocą TweenSequence
. Pamiętaj, że nie używasz żadnych narzędzi z biblioteki dart:async
, takich jak Future.delayed
. Dzieje się tak, ponieważ opóźnienie jest częścią animacji i nie jest kontrolowane przez widżet, gdy używa on funkcji AnimationController
. Dzięki temu łatwiej debugować efekt animacji po włączeniu powolnych animacji w Narzędziach deweloperskich, ponieważ używa on tego samego TickerProvider
.
Aby użyć TweenSequence
, utwórz 2 elementy TweenSequenceItem
, z których jeden zawiera ConstantTween
, który utrzymuje animację na poziomie 0 przez czas względny, oraz zwykły element Tween
, który przechodzi z poziomu 0.0
do 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.
}
Na koniec zastąp animację AnimationController
nową opóźnioną animacją w metodzie 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,
);
}
Teraz ponownie załaduj aplikację i obserwuj, jak karty będą się przewracać pojedynczo. Aby spróbować czegoś bardziej wymagającego, spróbuj zmienić perspektywę efektu 3D, który zapewnia widżet Transform
.
7. Używanie niestandardowych przejść nawigacyjnych
Do tej pory omawialiśmy dostosowywanie efektów na jednym ekranie, ale animacje można też stosować do przejścia między ekranami. Z tej sekcji dowiesz się, jak stosować efekty animacji do przejść między ekranami za pomocą wbudowanych efektów animacji i efektów animacji gotowych do użycia, które są dostępne w oficjalnym pakiecie animacji na stronie pub.dev.
Animowanie przejścia nawigacyjnego
Klasa PageRouteBuilder
to Route
, która umożliwia dostosowywanie animacji przejścia. Umożliwia ona zastąpienie wywołania transitionBuilder
, które udostępnia 2 obiekty Animation reprezentujące animację przychodzącą i wychodzącą uruchamianą przez Navigator.
Aby dostosować animację przejścia, zastąp MaterialPageRoute
wartością PageRouteBuilder
. Aby dostosować animację przejścia, gdy użytkownik przechodzi z poziomu HomeScreen
do poziomu QuestionScreen
, zastąp MaterialPageRoute
wartością PageRouteBuilder
. Użyj FadeTransition
(widżetu z wyraźnie animowanymi elementami), aby nowy ekran pojawiał się stopniowo na wierzchu poprzedniego.
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'),
),
Pakiet animacji zawiera gotowe efekty animacji, takie jak FadeThroughTransition
. Zaimportuj pakiet animacji i zastąp widget FadeTransition
widgetem 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'),
),
Dostosowywanie animacji przewidywanego przejścia wstecz
Prognozowane cofnięcie to nowa funkcja Androida, która umożliwia użytkownikowi sprawdzenie, co znajduje się za bieżącą trasą lub aplikacją, zanim przejdzie do następnej czynności. Animacja podglądu jest sterowana przez lokalizację palca użytkownika, gdy przeciąga on palcem po ekranie.
Flutter obsługuje systematyczne cofanie się, włączając tę funkcję na poziomie systemu, gdy nie ma żadnych tras do wyświetlenia na stosie nawigacji, czyli gdy cofnięcie spowoduje zamknięcie aplikacji. Tą animacją zarządza system, a nie Flutter.
Flutter obsługuje też przewidywane cofanie podczas przechodzenia między trasami w aplikacji Flutter. Specjalny element PageTransitionsBuilder
o nazwie PredictiveBackPageTransitionsBuilder
odbiera gesty przewidywanego cofania systemu i steruje przejściem między stronami w miarę ich wykonywania.
Wsteczne cofanie jest obsługiwane tylko w Androidzie U i nowszych wersjach, ale Flutter płynnie przełączy się na pierwotne zachowanie gestu cofnięcia i ZoomPageTransitionBuilder. Więcej informacji znajdziesz w poście na blogu, w tym w sekcji poświęconej konfigurowaniu tej funkcji w Twojej aplikacji.
W konfiguracji ThemeData swojej aplikacji skonfiguruj PageTransitionsTheme
, aby używać PredictiveBack
na Androidzie, oraz efekt przejścia przezroczystego z pakietu animacji na innych platformach:
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(),
);
}
}
Teraz możesz zmienić Navigator.push()
na 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'),
),
Zmiana bieżącego pytania za pomocą przejścia znikającego
Widget AnimatedSwitcher
udostępnia tylko 1 wartość Animation
w swoim wywołaniu zwrotnym w kreatorze. Aby rozwiązać ten problem, pakiet animations
zawiera 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,
),
),
),
);
}
}
Używanie OpenContainer
Widżet OpenContainer z pakietu animations
zawiera efekt animacji przekształcenia kontenera, który rozszerza się, tworząc wizualne połączenie między 2 widżetami.
Początkowo wyświetlany jest widżet zwracany przez funkcję closedBuilder
, a po kliknięciu kontenera lub wywołaniu funkcji zwrotnej openContainer
wyświetla się widżet zwracany przez funkcję openBuilder
.
Aby połączyć funkcję openContainer
z modelem widoku, dodaj nowy przekaz viewModel
do widżetu QuestionCard
i przechowuj funkcję openContainer
, która będzie używana do wyświetlania ekranu „Koniec gry”:
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
}
Dodaj nowy widżet 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);
},
),
],
),
),
);
}
}
W widżecie QuestionCard
zastąp widżet Card
widżetem OpenContainer
z pakietu animations
, dodając 2 nowe pola dla funkcji viewModel
i otwartego wywołania zwrotnego kontenera:
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. Gratulacje
Gratulacje! Udało Ci się dodać efekty animacji do aplikacji Flutter i dowiedzieć się więcej o głównych komponentach systemu animacji Flutter. W szczególności dowiesz się:
- Jak używać
ImplicitlyAnimatedWidget
- Jak używać
ExplicitlyAnimatedWidget
- Jak zastosować
Curves
iTweens
do animacji - Jak korzystać z gotowych widżetów przejść, takich jak
AnimatedSwitcher
lubPageRouteBuilder
- Jak używać gotowych efektów animacji z pakietu
animations
, takich jakFadeThroughTransition
iOpenContainer
- Jak dostosować domyślną animację przejścia, w tym dodać obsługę funkcji Wstecz na podstawie przewidywania na Androidzie.
Co dalej?
Zapoznaj się z tymi ćwiczeniami z programowania:
- Tworzenie animowanego elastycznego układu aplikacji za pomocą Material 3
- Tworzenie atrakcyjnych przejść za pomocą Material Motion w Flutterze
- Jak sprawić, aby aplikacja Flutter była ładna, a nie nudna
Możesz też pobrać przykładową aplikację z animacjami, która pokazuje różne techniki animacji.
Więcej informacji
Więcej materiałów na temat animacji znajdziesz na flutter.dev:
- Wprowadzenie do animacji
- Samouczek dotyczący animacji (samouczek)
- Animacje niejawne (samouczek)
- Animowanie właściwości kontenera (książka kucharska)
- Pokazywanie i ukrywanie widżetu (książka kucharska)
- Animacje banerów powitalnych
- Animowanie przejścia między ścieżkami na stronie (książka kucharska)
- Animowanie widżetu za pomocą symulacji fizyki (książka kucharska)
- Animacje opóźnione
- Widżety animacji i ruchu (katalog widżetów)
Możesz też przeczytać te artykuły na Medium:
- Szczegółowa analiza animacji
- Niestandardowe domyślne animacje w Flutterze
- Zarządzanie animacjami za pomocą Fluttera i Fluxa / Reduxa
- Jak wybrać odpowiedni widget animacji Flutter?
- Animacje kierunkowe z wbudowanymi animacjami dosłownymi
- Podstawy animacji Fluttera z użyciem animacji domyślnych
- Kiedy używać AnimatedBuilder lub AnimatedWidget?