Irbisa · Шпаргалка

Анимации и движение

AnimationController, Ticker, Tween, Curves, AnimatedBuilder, staggered-анимации, Lottie и Rive

Middle-разработчик · 21 вопрос

  1. 01

    Что такое Animation, AnimationController, Tween и Ticker во Flutter и как они работают вместе?

    AnimationController выдаёт число, которое за duration проходит от 0 до 1 и продвигается раз в кадр с помощью Ticker; Tween переводит эти 0..1 в нужный диапазон, и в итоге получается Animation<T>, которую слушают transition-виджеты и по которой перерисовываются.

    class _PopInState extends State<PopIn> with SingleTickerProviderStateMixin {  // vsync: this -> the mixin creates the Ticker that drives the controller  late final AnimationController _controller = AnimationController(    vsync: this,    duration: const Duration(milliseconds: 400),  )..forward();…
  2. 02

    Как неявный анимированный виджет вроде AnimatedContainer или AnimatedOpacity понимает, что анимировать?

    Вы перестраиваете его с новым значением свойства; он сравнивает его с предыдущим и анимирует от того, что сейчас на экране, к новой цели за свои duration и curve, причём контроллером, который сам создаёт и сам уничтожает.

    class _ToggleCardState extends State<ToggleCard> {  bool _open = false;   @override  Widget build(BuildContext context) {    return GestureDetector(…
  3. 03

    Что не так с этим State и почему его виджет-тест падает с «was disposed with an active Ticker»?

    class _FaderState extends State<Fader> with SingleTickerProviderStateMixin {  @override  Widget build(BuildContext context) {    final controller = AnimationController(      vsync: this,      duration: widget.duration,    )..forward();    return FadeTransition(opacity: controller, child: widget.child);  }}

    Контроллер создаётся в build и нигде не уничтожается.

  4. 04

    Зачем контроллеру анимации нужен vsync и когда вместо SingleTickerProviderStateMixin нужен TickerProviderStateMixin?

    vsync выдаёт Ticker, который продвигает контроллер раз в кадр и глушит его, когда поддерево скрыто.

  5. 05

    Что выведет этот код и почему stop() не меняет статус контроллера?

    final c = AnimationController(  vsync: this,  duration: const Duration(milliseconds: 400),)..addStatusListener((s) => debugPrint(s.name)); c.forward();await Future<void>.delayed(const Duration(milliseconds: 200));c.stop();debugPrint('${c.status.name} animating=${c.isAnimating}');c.reverse();

    Он выведет forward, затем forward animating=false, затем reverse, а примерно через 200 мс выведет dismissed.

  6. 06

    Найдите баг: сердечко лайка пульсирует на первом тапе, а на всех следующих иконка переключается без анимации.

    class _LikeState extends State<Like> with SingleTickerProviderStateMixin {  late final _pop = AnimationController( // disposed in dispose()      vsync: this, duration: const Duration(milliseconds: 300));  late final _scale = _pop.drive(TweenSequence([    TweenSequenceItem(tween: Tween(begin: 1.0, end: 1.3), weight: 1),    TweenSequenceItem(tween: Tween(begin: 1.3, end: 1.0), weight: 1),  ]));  @override  Widget build(BuildContext context) => IconButton(        onPressed: () {          widget.onToggle();          _pop.forward();        },        icon: ScaleTransition(scale: _scale, child: Icon(widget.icon)),      );}

    forward() ведёт контроллер к верхней границе от текущего значения.

  7. 07

    Как Tween превращает 0..1 контроллера в Color, Offset или TextStyle и что возвращает tween.animate(controller)?

    Tween задаёт формулу, а не анимацию: вы задаёте begin и end, а transform(t) возвращает значение в точке t между ними.

    class _ProfileCardState extends State<ProfileCard>    with SingleTickerProviderStateMixin {  late final _c = AnimationController(      vsync: this, duration: const Duration(milliseconds: 350))..forward();   // Offset has + - *, so the base Tween works…
  8. 08

    Почему Curves.easeOutBack ломает анимацию Opacity и как оставить перелёт там, где он допустим?

    Curves.easeOutBack и семейство elastic намеренно заходят за 1, прежде чем осесть (in-кривые ещё и уходят ниже 0), поэтому tween-ы дальше по цепочке получают значения вроде 1.09.

    late final _c = AnimationController(    vsync: this, duration: const Duration(milliseconds: 420)); // Scale may overshoot: easeOutBack peaks above 1.0late final _pop = CurvedAnimation(  parent: _c,…
  9. 09

    Как написать свою кривую анимации и что она обязана возвращать при t = 0 и t = 1?

    Наследуйтесь от Curve и переопределите transformInternal(double t), который переводит время 0..1 в прогресс.

    // A designer's cubic-bezier(0.2, 0, 0, 1): no subclass neededconst emphasized = Cubic(0.2, 0.0, 0.0, 1.0); // CSS steps(n): jumps in n equal steps, e.g. a ticking clock handclass StepsCurve extends Curve {  const StepsCurve(this.steps) : assert(steps > 0);…
  10. 10

    Когда использовать AnimatedBuilder, свой AnimatedWidget или встроенный *Transition-виджет для явной анимации?

    Все три слушают Animation и перерисовываются на её тиках, так что setState вызывать самим не нужно.

    // One-off effect: AnimatedBuilder, with the static subtree passed as childAnimatedBuilder(  animation: _controller,  child: const ProductImage(), // built once, not on every tick  builder: (context, child) => Transform(    alignment: Alignment.center,…
  11. 11

    Экран нужно закрыть ровно в момент окончания анимации ухода: await controller.reverse() или status listener, и что будет, если пользователь её прервёт?

    forward() и reverse() возвращают TickerFuture, который завершается, только если анимация дошла до конца.

    // wrong: after an interruption this never returns, so callers awaiting close() hang and finally blocks never runFuture<void> closeNaive() async {  await _c.reverse();  Navigator.of(context).pop();}…
  12. 12

    Найдите баг: после третьего неверного PIN одна тряска отправляет событие аналитики pin_error_shown три раза.

    class _PinFieldState extends State<PinField> with SingleTickerProviderStateMixin {  late final _shake = AnimationController(vsync: this, duration: Durations.medium4);  void onWrongPin() {    _shake.addStatusListener((s) {      if (s.isCompleted) widget.onShaken();    });    _shake.forward(from: 0);  }  @override  Widget build(BuildContext context) => AnimatedBuilder(        animation: _shake,        builder: (_, child) => Transform.translate(            offset: Offset(sin(_shake.value * pi * 6) * 12, 0), child: child),        child: widget.child,      );} // dispose() disposes _shake

    onWrongPin при каждом вызове добавляет новый status listener и никогда его не удаляет.

  13. 13

    Карточка проявляется, потом заголовок выезжает вверх, потом пружинит кнопка: один контроллер с Interval или три контроллера, и как держать их синхронными?

    Один контроллер: stagger представляет собой единый таймлайн, контроллер и есть этот таймлайн, а каждый Interval говорит, какой его кусок принадлежит свойству.

    // One clock for the whole sequence: 300 + 300 + 300 mslate final _c = AnimationController(    vsync: this, duration: const Duration(milliseconds: 900))..forward(); // Each slice is a FRACTION of that durationlate final Animation<double> _cardFade = _c.drive(…
  14. 14

    Бейдж прогресса анимируется к значению, которое передаёт родитель, и новое значение приходит прямо в полёте: что TweenAnimationBuilder делает с вашим tween?

    Он забирает ваш tween себе: при каждом новом end ставит в begin значение, которое сейчас на экране, в end ставит новую цель и перезапускает внутренний контроллер с 0.

    class UploadBadge extends StatelessWidget {  const UploadBadge({super.key, required this.progress});   final double progress; // the parent may pass a new target at any time   @override…
  15. 15

    Ваш AnimatedSwitcher меняет текст, но ничего не анимирует: как он решает, что ребёнок новый, и как это исправить?

    AnimatedSwitcher решает «ребёнок новый» через Widget.canUpdate: тот же runtime-тип и тот же key означают тот же виджет.

    // wrong: same type, no key -> Widget.canUpdate says "same widget", no animationAnimatedSwitcher(  duration: const Duration(milliseconds: 250),  child: Text('$_count'),);…
  16. 16

    Зачем AnimatedList.removeItem просит билдер для строки, которую вы уже убрали из данных, и что должно произойти в том же шаге?

    Потому что удалённая строка должна оставаться на экране, пока проигрывается её исчезновение, а в данных её уже нет, поэтому AnimatedList просит вас отдать виджет самим через билдер, переданный в removeItem.

    class _InboxState extends State<Inbox> {  final _listKey = GlobalKey<AnimatedListState>();  final List<Mail> _mails = List.of(seedMails);   Widget _row(Mail mail, Animation<double> animation) => SizeTransition(        sizeFactor: animation,…
  17. 17

    Дизайнер отдаёт четырёхсекундную анимацию праздника: когда выбрать Lottie, Rive или собрать анимацию на Flutter и как проиграть её из кода?

    Lottie проигрывает фиксированный таймлайн из After Effects, который вы запускаете, зацикливаете или перематываете; Rive крутит state machine, входы которой вы задаёте из Dart, поэтому подходит для анимаций, реагирующих на ввод; анимация на Flutter остаётся обычным кодом, который следует теме и состоянию приложения и легко тестируется.

    late final _c = AnimationController(vsync: this); Lottie.asset(  'assets/celebrate.json',  controller: _c,  onLoaded: (composition) {…
  18. 18

    Карточка растёт, когда раскрывается блок деталей, а её высота неизвестна до прохода layout: как анимировать размер, для которого нельзя написать Tween?

    Пусть числа даёт layout, а анимируется контейнер, а не значение: AnimatedSize измеряет ребёнка и, когда измерение меняется, плавно ведёт собственный размер от старого к новому; ни begin, ни end вы не пишете.

    // The height comes out of layout, so animate the box, not a numberAnimatedSize(  duration: const Duration(milliseconds: 220),  curve: Curves.easeOut,  alignment: Alignment.topCenter,  child: Column(…
  19. 19

    Как сделать свой неявный AnimatedX-виджет: что заставляет реализовать ImplicitlyAnimatedWidget и каков контракт forEachTween?

    Наследуйтесь от ImplicitlyAnimatedWidget, сделайте его State наследником AnimatedWidgetBaseState и реализуйте один метод, forEachTween: вы отдаёте фреймворку каждый хранимый tween и сохраняете обратно тот, что он вернул.

    class AnimatedRing extends ImplicitlyAnimatedWidget {  const AnimatedRing({    super.key,    required this.color,    required this.thickness,    required super.duration,…
  20. 20

    Нижняя шторка должна следовать за пальцем, а после отпускания доводить движение сама: как связать AnimationController с драгом?

    Пока палец на экране, вы сами пишете в controller.value на каждом обновлении драга; когда палец отрывается, отдаёте скорость жеста в controller.fling(velocity: …), и контроллер доводит движение пружиной с той скоростью, которую оставил палец.

    class _SheetState extends State<Sheet> with SingleTickerProviderStateMixin {  static const double _extent = 320; // sheet height in logical pixels   // 0 = hidden, 1 = open  late final _c = AnimationController(      vsync: this, duration: const Duration(milliseconds: 300));…
  21. 21

    Карточка возвращается на место с easeOut, и тестировщики говорят, что движение мёртвое: когда физическая симуляция выигрывает у любой кривой?

    У кривой фиксированные форма и длительность, поэтому она не может использовать скорость, которая у карточки уже есть; Simulation стартует с реальной скорости и физических констант, а её длительность получается из физики.

    // unbounded: an underdamped spring passes its target,// and a 0..1 controller would clamp that overshoot awaylate final _c = AnimationController.unbounded(vsync: this); static final _spring = SpringDescription.withDampingRatio(  mass: 1,…