1. Введение
Анимация — отличный способ улучшить пользовательский опыт вашего приложения, донести до пользователя важную информацию и сделать приложение более качественным и приятным в использовании.
Обзор фреймворка анимации Flutter
Flutter отображает анимационные эффекты, перестраивая часть дерева виджетов в каждом кадре. Он предоставляет готовые анимационные эффекты и другие API, упрощающие создание и компоновку анимаций.
- Неявные анимации — это предварительно созданные эффекты анимации, которые автоматически воспроизводят всю анимацию целиком. При изменении целевого значения анимации она запускается от текущего значения до целевого значения и отображает каждое промежуточное значение, чтобы виджет плавно анимировался. Примерами неявных анимаций являются
AnimatedSize,AnimatedScaleиAnimatedPositioned. - Явные анимации также представляют собой встроенные эффекты анимации, но для их работы требуется объект
Animation. Примерами таких объектов являютсяSizeTransition,ScaleTransitionилиPositionedTransition. - Класс Animation представляет собой класс, описывающий запущенную или остановленную анимацию, и состоит из значения , представляющего целевое значение, до которого выполняется анимация, и статуса , который представляет текущее значение, отображаемое анимацией на экране в любой момент времени. Он является подклассом класса
Listenableи уведомляет своих слушателей об изменении статуса во время выполнения анимации. - AnimationController — это способ создания анимации и управления её состоянием. Его методы, такие как
forward(),reset(),stop()иrepeat()позволяют управлять анимацией без необходимости определять отображаемый эффект анимации, например, масштаб, размер или положение. - Анимированные окна (tweens) используются для интерполяции значений между начальным и конечным значениями и могут представлять любой тип данных, например, число с плавающей запятой (double),
Offset) илиColor. - Кривые используются для регулирования скорости изменения параметра во времени. При воспроизведении анимации часто применяется кривая сглаживания , чтобы ускорить или замедлить скорость изменения в начале или конце анимации. Кривые принимают входное значение от 0,0 до 1,0 и возвращают выходное значение от 0,0 до 1,0.
Что вы построите
В этом практическом задании вы создадите игру-викторину с вариантами ответов, в которой будут использованы различные анимационные эффекты и приемы.

Вы увидите, как это сделать...
- Создайте виджет, который анимирует изменение своего размера и цвета.
- Создайте эффект переворачивания карты в 3D-формате.
- Используйте встроенные анимационные эффекты из пакета анимаций.
- Добавлена поддержка предиктивного ввода жеста «Назад», доступная в последней версии Android.
Что вы узнаете
В этом практическом занятии вы узнаете:
- Как использовать эффекты, задаваемые неявно, для создания великолепных анимаций без необходимости написания большого количества кода.
- Как использовать явно анимированные эффекты для настройки собственных эффектов с помощью готовых анимированных виджетов, таких как
AnimatedSwitcherилиAnimationController. - Как использовать
AnimationControllerдля определения собственного виджета, отображающего 3D-эффект. - Как использовать пакет
animationsдля отображения эффектных анимационных эффектов с минимальными затратами ресурсов.
Что вам понадобится
- Flutter SDK
- Среда разработки (IDE), например, VSCode или Android Studio / IntelliJ.
2. Настройте среду разработки Flutter.
Для выполнения этой лабораторной работы вам понадобятся два программных компонента: Flutter SDK и редактор .
Вы можете выполнить это практическое задание, используя любое из следующих устройств:
- Физическое устройство Android ( рекомендуется для реализации функции предиктивного ввода на шаге 7 ) или iOS , подключенное к компьютеру и настроенное в режим разработчика.
- Симулятор iOS (требуется установка инструментов Xcode).
- Эмулятор Android (требуется настройка в Android Studio).
- Для работы требуется браузер (для отладки необходим Chrome).
- Для разработки вам потребуется настольный компьютер под управлением Windows , Linux или macOS . Разработка должна производиться на той платформе, на которой вы планируете развертывать приложение. Таким образом, если вы хотите разработать настольное приложение для Windows, вам необходимо использовать Windows для доступа к соответствующей цепочке сборки. Существуют специфические требования к операционной системе, которые подробно описаны в документации по адресу docs.flutter.dev/desktop .
Проверьте установку.
Чтобы убедиться в правильности настройки Flutter SDK и наличии хотя бы одной из указанных выше целевых платформ, воспользуйтесь инструментом 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. Запустите стартовое приложение.
Скачайте стартовое приложение
Используйте git для клонирования стартового приложения из репозитория flutter/samples на GitHub.
git clone https://github.com/flutter/codelabs.git cd codelabs/animations/step_01/
В качестве альтернативы вы можете скачать исходный код в виде ZIP-файла .
Запустите приложение
Для запуска приложения используйте команду flutter run и укажите целевое устройство, например, android , ios или chrome . Полный список поддерживаемых платформ см. на странице «Поддерживаемые платформы» .
flutter run -d android
Вы также можете запускать и отлаживать приложение, используя любую IDE по вашему выбору. Дополнительную информацию см. в официальной документации Flutter.
Изучите код
Стартовое приложение представляет собой викторину с несколькими вариантами ответов, состоящую из двух экранов, построенных по принципу «модель-представление-представление-модель» (MVVM). Экран QuestionScreen (представление) использует класс QuizViewModel (представление-модель) для того, чтобы задавать пользователю вопросы с несколькими вариантами ответов из класса QuestionBank (модель).
- home_screen.dart - Отображает экран с кнопкой " Новая игра"
- main.dart — Настраивает
MaterialAppдля использования Material 3 и отображения главного экрана. - model.dart — определяет основные классы, используемые во всем приложении.
- question_screen.dart — Отображает пользовательский интерфейс викторины.
- view_model.dart — хранит состояние и логику викторины, отображаемой на экране
QuestionScreen

Приложение пока не поддерживает никаких анимированных эффектов, за исключением стандартного перехода между окнами, отображаемого классом Navigator во Flutter при нажатии пользователем кнопки «Новая игра» .
4. Используйте неявные эффекты анимации.
Неявные анимации — отличный выбор во многих ситуациях, поскольку они не требуют специальной настройки. В этом разделе вы обновите виджет StatusBar , чтобы он отображал анимированное табло. Чтобы найти распространенные эффекты неявной анимации, ознакомьтесь с документацией API ImplicitlyAnimatedWidget .

Создайте неанимированный виджет табло.
Создайте новый файл lib/scoreboard.dart со следующим кодом:
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,
),
],
),
);
}
}
Затем добавьте виджет Scoreboard в дочерние элементы виджета StatusBar , заменив виджеты Text , которые ранее отображали счет и общее количество вопросов. Ваш редактор должен автоматически добавить необходимый import "scoreboard.dart" в начало файла.
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
),
],
),
),
);
}
}
Этот виджет отображает значок звездочки для каждого вопроса. Когда на вопрос дан правильный ответ, мгновенно загорается еще одна звездочка без какой-либо анимации. На следующих шагах вы поможете пользователю узнать об изменении его результата, анимируя изменение размера и цвета звездочки.
Используйте неявный эффект анимации.
Создайте новый виджет под названием AnimatedStar , который использует виджет AnimatedScale для изменения величины scale с 0.5 до 1.0 когда звезда становится активной:
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.
Теперь, когда пользователь правильно отвечает на вопрос, виджет AnimatedStar обновляет свой размер с помощью неявной анимации. color Icon здесь не анимируется, а только scale , который выполняется виджетом AnimatedScale .

Используйте анимацию (Tween) для интерполяции между двумя значениями.
Обратите внимание, что цвет виджета AnimatedStar меняется сразу после того, как значение поля isActive становится равным true.
Для создания эффекта анимации цвета можно попробовать использовать виджет AnimatedContainer (который является еще одним подклассом ImplicitlyAnimatedWidget ), поскольку он может автоматически анимировать все свои атрибуты, включая цвет. К сожалению, нашему виджету нужно отображать иконку, а не контейнер.
Вы также можете попробовать AnimatedIcon , который реализует эффекты перехода между формами значков. Но в классе AnimatedIcons нет реализации значка звезды по умолчанию.
Вместо этого мы воспользуемся другим подклассом ImplicitlyAnimatedWidget , называемым TweenAnimationBuilder , который принимает в качестве параметра объект Tween . Tween — это класс, который принимает два значения ( begin и end ) и вычисляет промежуточные значения, чтобы анимация могла их отобразить. В этом примере мы будем использовать ColorTween , который удовлетворяет условию Tween Для создания анимационного эффекта необходим соответствующий интерфейс.
Выберите виджет Icon и воспользуйтесь быстрым действием "Wrap with Builder" в вашей IDE, измените его имя на TweenAnimationBuilder . Затем укажите длительность и 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.
},
),
);
}
}
Теперь перезагрузите приложение, чтобы увидеть новую анимацию.

Обратите внимание, что end значение нашего ColorTween изменяется в зависимости от значения параметра isActive . Это происходит потому, что TweenAnimationBuilder перезапускает анимацию всякий раз, когда изменяется значение Tween.end . В этом случае новая анимация запускается от текущего значения анимации до нового конечного значения, что позволяет изменять цвет в любое время (даже во время выполнения анимации) и отображать плавный эффект анимации с правильными промежуточными значениями.
Примените кривую
Оба этих анимационных эффекта воспроизводятся с постоянной скоростью, но анимация часто выглядит более визуально интересной и информативной, когда она ускоряется или замедляется.
Curve применяет функцию сглаживания , которая определяет скорость изменения параметра во времени. Flutter поставляется с набором предварительно созданных кривых сглаживания в классе Curves , таких как easeIn или easeOut .


Эти диаграммы (доступные на странице документации API Curves ) дают представление о том, как работают кривые. Кривые преобразуют входное значение от 0,0 до 1,0 (отображается по оси x) в выходное значение от 0,0 до 1,0 (отображается по оси y). На этих диаграммах также показан предварительный просмотр того, как выглядят различные эффекты анимации при использовании кривой сглаживания.
Создайте в AnimatedStar новое поле с именем _curve и передайте его в качестве параметра виджетам AnimatedScale и 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);
},
),
);
}
}
В этом примере кривая elasticOut создает преувеличенный пружинный эффект, который начинается с пружинного движения и уравновешивается к концу.

Чтобы увидеть применение этой кривой к AnimatedSize и TweenAnimationBuilder , перезагрузите приложение.

Используйте инструменты разработчика, чтобы включить замедленную анимацию.
Для отладки любых анимационных эффектов Flutter DevTools предоставляет возможность замедлить все анимации в вашем приложении, чтобы вы могли более четко их увидеть.
Чтобы открыть инструменты разработчика, убедитесь, что приложение запущено в режиме отладки, и откройте инспектор виджетов , выбрав его на панели инструментов отладки в VSCode или выбрав кнопку « Открыть инструменты разработчика Flutter» в окне инструментов отладки в IntelliJ / Android Studio.


После открытия инспектора виджетов нажмите кнопку «Замедлить анимацию» на панели инструментов.

5. Используйте явные анимационные эффекты.
Подобно неявным анимациям, явные анимации представляют собой предварительно созданные эффекты анимации, но вместо целевого значения они принимают в качестве параметра объект Animation . Это делает их полезными в ситуациях, когда анимация уже определена, например, переходом навигации, AnimatedSwitcher или AnimationController .
Используйте явный эффект анимации.
Чтобы начать использовать явный анимационный эффект, оберните виджет Card в AnimatedSwitcher .
lib/question_screen.dart
class QuestionCard extends StatelessWidget {
final String? question;
const QuestionCard({required this.question, super.key});
@override
Widget build(BuildContext context) {
return AnimatedSwitcher( // NEW
duration: const Duration(milliseconds: 300), // NEW
child: Card(
key: ValueKey(question),
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
question ?? '',
style: Theme.of(context).textTheme.displaySmall,
),
),
), // NEW
);
}
}
AnimatedSwitcher по умолчанию использует эффект плавного перехода, но вы можете переопределить его с помощью параметра transitionBuilder . Конструктор переходов предоставляет дочерний виджет, переданный в AnimatedSwitcher , и объект Animation . Это отличная возможность использовать явную анимацию.
В этом практическом занятии первой явной анимацией, которую мы будем использовать, является SlideTransition , которая принимает Animation<Offset> , определяющее начальное и конечное смещение, между которыми будут перемещаться входящие и исходящие виджеты.
Функция animate() имеет вспомогательную функцию, которая преобразует любую Animation в другую Animation с применением этой анимации. Это означает, что Tween быть преобразована в другую анимацию. Tween может использоваться для преобразования Animation предоставлено AnimatedSwitcher в Animation Этот параметр будет предоставлен виджету 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,
),
),
),
);
}
}
Обратите внимание, что здесь используется Tween.animate для применения Curve к Animation , а затем для преобразования её из Tween в диапазоне от 0,0 до 1,0, до Tween Этот показатель изменяется от -0,1 до 0,0 по оси X.
В качестве альтернативы, класс Animation имеет функцию drive() , которая принимает любой объект Tween (или Animatable ) и преобразует его в новый Animation . Это позволяет «связывать» объекты Tween, делая результирующий код более лаконичным:
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);
},
Еще одно преимущество использования явных анимаций заключается в том, что их можно комбинировать. Добавьте еще одну явную анимацию, FadeTransition , которая использует ту же самую анимацию с затуханием, обернув ею виджет 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
},
Настройте layoutBuilder
Вы можете заметить небольшую проблему с AnimationSwitcher . Когда QuestionCard переключается на новый вопрос, он размещается в центре доступного пространства во время выполнения анимации, но когда анимация останавливается, виджет резко перемещается в верхнюю часть экрана. Это приводит к некорректной анимации, поскольку конечное положение карточки с вопросом не совпадает с положением во время выполнения анимации.

Для решения этой проблемы AnimatedSwitcher также имеет параметр layoutBuilder , который можно использовать для определения макета. Используйте эту функцию, чтобы настроить конструктор макета таким образом, чтобы карточка была выровнена по верхнему краю экрана:
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,
],
);
},
Этот код представляет собой модифицированную версию класса defaultLayoutBuilder из класса AnimatedSwitcher , но использует Alignment.topCenter вместо Alignment.center .
Краткое содержание
- Явные анимации — это эффекты анимации, которые принимают объект
Animation(в отличие отImplicitlyAnimatedWidgets, которые принимают целевоеvalueиduration). - Класс
Animationпредставляет собой анимацию движения, но не определяет конкретный эффект. - Используйте
Tween().animateилиAnimation.drive()для примененияTweensиCurves(с помощьюCurveTween) к анимации. - Используйте параметр
layoutBuilderобъектаAnimatedSwitcher, чтобы настроить расположение его дочерних элементов.
6. Управление состоянием анимации
До сих пор все анимации запускались автоматически фреймворком. Неявные анимации запускаются автоматически, а для корректной работы явных анимационных эффектов требуется объект Animation . В этом разделе вы узнаете, как создавать собственные объекты Animation с помощью AnimationController и использовать TweenSequence для объединения анимаций ( Tween ).
Запустите анимацию с помощью AnimationController.
Для создания анимации с помощью AnimationController необходимо выполнить следующие шаги:
- Создайте
StatefulWidget - Используйте примесь
SingleTickerProviderStateMixinв вашем классеState, чтобы передатьTickerв вашAnimationController - Инициализируйте
AnimationControllerв методе жизненного циклаinitState, передав текущий объектStateв параметрvsync(TickerProvider). - Убедитесь, что ваш виджет перестраивается всякий раз, когда
AnimationControllerуведомляет своих слушателей, используяAnimatedBuilderили вызываяlisten()иsetStateвручную.
Создайте новый файл flip_effect.dart и скопируйте в него следующий код:
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,
);
}
}
Этот класс создает AnimationController и перезапускает анимацию всякий раз, когда фреймворк вызывает didUpdateWidget , чтобы уведомить его об изменении конфигурации виджета и о возможном появлении нового дочернего виджета.
AnimatedBuilder гарантирует, что дерево виджетов будет перестроено всякий раз, когда AnimationController уведомляет своих слушателей, а виджет Transform используется для применения эффекта 3D-вращения, имитирующего переворачивание карточки.
Для использования этого виджета оберните каждую карточку с ответом в виджет CardFlipEffect . Обязательно укажите key к виджету 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
);
}),
);
}
Теперь перезагрузите приложение, чтобы увидеть, как карточки с ответами переворачиваются с помощью виджета CardFlipEffect .

Вы можете заметить, что этот класс очень похож на явно реализованный эффект анимации. На самом деле, часто бывает хорошей идеей напрямую расширить класс AnimatedWidget , чтобы реализовать свою собственную версию. К сожалению, поскольку этому классу необходимо хранить предыдущий виджет в своем State , ему требуется использовать StatefulWidget . Чтобы узнать больше о создании собственных явных эффектов анимации, см. документацию API для AnimatedWidget .
Добавьте задержку с помощью TweenSequence.
В этом разделе вы добавите задержку к виджету CardFlipEffect , чтобы каждая карта переворачивалась по очереди. Для начала добавьте новое поле с именем 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();
}
Затем добавьте параметр delayAmount в метод build AnswerCards .
lib/question_screen.dart
@override
Widget build(BuildContext context) {
return GridView.count(
shrinkWrap: true,
crossAxisCount: 2,
childAspectRatio: 5 / 2,
children: List.generate(answers.length, (index) {
var color = Theme.of(context).colorScheme.primaryContainer;
if (correctAnswer == index) {
color = Theme.of(context).colorScheme.tertiaryContainer;
}
return CardFlipEffect(
delayAmount: index.toDouble() / 2, // NEW
duration: const Duration(milliseconds: 300),
child: Card.filled(
key: ValueKey(answers[index]),
Затем в классе _CardFlipEffectState создайте новую Animation , которая применяет задержку с помощью TweenSequence . Обратите внимание, что здесь не используются никакие утилиты из библиотеки dart:async , такие как Future.delayed . Это связано с тем, что задержка является частью анимации , а не чем-то, что виджет явно контролирует при использовании AnimationController . Это упрощает отладку эффекта анимации при включении медленных анимаций в DevTools, поскольку используется тот же TickerProvider .
Для использования TweenSequence создайте два объекта TweenSequenceItem : один будет содержать ConstantTween , который поддерживает анимацию на уровне 0 в течение относительного времени, а другой — обычный Tween , изменяющийся от 0.0 до 1.0 .
lib/flip_effect.dart
class _CardFlipEffectState extends State<CardFlipEffect>
with SingleTickerProviderStateMixin {
late final AnimationController _animationController;
Widget? _previousChild;
late final Animation<double> _animationWithDelay; // NEW
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: widget.duration * (widget.delayAmount + 1),
);
_animationController.addListener(() {
if (_animationController.value == 1) {
_animationController.reset();
}
});
_animationWithDelay = TweenSequence<double>([ // 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.
}
Наконец, замените анимацию AnimationController на новую отложенную анимацию в методе 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,
);
}
Теперь перезагрузите приложение и наблюдайте, как карты переворачиваются одна за другой. Для разнообразия попробуйте поэкспериментировать с изменением перспективы 3D-эффекта, создаваемого виджетом Transform .

7. Используйте пользовательские переходы навигации.
До сих пор мы рассматривали, как настраивать эффекты на одном экране, но еще один способ использования анимации — это применение ее для перехода между экранами. В этом разделе вы узнаете, как применять анимационные эффекты к переходам между экранами, используя встроенные анимационные эффекты и готовые анимационные эффекты, предоставляемые официальным пакетом animations на pub.dev .
Анимируйте переход между навигационными окнами.
Класс PageRouteBuilder — это Route , позволяющий настраивать анимацию перехода. Он позволяет переопределить функцию обратного вызова transitionBuilder , которая предоставляет два объекта Animation, представляющих входящую и исходящую анимацию, выполняемую навигатором.
Чтобы настроить анимацию перехода, замените MaterialPageRoute на PageRouteBuilder , а для настройки анимации перехода при переходе пользователя с HomeScreen на QuestionScreen используйте FadeTransition (виджет с явной анимацией), чтобы новый экран плавно появлялся поверх предыдущего.
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'),
),
Пакет анимаций предоставляет множество встроенных анимационных эффектов, таких как FadeThroughTransition . Импортируйте пакет анимаций и замените виджет FadeTransition виджетом 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'),
),
Настройте анимацию возврата в исходное положение.

Функция «Предиктивный возврат» — это новая функция Android, которая позволяет пользователю заглянуть за текущий маршрут или приложение, чтобы увидеть, что находится позади него, прежде чем начать движение. Анимация заглядывания запускается в зависимости от положения пальца пользователя, когда он проводит им назад по экрану.
Flutter поддерживает анимацию «Назад» на системном уровне, включая эту функцию, когда у Flutter нет маршрутов для добавления в стек навигации, или, другими словами, когда нажатие кнопки «Назад» привело бы к выходу из приложения. Эта анимация обрабатывается системой, а не самим Flutter.
Flutter также поддерживает предиктивную навигацию «Назад» при переходе между маршрутами внутри приложения Flutter. Специальный PageTransitionsBuilder , называемый PredictiveBackPageTransitionsBuilder , отслеживает системные жесты предиктивной навигации «Назад» и управляет переходом между страницами в зависимости от прогресса жеста.
Функция предиктивного возврата поддерживается только в Android U и выше, но Flutter корректно вернется к исходному поведению жеста «назад» и использованию ZoomPageTransitionBuilder . Подробнее об этом, включая раздел о настройке в вашем приложении, читайте в нашей статье в блоге .
В конфигурации ThemeData для вашего приложения настройте PageTransitionsTheme так, чтобы на Android использовался PredictiveBack , а на других платформах — эффект плавного перехода из пакета animations:
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(),
);
}
}
Теперь вы можете изменить вызов Navigator.push() обратно на 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'),
),
Используйте FadeThroughTransition, чтобы изменить текущий вопрос.
Виджет AnimatedSwitcher предоставляет только одну Animation в функции обратного вызова построителя. Для решения этой проблемы пакет animations предоставляет виджет 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,
),
),
),
);
}
}
Используйте OpenContainer

Виджет OpenContainer из пакета animations предоставляет эффект анимации трансформации контейнера, который расширяется, создавая визуальную связь между двумя виджетами.
Виджет, возвращаемый функцией closedBuilder отображается изначально и расширяется до виджета, возвращаемого функцией openBuilder , при нажатии на контейнер или при вызове функции обратного вызова openContainer .
Чтобы связать функцию обратного вызова openContainer с моделью представления, добавьте новый параметр, передающий viewModel в виджет QuestionCard и сохраняющий функцию обратного вызова, которая будет использоваться для отображения экрана «Игра окончена»:
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
}
Добавьте новый виджет, 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);
},
),
],
),
),
);
}
}
В виджете QuestionCard замените Card виджетом OpenContainer из пакета animations , добавив два новых поля для viewModel и функции обратного вызова 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. Поздравляем!
Поздравляем, вы успешно добавили анимационные эффекты в приложение Flutter и изучили основные компоненты системы анимации Flutter. В частности, вы узнали:
- Как использовать
ImplicitlyAnimatedWidget - Как использовать
ExplicitlyAnimatedWidget - Как применять
CurvesиTweensк анимации - Как использовать готовые виджеты для перехода между страницами, такие как
AnimatedSwitcherилиPageRouteBuilder - Как использовать встроенные анимационные эффекты из пакета
animations, такие какFadeThroughTransitionиOpenContainer - Как настроить анимацию перехода по умолчанию, включая добавление поддержки функции «Предиктивный возврат» на Android.

Что дальше?
Посмотрите некоторые из этих практических заданий:
- Создание анимированного адаптивного макета приложения с помощью Material 3
- Создание красивых переходов с помощью Material Motion для Flutter
- Превратите своё Flutter-приложение из скучного в прекрасное.
Или скачайте демонстрационное приложение с примерами анимации , в котором представлены различные техники анимации.
Дополнительная информация
Больше ресурсов по анимации вы найдете на flutter.dev:
- Введение в анимацию
- Учебное пособие по анимации (урок)
- Неявная анимация (учебное пособие)
- Анимируйте свойства контейнера (кулинарной книги).
- Плавное появление и исчезновение виджета (поваренная книга)
- Анимация героев
- Анимация перехода между страницами (шаблон)
- Анимируйте виджет с помощью физического моделирования (справочник).
- Пошаговая анимация
- Анимационные и движущиеся виджеты (каталог виджетов)
Или ознакомьтесь с этими статьями на Medium:
- Углубленный анализ анимации
- Пользовательские неявные анимации во Flutter
- Управление анимацией с помощью Flutter и Flux/Redux
- Как выбрать подходящий именно вам виджет анимации Flutter?
- Направленная анимация со встроенными явными анимациями
- Основы анимации во Flutter с использованием неявной анимации.
- В каких случаях следует использовать AnimatedBuilder или AnimatedWidget?