← Назад к вопросам

Как работать с AnimationController?

2.2 Middle🔥 201 комментариев
#Анимации

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

AnimationController в Flutter: Полное руководство

AnimationController — это основной инструмент для управления анимациями в Flutter. Это не просто воспроизводит анимацию, а управляет временем, значениями и слушателями. Расскажу как это работает и как правильно его использовать.

1. Что такое AnimationController?

AnimationController — это объект, который:

  • Управляет временем анимации (от 0 до duration)
  • Генерирует значения от 0.0 до 1.0
  • Уведомляет слушателей об изменениях
  • Может повторять, реверсировать, останавливать
final controller = AnimationController(
  duration: Duration(seconds: 2),
  vsync: this, // требует TickerProvider (обычно State)
);

// После создания нужно запустить
controller.forward(); // Начать от 0 к 1

// Значение контроллера
print(controller.value); // 0.0 в начале, 1.0 в конце

2. Создание и инициализация

class AnimationScreen extends StatefulWidget {
  @override
  State<AnimationScreen> createState() => _AnimationScreenState();
}

class _AnimationScreenState extends State<AnimationScreen>
    with SingleTickerProviderStateMixin { // ← Важно!
  late AnimationController controller;

  @override
  void initState() {
    super.initState();
    
    controller = AnimationController(
      duration: Duration(milliseconds: 500),
      vsync: this, // TickerProvider
    );
    
    // Слушаем изменения
    controller.addListener(() {
      setState(() {}); // Перестроить при каждом обновлении
    });
    
    // Или слушаем завершение
    controller.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        print('Анимация завершена');
      }
    });
  }

  @override
  void dispose() {
    controller.dispose(); // Критично!
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        if (controller.isAnimating) {
          controller.stop();
        } else if (controller.isCompleted) {
          controller.reverse();
        } else {
          controller.forward();
        }
      },
      child: Container(
        width: 100 * controller.value, // Использовать значение
        height: 100,
        color: Colors.blue,
      ),
    );
  }
}

3. Основные методы

final controller = AnimationController(
  duration: Duration(seconds: 1),
  vsync: this,
);

// Воспроизведение анимации
controller.forward(); // От 0 к 1
await controller.forward(); // С ожиданием завершения

controller.reverse(); // От 1 к 0
await controller.reverse();

controller.repeat(); // Повторять бесконечно
controller.repeat(reverse: true); // Туда-сюда

controller.stop(); // Остановить
controller.reset(); // Вернуть к 0

controller.animateTo(0.5); // К произвольному значению
await controller.animateTo(0.5);

// Проверка статуса
print(controller.value); // Текущее значение (0.0 - 1.0)
print(controller.isAnimating); // Сейчас ли анимирует
print(controller.isCompleted); // Завершена ли
print(controller.isDismissed); // Находится ли в начале

4. Слушатели (Listeners)

controller.addListener(() {
  // Вызывается при каждом изменении значения
  // Много раз в секунду (60 раз при 60 FPS)
  print('Value: ${controller.value}');
});

controller.addStatusListener((status) {
  // Вызывается при изменении статуса
  // Может быть: dismissed, forward, reverse, completed
  switch (status) {
    case AnimationStatus.dismissed:
      print('Анимация в начале');
    case AnimationStatus.forward:
      print('Проигрывается вперёд');
    case AnimationStatus.reverse:
      print('Проигрывается в обратном');
    case AnimationStatus.completed:
      print('Анимация завершена');
  }
});

// Удалить слушателя
void removeListener(() {
  print('Старый слушатель');
});
controller.removeListener(removeListener);

5. Анимирование значений

Простой способ: умножить на значение

class SimpleAnimation extends StatefulWidget {
  @override
  State<SimpleAnimation> createState() => _SimpleAnimationState();
}

class _SimpleAnimationState extends State<SimpleAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
      duration: Duration(seconds: 1),
      vsync: this,
    );
    controller.addListener(() => setState(() {}));
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => controller.forward(from: 0.0),
      child: Transform.scale(
        scale: 0.5 + (controller.value * 0.5), // От 0.5 до 1.0
        child: Container(
          width: 100,
          height: 100,
          color: Colors.blue,
        ),
      ),
    );
  }

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

Правильный способ: Animation widget

class ProperAnimation extends StatefulWidget {
  @override
  State<ProperAnimation> createState() => _ProperAnimationState();
}

class _ProperAnimationState extends State<ProperAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController controller;
  late Animation<double> scaleAnimation;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
      duration: Duration(seconds: 1),
      vsync: this,
    );
    
    // Создаём Animation, которая преобразует значение
    scaleAnimation = Tween<double>(
      begin: 0.5,
      end: 1.0,
    ).animate(
      CurvedAnimation(parent: controller, curve: Curves.easeOut),
    );
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => controller.forward(from: 0.0),
      child: AnimatedBuilder(
        animation: scaleAnimation,
        builder: (context, child) => Transform.scale(
          scale: scaleAnimation.value,
          child: child,
        ),
        child: Container(
          width: 100,
          height: 100,
          color: Colors.blue,
        ),
      ),
    );
  }

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

6. Tween для преобразования значений

// Tween преобразует значение от 0-1 в диапазон begin-end
final colorAnimation = ColorTween(
  begin: Colors.red,
  end: Colors.blue,
).animate(controller);

final sizeAnimation = Tween<double>(
  begin: 10.0,
  end: 100.0,
).animate(controller);

final offsetAnimation = Tween<Offset>(
  begin: Offset.zero,
  end: Offset(200, 0),
).animate(controller);

// Использование
Container(
  width: sizeAnimation.value,
  height: sizeAnimation.value,
  color: colorAnimation.value,
)

7. CurvedAnimation - кривые анимации

// Без кривой - линейно
var linearAnimation = controller;

// С кривой - более естественно
var curvedAnimation = CurvedAnimation(
  parent: controller,
  curve: Curves.easeOut, // Начало быстрое, конец медленное
);

var animation = Tween<double>(
  begin: 0.0,
  end: 1.0,
).animate(curvedAnimation);

// Доступные кривые:
// Curves.linear - равномерно
// Curves.ease - ease-in-out
// Curves.easeIn - медленно начинает
// Curves.easeOut - медленно заканчивает
// Curves.easeInOut - медленно везде
// Curves.elasticIn - пружинка в начале
// Curves.elasticOut - пружинка в конце
// Curves.bounceOut - подпрыгивание

8. AnimatedBuilder - оптимизация

// ❌ Перестраивает весь виджет
class BadAnimation extends StatefulWidget {
  @override
  State<BadAnimation> createState() => _BadAnimationState();
}

class _BadAnimationState extends State<BadAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
      duration: Duration(seconds: 1),
      vsync: this,
    );
    controller.addListener(() => setState(() {})); // setState!
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        ExpensiveWidget(), // Перестраивается каждый раз!
        Transform.scale(
          scale: 0.5 + (controller.value * 0.5),
          child: Container(),
        ),
      ],
    );
  }
}

// ✅ Правильно: только AnimatedBuilder перестраивается
class GoodAnimation extends StatefulWidget {
  @override
  State<GoodAnimation> createState() => _GoodAnimationState();
}

class _GoodAnimationState extends State<GoodAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController controller;
  late Animation<double> scaleAnimation;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
      duration: Duration(seconds: 1),
      vsync: this,
    );
    scaleAnimation = Tween<double>(
      begin: 0.5,
      end: 1.0,
    ).animate(controller);
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        ExpensiveWidget(), // НЕ перестраивается
        AnimatedBuilder(
          animation: scaleAnimation,
          builder: (context, child) => Transform.scale(
            scale: scaleAnimation.value,
            child: child,
          ),
          child: Container(),
        ),
      ],
    );
  }
}

9. Множественные анимации

class MultipleAnimations extends StatefulWidget {
  @override
  State<MultipleAnimations> createState() => _MultipleAnimationsState();
}

class _MultipleAnimationsState extends State<MultipleAnimations>
    with TickerProviderStateMixin { // ← Важно: для многих анимаций
  late AnimationController scaleController;
  late AnimationController rotateController;
  late AnimationController opacityController;

  @override
  void initState() {
    super.initState();
    
    scaleController = AnimationController(
      duration: Duration(seconds: 1),
      vsync: this,
    );
    
    rotateController = AnimationController(
      duration: Duration(seconds: 2),
      vsync: this,
    );
    
    opacityController = AnimationController(
      duration: Duration(seconds: 1),
      vsync: this,
    );
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        scaleController.forward(from: 0.0);
        rotateController.repeat(); // Бесконечно
        opacityController.forward(from: 0.0);
      },
      child: AnimatedBuilder(
        animation: Listenable.merge([
          scaleController,
          rotateController,
          opacityController,
        ]),
        builder: (context, child) => Transform.scale(
          scale: 0.5 + (scaleController.value * 0.5),
          child: Transform.rotate(
            angle: rotateController.value * 2 * 3.14159,
            child: Opacity(
              opacity: opacityController.value,
              child: child,
            ),
          ),
        ),
        child: Container(
          width: 100,
          height: 100,
          color: Colors.blue,
        ),
      ),
    );
  }

  @override
  void dispose() {
    scaleController.dispose();
    rotateController.dispose();
    opacityController.dispose();
    super.dispose();
  }
}

10. Практический пример: Loading Spinner

class LoadingSpinner extends StatefulWidget {
  @override
  State<LoadingSpinner> createState() => _LoadingSpinnerState();
}

class _LoadingSpinnerState extends State<LoadingSpinner>
    with SingleTickerProviderStateMixin {
  late AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
      duration: Duration(seconds: 1),
      vsync: this,
    )..repeat(); // Повторять бесконечно
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: controller,
      builder: (context, child) => Transform.rotate(
        angle: controller.value * 2 * 3.14159,
        child: child,
      ),
      child: Container(
        width: 50,
        height: 50,
        decoration: BoxDecoration(
          border: Border.all(
            color: Colors.blue,
            width: 3,
          ),
          borderRadius: BorderRadius.circular(25),
        ),
      ),
    );
  }

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

11. Важные практики

✅ ДО:

// 1. Всегда вызови dispose()
@override
void dispose() {
  controller.dispose();
  super.dispose();
}

// 2. Используй AnimatedBuilder для оптимизации
AnimatedBuilder(animation: controller, ...)

// 3. Используй CurvedAnimation для естественности
CurvedAnimation(parent: controller, curve: Curves.easeOut)

// 4. Используй TickerProviderStateMixin
with SingleTickerProviderStateMixin { ... }

❌ ИЗБЕГАЙ:

// Забыть dispose()
controller.dispose(); // ← Обязательно!

// setState в listener
controller.addListener(() => setState(() {})); // Медленно

// Множественные контроллеры без TickerProvider
with SingleTickerProviderStateMixin { // Не хватит!

Вывод

AnimationController:

  • Управляет временем анимации
  • Генерирует значения от 0 к 1
  • Требует TickerProvider (State mixin)
  • Всегда вызови dispose()
  • Используй AnimatedBuilder для оптимизации
  • Используй Tween и CurvedAnimation для преобразований

Это основной инструмент для всех анимаций в Flutter!

Как работать с AnimationController? | PrepBro