Анимации — это ключевой элемент пользовательского опыта. Правильно реализованные анимации делают приложение живым, интуитивным и приятным в использовании. В этом руководстве мы разберём все типы анимаций во Flutter, от простых до продвинутых.
Виды анимаций во Flutter
Flutter поддерживает два основных подхода к анимациям:
- Implicit animations — простые анимации, которые Flutter автоматически анимирует при изменении свойств виджета
- Explicit animations — сложные анимации с полным контролем над процессом анимации
Когда использовать какой тип
Используйте implicit animations когда:
— Нужно простое изменение свойства (цвет, размер, прозрачность)
— Не нужен точный контроль над процессом анимации
— Хотите минимизировать код
Используйте explicit animations когда:
— Нужен точный контроль над анимацией
— Требуется сложная последовательность анимаций
— Нужно взаимодействовать с анимацией (пауза, реверс)
Implicit анимации
Implicit анимации — самый простой способ добавить анимацию во Flutter. Flutter автоматически анимирует переход между значениями свойства.
AnimatedContainer
AnimatedContainer анимирует изменения своих свойств: цвета, размера, позиции, выравнивания и других.
class AnimatedContainerExample extends StatefulWidget {
const AnimatedContainerExample({super.key});
@override
State<AnimatedContainerExample> createState() =>
_AnimatedContainerExampleState();
}
class _AnimatedContainerExampleState extends State<AnimatedContainerExample> {
bool _expanded = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => setState(() => _expanded = !_expanded),
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: _expanded ? 200 : 100,
height: _expanded ? 200 : 100,
decoration: BoxDecoration(
color: _expanded ? Colors.blue : Colors.red,
borderRadius: _expanded ? BorderRadius.circular(100) : BorderRadius.circular(8),
),
child: Center(
child: Text(
_expanded ? 'Expanded' : 'Collapsed',
style: TextStyle(color: Colors.white, fontSize: 16),
),
),
),
);
}
}
Ключевые моменты:
— duration определяет длительность анимации
— curve задаёт timing function (как быстро протекает анимация)
— Flutter автоматически анимирует все изменённые свойства
AnimatedOpacity
Для анимации прозрачности используйте AnimatedOpacity:
class FadeExample extends StatefulWidget {
const FadeExample({super.key});
@override
State<FadeExample> createState() => _FadeExampleState();
}
class _FadeExampleState extends State<FadeExample> {
double _opacity = 1.0;
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedOpacity(
opacity: _opacity,
duration: Duration(milliseconds: 500),
curve: Curves.easeInOut,
child: FlutterLogo(size: 100),
),
SizedBox(height: 20),
Slider(
value: _opacity,
onChanged: (value) => setState(() => _opacity = value),
),
],
);
}
}
AnimatedPositioned
Для анимации позиции в Stack используется AnimatedPositioned:
class PositionedAnimationExample extends StatefulWidget {
const PositionedAnimationExample({super.key});
@override
State<PositionedAnimationExample> createState() =>
_PositionedAnimationExampleState();
}
class _PositionedAnimationExampleState extends State<PositionedAnimationExample> {
bool _selected = false;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
AnimatedPositioned(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
top: _selected ? 100 : 50,
left: _selected ? 100 : 50,
child: GestureDetector(
onTap: () => setState(() => _selected = !_selected),
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
width: 100,
height: 100,
decoration: BoxDecoration(
color: _selected ? Colors.blue : Colors.red,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 10,
spreadRadius: 2,
),
],
),
),
),
),
],
),
);
}
}
AnimatedCrossFade
Для плавного перехода между двумя виджетами:
class CrossFadeExample extends StatefulWidget {
const CrossFadeExample({super.key});
@override
State<CrossFadeExample> createState() => _CrossFadeExampleState();
}
class _CrossFadeExampleState extends State<CrossFadeExample> {
bool _showFirst = true;
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedCrossFade(
firstChild: FlutterLogo(size: 100),
secondChild: Icon(Icons.star, size: 100, color: Colors.amber),
crossFadeState: _showFirst
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
duration: Duration(milliseconds: 500),
firstCurve: Curves.easeIn,
secondCurve: Curves.easeOut,
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () => setState(() => _showFirst = !_showFirst),
child: Text('Toggle'),
),
],
);
}
}
Другие implicit анимации
Flutter предоставляет множество готовых анимированных виджетов:
AnimatedAlign — анимация выравнивания
AnimatedPadding — анимация отступов
AnimatedScale — анимация масштаба
AnimatedRotation — анимация вращения
AnimatedDefaultTextStyle — анимация стиля текста
AnimatedPhysicalModel — анимация карточки с elevation
AnimatedTheme — анимация темы
Explicit анимации
Explicit анимации дают полный контроль над процессом анимации. Они требуют больше кода, но предоставляют больше гибкости.
AnimationController
ОсExplicit анимаций — AnimationController. Он управляет жизненным циклом анимации.
import 'package:flutter/material.dart';
class ExplicitAnimationExample extends StatefulWidget {
const ExplicitAnimationExample({super.key});
@override
State<ExplicitAnimationExample> createState() =>
_ExplicitAnimationExampleState();
}
class _ExplicitAnimationExampleState extends State<ExplicitAnimationExample>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
// Создаём контроллер
_controller = AnimationController(
duration: Duration(seconds: 2),
vsync: this,
);
// Создаём анимацию с кривой
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
);
// Запускаем анимацию
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Center(
child: AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Transform.scale(
scale: _animation.value,
child: FlutterLogo(size: 100),
);
},
),
);
}
}
Ключевые компоненты:
— SingleTickerProviderStateMixin — предоставляет vsync для оптимизации
— AnimationController — управляет анимацией
— CurvedAnimation — задаёт timing curve
— AnimatedBuilder — перестраивает UI при изменении анимации
Tween анимации
Tween (abbreviation for in-between) определяет диапазон значений анимации.
class TweenAnimationExample extends StatefulWidget {
const TweenAnimationExample({super.key});
@override
State<TweenAnimationExample> createState() => _TweenAnimationExampleState();
}
class _TweenAnimationExampleState extends State<TweenAnimationExample>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<Color?> _colorAnimation;
late Animation<double> _sizeAnimation;
late Animation<double> _rotationAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(seconds: 2),
vsync: this,
)..repeat(reverse: true);
// Tween для цвета
_colorAnimation = ColorTween(
begin: Colors.blue,
end: Colors.red,
).animate(_controller);
// Tween для размера
_sizeAnimation = Tween<double>(
begin: 50.0,
end: 150.0,
).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
),
);
// Tween для вращения
_rotationAnimation = Tween<double>(
begin: 0.0,
end: 2 * pi,
).animate(_controller);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.rotate(
angle: _rotationAnimation.value,
child: Container(
width: _sizeAnimation.value,
height: _sizeAnimation.value,
decoration: BoxDecoration(
color: _colorAnimation.value,
shape: BoxShape.circle,
),
),
);
},
);
}
}
Stacked анимации
Для одновременной анимации нескольких свойств используйте несколько Tween:
class StackedAnimationExample extends StatefulWidget {
const StackedAnimationExample({super.key});
@override
State<StackedAnimationExample> createState() =>
_StackedAnimationExampleState();
}
class _StackedAnimationExampleState extends State<StackedAnimationExample>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _opacity;
late Animation<double> _scale;
late Animation<double> _translation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 1500),
vsync: this,
);
_opacity = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(0.0, 0.5, curve: Curves.easeIn),
),
);
_scale = Tween<double>(begin: 0.5, end: 1.0).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(0.0, 0.5, curve: Curves.elasticOut),
),
);
_translation = Tween<double>(begin: 100.0, end: 0.0).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(0.3, 1.0, curve: Curves.easeOut),
),
);
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Opacity(
opacity: _opacity.value,
child: Transform.translate(
offset: Offset(0, _translation.value),
child: Transform.scale(
scale: _scale.value,
child: Container(
width: 200,
height: 200,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(20),
),
),
),
),
);
},
);
}
}
Hero анимации
Hero анимации создают плавный переход при навигации между экранами. Виджет с одинаковым tag плавно переходит из одной позиции в другую.
Базовая Hero анимация
class FirstScreen extends StatelessWidget {
const FirstScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('First Screen')),
body: GridView.count(
crossAxisCount: 2,
children: List.generate(10, (index) {
return GestureDetector(
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => SecondScreen(imageIndex: index),
),
),
child: Hero(
tag: 'image-$index',
child: Image.network(
'https://picsum.photos/200?random=$index',
fit: BoxFit.cover,
),
),
);
}),
),
);
}
}
class SecondScreen extends StatelessWidget {
final int imageIndex;
const SecondScreen({required this.imageIndex, super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Second Screen')),
body: Center(
child: Hero(
tag: 'image-$imageIndex',
child: Image.network(
'https://picsum.photos/400?random=$imageIndex',
fit: BoxFit.contain,
),
),
),
);
}
}
Hero с placeholder
Для загрузки изображения во время анимации:
Hero(
tag: 'image-$index',
placeholderBuilder: (context, heroSize, child) {
return Container(
width: heroSize.width,
height: heroSize.height,
color: Colors.grey[300],
child: Center(child: CircularProgressIndicator()),
);
},
child: Image.network(imageUrl),
)
CustomPainter анимации
Для создания кастомных анимированных графики используйте CustomPainter.
Анимированный круг
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
class AnimatedCirclePainter extends CustomPainter {
final double progress;
final Color color;
AnimatedCirclePainter({
required this.progress,
required this.color,
});
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = size.width / 2 * 0.8;
final paint = Paint()
..color = color
..strokeWidth = 10
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
const startAngle = -pi / 2;
final sweepAngle = 2 * pi * progress;
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
startAngle,
sweepAngle,
false,
paint,
);
}
@override
bool shouldRepaint(AnimatedCirclePainter oldDelegate) {
return oldDelegate.progress != progress || oldDelegate.color != color;
}
}
// Использование
class CircularProgressWidget extends StatefulWidget {
const CircularProgressWidget({super.key});
@override
State<CircularProgressWidget> createState() =>
_CircularProgressWidgetState();
}
class _CircularProgressWidgetState extends State<CircularProgressWidget>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(seconds: 2),
vsync: this,
)..repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return CustomPaint(
painter: AnimatedCirclePainter(
progress: _controller.value,
color: Colors.blue,
),
size: Size(200, 200),
);
},
);
}
}
Curves
Curves определяют, как быстро протекает анимация во времени.
Популярные кривые
// Линейная — постоянная скорость
Curves.linear
// Ускорение
Curves.easeIn
Curves.easeInCubic
Curves.easeInQuart
Curves.easeInQuad
// Замедление
Curves.easeOut
Curves.easeOutCubic
Curves.easeOutQuad
// Ускорение и замедление
Curves.easeInOut
Curves.easeInOutCubic
Curves.easeInOutQuad
// Bounce эффекты
Curves.bounceIn
Curves.bounceOut
Curves.bounceInOut
// Elastic эффекты
Curves.elasticIn
Curves.elasticOut
Curves.elasticInOut
// Специализированные
Curves.fastOutSlowIn // Material Design default
Curves.slowMiddle
Custom curve
class CustomCurve extends Curve {
@override
double transformInternal(double t) {
// Ваша custom кривая
return sin(t * pi);
}
}
Lottie анимации
Lottie — это библиотека для анимаций, созданных в Adobe After Effects.
Установка
dependencies:
lottie: ^3.0.0
Использование
import 'package:lottie/lottie.dart';
class LottieExample extends StatelessWidget {
const LottieExample({super.key});
@override
Widget build(BuildContext context) {
return Lottie.asset('assets/animations/loader.json');
}
}
Контролируемая Lottie анимация
class ControlledLottieExample extends StatefulWidget {
const ControlledLottieExample({super.key});
@override
State<ControlledLottieExample> createState() =>
_ControlledLottieExampleState();
}
class _ControlledLottieExampleState extends State<ControlledLottieExample> {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this);
// Загружаем анимацию и получаем длительность
_loadAnimation();
}
void _loadAnimation() async {
final composition = await Lottie.asset('assets/animations/loader.json');
_controller.duration = composition.duration;
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Lottie.asset(
'assets/animations/loader.json',
controller: _controller,
onLoaded: (composition) {
_controller.duration = composition.duration;
},
frameRate: FrameRate(60),
repeat: true,
animate: true,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(Icons.play_arrow),
onPressed: () => _controller.forward(),
),
IconButton(
icon: Icon(Icons.pause),
onPressed: () => _controller.stop(),
),
IconButton(
icon: Icon(Icons.stop),
onPressed: () {
_controller.stop();
_controller.reset();
},
),
],
),
],
);
}
}
Rive анимации
Rive (ранее Flare) — это мощный инструмент для создания real-time анимаций.
Установка
dependencies:
flutter_rive: ^0.13.0
Использование
import 'package:flutter_rive/flutter_rive.dart';
class RiveExample extends StatelessWidget {
const RiveExample({super.key});
@override
Widget build(BuildContext context) {
return RiveAnimation.asset('assets/vehicles.riv');
}
}
Контролируемая Rive анимация
class ControlledRiveExample extends StatefulWidget {
const ControlledRiveExample({super.key});
@override
State<ControlledRiveExample> createState() => _ControlledRiveExampleState();
}
class _ControlledRiveExampleState extends State<ControlledRiveExample> {
SMITrigger? _bump;
void _onRiveInit(Artboard artboard) {
final controller = StateMachineController.fromArtboard(
artboard,
'bumpy', // Имя state machine
);
artboard.addController(controller!);
_bump = controller.findInput<bool>('bump') as SMITrigger;
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => _bump?.fire(),
child: RiveAnimation.asset(
'assets/vehicles.riv',
onInit: _onRiveInit,
),
);
}
}
Best Practices
1. Используйте implicit анимации когда возможно
Implicit анимации проще в использовании и требуют меньше кода.
// Хорошо
AnimatedOpacity(opacity: _visible ? 1.0 : 0.0, child: widget)
// Плохо (для простых случаев)
OpacityAnimation(opacity: _visible ? 1.0 : 0.0, child: widget)
2. Оптимизируйте производительность
// Используйте const конструкторы где возможно
const SizedBox(height: 16)
// Изолируйте дорогие операции
RepaintBoundary(
child: ExpensiveAnimationWidget(),
)
3. Удаляйте контроллеры
@override
void dispose() {
_controller.dispose(); // Обязательно!
super.dispose();
}
4. Используйте правильные curves
// Входящие элементы — easeOut
Curves.easeOut
// Исходящие элементы — easeIn
Curves.easeIn
// Естественное движение — easeInOut
Curves.easeInOut
Заключение
Анимации во Flutter — это мощный инструмент для создания потрясающего пользовательского интерфейса. Используйте implicit анимации для простых случаев, explicit для сложного контроля, Hero для навигации, и Lottie/Rive для профессиональных анимаций.