Рубрики
Flutter

Анимации во Flutter: Полное руководство 2025

Полное руководство по анимациям во Flutter: implicit, explicit, Hero animations, Custom painters, Rive, Lottie.

Анимации — это ключевой элемент пользовательского опыта. Правильно реализованные анимации делают приложение живым, интуитивным и приятным в использовании. В этом руководстве мы разберём все типы анимаций во 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 для профессиональных анимаций.