Рубрики
Flutter

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

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

Анимации — важная часть пользовательского опыта. В этом руководстве разберём все типы анимаций во Flutter.

Виды анимаций

Flutter поддерживает два основных типа:

  • Implicit animations — простые анимации с минимальным кодом
  • Explicit animations — сложные анимации с полным контролем

Implicit анимации

AnimatedContainer

class AnimatedContainerWidget extends StatefulWidget {
  @override
  State<AnimatedContainerWidget> createState() => _AnimatedContainerWidgetState();
}

class _AnimatedContainerWidgetState extends State<AnimatedContainerWidget> {
  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,
        color: _expanded ? Colors.blue : Colors.red,
        child: Center(child: Text('Tap me')),
      ),
    );
  }
}

AnimatedOpacity

class FadeWidget extends StatefulWidget {
  @override
  State<FadeWidget> createState() => _FadeWidgetState();
}

class _FadeWidgetState extends State<FadeWidget> {
  double _opacity = 1.0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        AnimatedOpacity(
          opacity: _opacity,
          duration: Duration(milliseconds: 500),
          child: FlutterLogo(size: 100),
        ),
        Slider(
          value: _opacity,
          onChanged: (value) => setState(() => _opacity = value),
        ),
      ],
    );
  }
}

AnimatedPositioned

class PositionedWidget extends StatefulWidget {
  @override
  State<PositionedWidget> createState() => _PositionedWidgetState();
}

class _PositionedWidgetState extends State<PositionedWidget> {
  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: Container(
                width: 100,
                height: 100,
                color: Colors.blue,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

AnimatedCrossFade

class CrossFadeWidget extends StatefulWidget {
  @override
  State<CrossFadeWidget> createState() => _CrossFadeWidgetState();
}

class _CrossFadeWidgetState extends State<CrossFadeWidget> {
  bool _showFirst = true;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        AnimatedCrossFade(
          firstChild: FlutterLogo(size: 100),
          secondChild: Icon(Icons.star, size: 100),
          crossFadeState: _showFirst
              ? CrossFadeState.showFirst
              : CrossFadeState.showSecond,
          duration: Duration(milliseconds: 500),
        ),
        ElevatedButton(
          onPressed: () => setState(() => _showFirst = !_showFirst),
          child: Text('Toggle'),
        ),
      ],
    );
  }
}

Explicit анимации

AnimationController

class ExplicitAnimationWidget extends StatefulWidget {
  @override
  State<ExplicitAnimationWidget> createState() => _ExplicitAnimationWidgetState();
}

class _ExplicitAnimationWidgetState extends State<ExplicitAnimationWidget>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();

    _controller = AnimationController(
      duration: Duration(seconds: 2),
      vsync: this,
    );

    _animation = Tween<double>(begin: 0, end: 1).animate(_controller);

    _controller.forward();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animation,
      builder: (context, child) {
        return Opacity(
          opacity: _animation.value,
          child: FlutterLogo(size: 100),
        );
      },
    );
  }
}

Tween анимация

class TweenAnimationWidget extends StatefulWidget {
  @override
  State<TweenAnimationWidget> createState() => _TweenAnimationWidgetState();
}

class _TweenAnimationWidgetState extends State<TweenAnimationWidget>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<Color?> _colorAnimation;
  late Animation<double> _sizeAnimation;

  @override
  void initState() {
    super.initState();

    _controller = AnimationController(
      duration: Duration(seconds: 2),
      vsync: this,
    )..repeat(reverse: true);

    _colorAnimation = ColorTween(
      begin: Colors.blue,
      end: Colors.red,
    ).animate(_controller);

    _sizeAnimation = Tween<double>(begin: 50, end: 150).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        return Container(
          width: _sizeAnimation.value,
          height: _sizeAnimation.value,
          decoration: BoxDecoration(
            color: _colorAnimation.value,
            shape: BoxShape.circle,
          ),
        );
      },
    );
  }
}

СтackedTween

class StackedAnimationWidget extends StatefulWidget {
  @override
  State<StackedAnimationWidget> createState() => _StackedAnimationWidgetState();
}

class _StackedAnimationWidgetState extends State<StackedAnimationWidget>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _opacity;
  late Animation<double> _size;
  late Animation<double> _rotation;

  @override
  void initState() {
    super.initState();

    _controller = AnimationController(
      duration: Duration(seconds: 2),
      vsync: this,
    )..repeat(reverse: true);

    _opacity = Tween<double>(begin: 0.5, end: 1).animate(_controller);
    _size = Tween<double>(begin: 50, end: 150).animate(_controller);
    _rotation = Tween<double>(begin: 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: _rotation.value,
          child: Opacity(
            opacity: _opacity.value,
            child: Container(
              width: _size.value,
              height: _size.value,
              color: Colors.blue,
            ),
          ),
        );
      },
    );
  }
}

Hero анимации

Базовая Hero

class FirstPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('First')),
      body: GestureDetector(
        onTap: () => Navigator.push(
          context,
          MaterialPageRoute(builder: (_) => SecondPage()),
        ),
        child: Hero(
          tag: 'image-hero',
          child: Image.network('https://example.com/image.jpg'),
        ),
      ),
    );
  }
}

class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Second')),
      body: Center(
        child: Hero(
          tag: 'image-hero',
          child: Image.network('https://example.com/image.jpg'),
        ),
      ),
    );
  }
}

Hero с placeholder

Hero(
  tag: 'avatar',
  placeholderBuilder: (context, heroSize, child) {
    return Container(
      width: heroSize.width,
      height: heroSize.height,
      child: CircularProgressIndicator(),
    );
  },
  child: CircleAvatar(
    backgroundImage: NetworkImage(url),
  ),
)

CustomPainter анимации

Анимированный painter

class AnimatedCirclePainter extends CustomPainter {
  final double progress;

  AnimatedCirclePainter(this.progress);

  @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 = Colors.blue
      ..strokeWidth = 10
      ..style = PaintingStyle.stroke;

    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;
  }
}

// Использование
class AnimatedCircleWidget extends StatefulWidget {
  @override
  State<AnimatedCircleWidget> createState() => _AnimatedCircleWidgetState();
}

class _AnimatedCircleWidgetState extends State<AnimatedCircleWidget>
    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(_controller.value),
          size: Size(200, 200),
        );
      },
    );
  }
}

Lottie анимации

Установка

dependencies:
  lottie: ^3.0.0

Использование

class LottieWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Lottie.asset('assets/animation.json');
  }
}

// С контроллером
class ControlledLottieWidget extends StatefulWidget {
  @override
  State<ControlledLottieWidget> createState() => _ControlledLottieWidgetState();
}

class _ControlledLottieWidgetState extends State<ControlledLottieWidget> {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();

    _controller = AnimationController(vsync: this);

    // Загрузка анимации
    _loadAnimation();
  }

  void _loadAnimation() {
    final composition = Lottie.asset('assets/animation.json');
    composition.then((value) {
      _controller.duration = value.duration;
    });
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Lottie.asset(
          'assets/animation.json',
          controller: _controller,
          onLoaded: (composition) {
            _controller.duration = composition.duration;
          },
        ),
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            IconButton(
              icon: Icon(Icons.play_arrow),
              onPressed: () => _controller.forward(),
            ),
            IconButton(
              icon: Icon(Icons.pause),
              onPressed: () => _controller.stop(),
            ),
          ],
        ),
      ],
    );
  }
}

Rive анимации

Установка

dependencies:
  flutter_rive: ^0.13.0

Использование

class RiveWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RiveAnimation.asset('assets/animation.riv');
  }
}

// С контроллером
class ControlledRiveWidget extends StatefulWidget {
  @override
  State<ControlledRiveWidget> createState() => _ControlledRiveWidgetState();
}

class _ControlledRiveWidgetState extends State<ControlledRiveWidget> {
  late SMITrigger _bump;

  void _onRiveInit(Artboard artboard) {
    final controller = StateMachineController.fromArtboard(artboard, 'bumpy');
    artboard.addController(controller!);
    _bump = controller.findInput<bool>('bump') as SMITrigger;
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => _bump.fire(),
      child: RiveAnimation.asset(
        'assets/animation.riv',
        onInit: _onRiveInit,
      ),
    );
  }
}

Curves

Популярные кривые

// Линейная
Curves.linear

// Ускорение
Curves.easeIn
Curves.easeInCubic
Curves.easeInQuart

// Замедление
Curves.easeOut
Curves.easeOutCubic

// Ускорение и замедление
Curves.easeInOut
Curves.easeInOutCubic

// Bounce эффект
Curves.bounceIn
Curves.bounceOut
Curves.elasticOut

Заключение

Анимации во Flutter — это мощный инструмент для создания потрясающего пользовательского интерфейса. Используйте implicit анимации для простых случаев и explicit для сложных сценариев.