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
для отображения необычных анимационных эффектов с минимальной настройкой.
Что вам понадобится
- 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
(View) использует класс QuizViewModel
(View-Model), чтобы задавать пользователю вопросы с несколькими вариантами ответов из класса QuestionBank
(Model).
- home_screen.dart — отображает экран с кнопкой «Новая игра» .
- main.dart — настраивает
MaterialApp
для использования Material 3 и отображения главного экрана. - model.dart — определяет основные классы, используемые в приложении.
- вопрос_экран.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/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( // NEW
isActive: score > i, // NEW
) // NEW
],
),
);
}
}
class AnimatedStar extends StatelessWidget { // Add from here...
final bool isActive;
final Duration _duration = const Duration(milliseconds: 1000);
final Color _deactivatedColor = Colors.grey.shade400;
final Color _activatedColor = Colors.yellow.shade700;
AnimatedStar({super.key, required this.isActive});
@override
Widget build(BuildContext context) {
return AnimatedScale(
scale: isActive ? 1.0 : 0.5,
duration: _duration,
child: Icon(
Icons.star,
size: 50,
color: isActive ? _activatedColor : _deactivatedColor,
),
);
}
} // To here.
Теперь, когда пользователь правильно отвечает на вопрос, виджет AnimatedStar
обновляет свой размер, используя неявную анимацию. color
Icon
здесь не анимируется, а только scale
, который создается виджетом AnimatedScale
.
Используйте Tween для интерполяции между двумя значениями
Обратите внимание, что цвет виджета AnimatedStar
меняется сразу после того, как поле isActive
принимает значение true.
Чтобы добиться анимированного цветового эффекта, вы можете попробовать использовать виджет AnimatedContainer
(который является еще одним подклассом ImplicitlyAnimatedWidget
), поскольку он может автоматически анимировать все свои атрибуты, включая цвет. К сожалению, наш виджет должен отображать значок, а не контейнер.
Вы также можете попробовать AnimatedIcon
, который реализует эффекты перехода между формами значков. Но в классе AnimatedIcons
нет реализации значка звездочки по умолчанию.
Вместо этого мы будем использовать другой подкласс ImplicitlyAnimatedWidget
, называемый TweenAnimationBuilder
, который принимает Tween
в качестве параметра. Анимация — это класс, который принимает два значения ( begin
и end
) и вычисляет промежуточные значения, чтобы анимация могла их отобразить. В этом примере мы будем использовать ColorTween
, который соответствует интерфейсу Tween<Color>
необходимому для создания нашего эффекта анимации.
Выберите Icon
и используйте быстрое действие «Обернуть с помощью 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, // Modify from here...
);
}, // To here.
),
);
}
}
Теперь перезагрузите приложение, чтобы увидеть новую анимацию.
Обратите внимание, что end
значение нашего ColorTween
меняется в зависимости от значения параметра isActive
. Это связано с тем, что TweenAnimationBuilder
повторно запускает анимацию при каждом изменении значения Tween.end
. Когда это происходит, новая анимация выполняется от текущего значения анимации до нового конечного значения, что позволяет вам изменить цвет в любое время (даже во время работы анимации) и отобразить эффект плавной анимации с правильными промежуточными значениями. .
Применить кривую
Оба этих анимационных эффекта выполняются с постоянной скоростью, но анимация часто становится визуально более интересной и информативной, когда она ускоряется или замедляется.
Curve
применяет функцию плавности , которая определяет скорость изменения параметра с течением времени. Flutter поставляется с коллекцией готовых кривых замедления в классе Curves
, таких как easeIn
или easeOut
.
Эти диаграммы (доступны на странице документации Curves
API) дают представление о том, как работают кривые. Кривые преобразуют входное значение от 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, убедитесь, что приложение работает в режиме отладки, и откройте Инспектор виджетов , выбрав его на панели инструментов «Отладка» в VSCode или нажав кнопку «Открыть Flutter DevTools» в окне инструмента «Отладка» в 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<Offset>
можно использовать для преобразования Animation<double>
предоставленного AnimatedSwitcher
, в Animation<Offset>
, который будет предоставлен виджету 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<double>
в диапазоне от 0,0 до 1,0 в T ween<Offset>
, который переходит от -0,1 до 0,0 по оси x. -ось.
В качестве альтернативы класс Animation имеет функцию drive()
, которая принимает любой Tween
(или Animatable
) и преобразует его в новый Animation
. Это позволяет «связывать» анимацию, делая итоговый код более кратким:
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
},
Настройка макетаBuilder
Вы можете заметить небольшую проблему с AnimationSwitcher. Когда карта вопроса переключается на новый вопрос, она размещает его в центре доступного пространства во время анимации, но когда анимация останавливается, виджет привязывается к верхней части экрана. Это приводит к некорректной анимации, поскольку конечное положение карточки вопроса не соответствует положению во время выполнения анимации.
Чтобы исправить это, 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.center
использует Alignment.topCenter
.
Краткое содержание
- Явные анимации — это анимационные эффекты, которые принимают объект Animation (в отличие от ImplicitlyAnimatedWidgets, которые принимают целевое значение и продолжительность).
- Класс Animation представляет бегущую анимацию, но не определяет конкретный эффект.
- Используйте Tween().animate или Animation.drive(), чтобы применить к анимации анимации движения и кривые (с использованием CurveTween).
- Используйте параметр LayoutBuilder AnimatedSwitcher, чтобы настроить расположение дочерних элементов.
6. Контролируйте состояние анимации
До сих пор каждая анимация запускалась платформой автоматически. Неявная анимация запускается автоматически, а явные эффекты анимации требуют, чтобы анимация работала правильно. В этом разделе вы узнаете, как создавать собственные объекты Animation с помощью AnimationController и использовать TweenSequence для объединения Tween-аниматоров.
Запустите анимацию с помощью AnimationController.
Чтобы создать анимацию с помощью AnimationController, вам необходимо выполнить следующие шаги:
- Создайте виджет с отслеживанием состояния
- Используйте миксин SingleTickerProviderStateMixin в своем классе State, чтобы предоставить тикер вашему AnimationController.
- Инициализируйте AnimationController в методе жизненного цикла initState, предоставив текущий объект State параметру
vsync
(TickerProvider). - Убедитесь, что ваш виджет перестраивается всякий раз, когда AnimationController уведомляет своих слушателей, либо с помощью AnimatedBuilder, либо путем вызова Listen() и setState вручную.
Создайте новый файл флип_эффект.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 используется для применения эффекта трехмерного вращения для имитации переворачивания карты.
Чтобы использовать этот виджет, оберните каждую карточку ответа виджетом CardFlipEffect. Обязательно укажите key
для виджета «Карточка»:
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 для реализации собственной версии. К сожалению, поскольку этому классу необходимо хранить предыдущий виджет в его состоянии, ему необходимо использовать 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/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>([ // NEW
if (widget.delayAmount > 0) // NEW
TweenSequenceItem( // NEW
tween: ConstantTween<double>(0.0), // NEW
weight: widget.delayAmount, // NEW
), // NEW
TweenSequenceItem( // NEW
tween: Tween(begin: 0.0, end: 1.0), // NEW
weight: 1.0, // NEW
), // NEW
]).animate(_animationController); // NEW
}
Наконец, замените анимацию AnimationController новой отложенной анимацией в методе сборки.
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, представляющие входящую и исходящую анимацию, запускаемую навигатором.
Чтобы настроить анимацию перехода, замените MaterialPageRoute
на PageRouteBuilder
и настройте анимацию перехода, когда пользователь переходит от HomeScreen
к QuestionScreen
. Используйте FadeTransition (явно анимированный виджет), чтобы новый экран появлялся поверх предыдущего.
lib/home_screen.dart
ElevatedButton(
onPressed: () {
// Show the question screen to start the game
Navigator.push(
context,
PageRouteBuilder( // NEW
pageBuilder: (context, animation, secondaryAnimation) { // NEW
return QuestionScreen(); // NEW
}, // NEW
transitionsBuilder: // NEW
(context, animation, secondaryAnimation, child) { // NEW
return FadeTransition( // NEW
opacity: animation, // NEW
child: child, // NEW
); // NEW
}, // NEW
), // NEW
);
},
child: Text('New Game'),
),
Пакет анимаций предоставляет модные готовые анимационные эффекты, такие как 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( // NEW
animation: animation, // NEW
secondaryAnimation: secondaryAnimation, // NEW
child: child, // NEW
); // NEW
},
),
);
},
child: Text('New Game'),
),
Настройте прогнозирующую анимацию спины
Predictive Back — это новая функция Android, которая позволяет пользователю заглянуть за текущий маршрут или приложение, чтобы увидеть, что находится за ним, прежде чем начинать навигацию. Анимация просмотра определяется положением пальца пользователя при его перетаскивании по экрану.
Flutter поддерживает системный прогнозирующий возврат, включая эту функцию на уровне системы, когда у Flutter нет маршрутов для отображения в его стеке навигации или, другими словами, когда возврат выходит из приложения. Эта анимация обрабатывается системой, а не самим Flutter.
Flutter также поддерживает функцию прогнозирования при навигации между маршрутами в приложении Flutter. Специальный PageTransitionsBuilder под названием PredictiveBackPageTransitionsBuilder
прослушивает предиктивные жесты возврата системы и управляет переходом страниц в зависимости от выполнения жеста.
Прогнозируемый возврат поддерживается только в Android U и выше, но Flutter плавно вернется к исходному поведению жеста назад и ZoomPageTransitionBuilder . Дополнительную информацию см. в нашем блоге , включая раздел о том, как настроить его в своем собственном приложении.
В конфигурации ThemeData вашего приложения настройте PageTransitionsTheme для использования PredictiveBack на Android и эффекта плавного перехода из пакета анимаций на других платформах:
библиотека/main.dart
import 'package:animations/animations.dart'; // NEW
import 'package:flutter/material.dart';
import 'home_screen.dart';
void main() {
runApp(MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
pageTransitionsTheme: PageTransitionsTheme(
builders: {
TargetPlatform.android: PredictiveBackPageTransitionsBuilder(), // NEW
TargetPlatform.iOS: FadeThroughPageTransitionsBuilder(), // NEW
TargetPlatform.macOS: FadeThroughPageTransitionsBuilder(), // NEW
TargetPlatform.windows: FadeThroughPageTransitionsBuilder(), // NEW
TargetPlatform.linux: FadeThroughPageTransitionsBuilder(), // NEW
},
),
),
home: HomeScreen(),
);
}
}
Теперь вы можете изменить вызов Navigator.push() обратно на MaterialPageRoute.
lib/home_screen.dart
ElevatedButton(
onPressed: () {
// Show the question screen to start the game
Navigator.push(
context,
MaterialPageRoute(builder: (context) { // NEW
return const QuestionScreen(); // NEW
}), // NEW
);
},
child: Text('New Game'),
),
Используйте FadeThroughTransition, чтобы изменить текущий вопрос.
Виджет AnimatedSwitcher предоставляет только одну анимацию в обратном вызове компоновщика. Чтобы решить эту проблему, пакет 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( // NEW
layoutBuilder: (entries) { // NEW
return Stack( // NEW
alignment: Alignment.topCenter, // NEW
children: entries, // NEW
); // NEW
}, // NEW
transitionBuilder: (child, animation, secondaryAnimation) { // NEW
return FadeThroughTransition( // NEW
animation: animation, // NEW
secondaryAnimation: secondaryAnimation, // NEW
child: child, // NEW
); // NEW
}, // NEW
duration: const Duration(milliseconds: 300),
child: Card(
key: ValueKey(question),
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
question ?? '',
style: Theme.of(context).textTheme.displaySmall,
),
),
),
);
}
}
Используйте ОпенКонтейнер
Виджет 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 из пакета анимаций, добавив два новых поля для viewModel и обратного вызова открытого контейнера:
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
- Как применить кривые и анимацию к анимации
- Как использовать готовые виджеты перехода, такие как 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?