О практической работе
1. Введение
Анимация — отличный способ улучшить пользовательский опыт использования вашего приложения, донести до пользователя важную информацию и сделать ваше приложение более продуманным и приятным в использовании.
Обзор анимационного фреймворка Flutter
Flutter отображает эффекты анимации, перестраивая часть дерева виджетов на каждом кадре. Он предоставляет готовые эффекты анимации и другие API, чтобы упростить создание и компоновку анимаций.
- Неявные анимации — это готовые эффекты анимации, которые автоматически запускают всю анимацию. Когда целевое значение анимации изменяется, он запускает анимацию от текущего значения до целевого значения и отображает каждое значение между ними, чтобы виджет анимировался плавно. Примерами неявных анимаций являются
AnimatedSize
,AnimatedScale
иAnimatedPositioned
. - Явные анимации также являются готовыми эффектами анимации, но для работы им требуется объект
Animation
. Примерами являютсяSizeTransition
,ScaleTransition
илиPositionedTransition
. - Animation — это класс, представляющий запущенную или остановленную анимацию, и состоящий из значения , представляющего целевое значение, к которому движется анимация, и status , представляющего текущее значение, которое анимация отображает на экране в любой момент времени. Это подкласс
Listenable
, который уведомляет своих слушателей об изменении статуса во время работы анимации. - AnimationController — это способ создания анимации и управления ее состоянием. Его методы, такие как
forward()
,reset()
,stop()
иrepeat()
могут использоваться для управления анимацией без необходимости определять отображаемый эффект анимации, такой как масштаб, размер или положение. - Анимации используются для интерполяции значений между начальным и конечным значениями и могут представлять любой тип, например double,
Offset
илиColor
. - Кривые используются для регулировки скорости изменения параметра с течением времени. При запуске анимации обычно применяется кривая замедления , чтобы сделать скорость изменения быстрее или медленнее в начале или конце анимации. Кривые принимают входное значение от 0,0 до 1,0 и возвращают выходное значение от 0,0 до 1,0.
Что вы построите
В этой лабораторной работе вам предстоит создать игру-викторину с множественным выбором ответов, в которой будут использованы различные анимационные эффекты и приемы.
Вы увидите, как...
- Создайте виджет, который анимирует свой размер и цвет
- Создайте эффект переворачивания 3D-карты
- Используйте модные готовые эффекты анимации из пакета анимации
- Добавить поддержку предиктивного жеста «Назад», доступную в последней версии Android
Чему вы научитесь
В этой лабораторной работе вы узнаете:
- Как использовать неявно анимированные эффекты для создания великолепной анимации без написания большого количества кода.
- Как использовать явно анимированные эффекты для настройки собственных эффектов с помощью готовых анимированных виджетов, таких как
AnimatedSwitcher
илиAnimationController
. - Как использовать
AnimationController
для определения собственного виджета, отображающего 3D-эффект. - Как использовать пакет
animations
для отображения интересных анимационных эффектов с минимальной настройкой.
Что вам понадобится
- Пакет SDK Flutter
- 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
(View) использует класс QuizViewModel
(View-Model), чтобы задавать пользователю вопросы с множественным выбором из класса QuestionBank
(Model).
- home_screen.dart — Отображает экран с кнопкой « Новая игра»
- main.dart — настраивает
MaterialApp
на использование Material 3 и отображение домашнего экрана. - model.dart — определяет основные классы, используемые в приложении.
- question_screen.dart — отображает пользовательский интерфейс для игры-викторины
- view_model.dart — хранит состояние и логику для игры-викторины, отображаемой на
QuestionScreen
Приложение пока не поддерживает никаких анимированных эффектов, за исключением стандартного перехода между видами, отображаемого классом Flutter Navigator
, когда пользователь нажимает кнопку «Новая игра» .
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/вопрос_экран.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
.
Используйте DevTools для включения медленной анимации
Для отладки любого эффекта анимации Flutter DevTools предоставляет способ замедлить все анимации в вашем приложении, чтобы вы могли видеть анимацию более четко.
Чтобы открыть DevTools, убедитесь, что приложение работает в режиме отладки, и откройте Widget Inspector , выбрав его на панели инструментов отладки в VSCode или нажав кнопку Open Flutter DevTools в окне инструментов отладки в IntelliJ / Android Studio.
После открытия инспектора виджетов нажмите кнопку « Медленная анимация» на панели инструментов.
5. Используйте явные эффекты анимации
Как и неявные анимации, явные анимации являются предварительно созданными эффектами анимации, но вместо того, чтобы принимать целевое значение, они принимают объект Animation
в качестве параметра. Это делает их полезными в ситуациях, когда анимация уже определена навигационным переходом, AnimatedSwitcher
или AnimationController
, например.
Используйте явный эффект анимации
Чтобы начать работу с явным эффектом анимации, оберните виджет Card
в AnimatedSwitcher
.
lib/вопрос_экран.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>
, определяющий начальное и конечное смещение, между которыми будут перемещаться входящие и исходящие виджеты.
У Tween есть вспомогательная функция animate()
, которая преобразует любую Animation
в другую Animation
с примененным твином. Это означает, что Tween
может быть использован для преобразования Animation
предоставленный AnimatedSwitcher
в Animation
, который будет предоставлен виджету SlideTransition
.
lib/вопрос_экран.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/вопрос_экран.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/вопрос_экран.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/вопрос_экран.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
s.
Запуск анимации с помощью 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/вопрос_экран.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
в метод сборки AnswerCards
.
lib/вопрос_экран.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. Используйте пользовательские навигационные переходы
До сих пор мы видели, как настраивать эффекты на одном экране, но другой способ использования анимаций — использовать их для перехода между экранами. В этом разделе вы узнаете, как применять эффекты анимации к переходам между экранами, используя встроенные эффекты анимации и модные готовые эффекты анимации, предоставляемые официальным пакетом анимации на pub.dev .
Анимировать навигационный переход
Класс PageRouteBuilder
— это Route
, который позволяет вам настраивать анимацию перехода. Он позволяет вам переопределять его обратный вызов transitionBuilder
, который предоставляет два объекта Animation, представляющих входящую и исходящую анимацию, запускаемую Navigator.
Чтобы настроить анимацию перехода, замените 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
для использования PredictiveBack
на Android и эффекта плавного перехода из пакета анимации на других платформах:
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/вопрос_экран.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
и сохраните обратный вызов, который будет использоваться для отображения экрана «Game Over»:
lib/вопрос_экран.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/вопрос_экран.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
и обратного вызова открытого контейнера:
lib/вопрос_экран.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
- Как настроить анимацию перехода по умолчанию, включая добавление поддержки Predictive Back на Android.
Что дальше?
Ознакомьтесь с некоторыми из этих лабораторных работ:
- Создание анимированного адаптивного макета приложения с помощью Material 3
- Создание красивых переходов с помощью Material Motion для Flutter
- Превратите свое приложение Flutter из скучного в красивое
Или загрузите пример приложения анимации , демонстрирующий различные техники анимации.
Дальнейшее чтение
Больше ресурсов по анимации вы можете найти на flutter.dev:
- Введение в анимацию
- Учебник по анимации (учебник)
- Неявные анимации (учебник)
- Анимировать свойства контейнера (кулинарная книга)
- Появление и исчезновение виджета (кулинарная книга)
- Анимации героев
- Анимация перехода между страницами (кулинарная книга)
- Анимация виджета с использованием физической симуляции (кулинарная книга)
- Пошаговая анимация
- Виджеты анимации и движения (Каталог виджетов)
Или ознакомьтесь со следующими статьями на Medium:
- Глубокое погружение в анимацию
- Пользовательские неявные анимации во Flutter
- Управление анимацией с помощью Flutter и Flux / Redux
- Как выбрать подходящий именно вам виджет Flutter Animation?
- Направленная анимация со встроенными явными анимациями
- Основы анимации Flutter с неявной анимацией
- Когда следует использовать AnimatedBuilder или AnimatedWidget?