Flutter: анимации, которые всегда с тобой

15.09.23, Пт, 10:00, Мск,

Привет всем! Сегодня мы погрузимся в увлекательный мир анимаций в Flutter, популярном фреймворке для разработки кроссплатформенных мобильных приложений. Созданный Google, Flutter предоставляет обширный набор виджетов и инструментов, позволяющих разработчикам создавать визуально привлекательные и высокопроизводительные приложения. Одной из важных частей улучшения пользовательского интерфейса и обеспечения плавного взаимодействия с приложением являются анимации.

Содержание

Анимации могут придать вашему приложению более интуитивное восприятие, способствовать стильному внешнему виду и улучшить общее впечатление от работы с приложением.

Эта статья познакомит вас с различными типами анимаций, доступных в Flutter и предоставит практические примеры для иллюстрации их реализации. Статья особенно полезна для начинающих разработчиков, стремящимся улучшить визуальное восприятие своего приложения, но также, наверняка будет полезна и опытным разработчикам, кто стремится максимально просто и эффективно использовать анимации в своей работе. Давайте начнем этот захватывающий путь в мир анимаций Flutter!

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

1. Неявные анимации (анимации внутри виджетов)

Это самый простой тип анимации. Они автоматически анимируют свойство виджета в течение определенного времени при изменении целевого значения. Примеры неявных анимаций включают виджеты AnimatedContainer, AnimatedRotation, AnimatedOpacity, AnimatedCrossFade, AnimatedIcon, AnimatedTheme - всего во Flutter более 20 анимированных виджетов, каждый из которых имеет свое назначение.

Для реализации вращения виджета используем AnimatedRotation внутри Stateful-виджета. В первую очередь, определим количество поворотов при создании виджета:

class _BuildInScreenState extends State<BuildInScreen> {
  double turns = 0;

Далее внутри метода build, в Scaffold используем AnimatedRotation:

        Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            AnimatedRotation(
              turns: turns,
              duration: const Duration(seconds: 1),
              child: const FlutterLogo(
                size: 50,
              ),
            ),
            ElevatedButton(
                onPressed: () {
                  setState(() {
                    turns++;
                  });
                },
                child: const Text('Rotate')),
          ],
        ),
      ),

При изменении значения turns, AnimatedRotation получает новый параметр отображения и отрабатывает его анимацией с заданной продолжительностью. Точно таким же образом работают и остальные базовые виджеты Flutter с приставкой Animated. При этом анимироваться могут самые разные типы свойств. Яркий пример этому - AnimatedContainer, который предоставляет десятки свойств подлежащих анимации - размер, цвет заливки, отступы, выравнивание и т.д.Как с помощью EvaProject и EvaWiki построить прозрачную бесшовную среду для успешной работы крупного холдинга

Это очень мощный инструмент базового функционала Flutter и, если стоит задача интегрировать анимацию в интерфейс, в первую очередь, следует рассмотреть именно Animated-виджеты.

2. Явные анимации (использующие AnimatedController)

В этом и следующих разделах нам потребуется подготовить наш код для использования контроллера анимации - AnimatedController. Для этого выполним несколько обязательных действий:

1. Применим миксин SingleTickerProviderStateMixin на State нашего главного виджета

class _AnimatedBuilderScreenState extends State<AnimatedBuilderScreen>
    with SingleTickerProviderStateMixin {

Данный миксин предоставляет тики - единицы времени, используемые для анимации.

2. Создадим AnimationController

late final AnimationController _controller = AnimationController(
    duration: const Duration(seconds: 1),
    vsync: this,
  );
 
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

Метод dispose позволяет нам избежать утечек памяти при закрытии экрана с данным виджетом.

Далее у нас есть несколько возможностей внедрения анимации - с использованием AnimatedBuilder так и без него.

2.1 Явные анимации с использованием AnimatedBuilder

Сперва рассмотрим вариант с AnimatedBuilder внутри Column:

AnimatedBuilder(
                animation: _controller,
                child: const FlutterLogo(
                  size: 50,
                ),
                builder: (ctx, child) {
                  return Transform.rotate(
                    angle: _controller.value * 2.0 * pi,
                    child: child,
                  );
                }),
             ElevatedButton(
                onPressed: () {
                  _controller.forward(from: 0);
                },
                child: const Text('Rotate')),

Использование AnimatedBuilder имеет ряд преимуществ - более читабельный, модульный код и нет необходимости создавать дополнительный listener на _controller для обновления состояния виджета.

2.2 Явные анимации без использования AnimatedBuilder

Здесь нам потребуется немного модифицировать инициализацию AnimatedController, т.к. для отрисовки каждого тика анимации нам требуется вызывать функцию setState. Для этого добавим listener на наш _controller (и не забудем про dispose!).

late final AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(seconds: 1),
      vsync: this,
    )..addListener(_controllerListener);
  }
 
  @override
  void dispose() {
    _controller.removeListener(_controllerListener);
    _controller.dispose();
    super.dispose();
  }
 
  _controllerListener() {
    setState(() {});
  }

Далее, непосредственно в методе build вместо AnimatedBuilder мы можем вызвать сразу требуемый виджет:

         Transform.rotate(
              angle: _controller.value * 2.0 * pi,
              child: const FlutterLogo(
                size: 50,
              ),
            ),

3. Использование внешнего пакета для анимаций (flutter_animate)

Данный вариант предоставляет большое удобство при работе с виджетами без необходимости добавлять специальный listener для отрисовки нового фрейма при следующем тике анимации. Точно также, как в варианте 2.1 применим миксин и инициализируем AnimationController. Далее в методе build вместо AnimatedBuilder указываем виджет, подлежащий анимации и применяем сразу требуемые параметры для вращения:

FlutterLogo(size: 50,)
                  .animate(controller: _controller, autoPlay: false)
                  .rotate(duration: const Duration(seconds: 1)),

Особенно удобно использовать данный пакет совместно с другим универсальным пакетом, добавляющим тонну удобства при написании кода на Flutter – flutter_hooks (очень рекомендую!).

При использовании flutter_hooks мы можем отказаться от StatefulWidget и работать внутри HookWidget - имплементации StatelessWidget. Как мы знаем, в целях оптимизации кода мы должны максимально использовать именно StatelessWidget-ы, т. к. они предоставляют лучшую производительность и используют меньше ресурсов. Полный код виджета при использовании обеих пакетов flutter_animate и flutter_hooks:

class UsePackageWithHooksScreen extends HookWidget {
  const UsePackageWithHooksScreen({super.key});
 
  @override
  Widget build(BuildContext context) {
    final controller = useAnimationController(duration: const Duration(seconds: 1));
 
    return Scaffold(
      appBar: AppBar(title: const Text('Use package and hooks'), ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            RepaintBoundary(
              child: const FlutterLogo(
                size: 50,
              )
                  .animate(controller: controller, autoPlay: false)
                  .rotate(duration: const Duration(seconds: 1)),
            ),
            ElevatedButton(
                onPressed: () => controller.forward(from: 0),
                child: const Text('Rotate')),
          ],
        ),
      ),
    );
  }
}

Обратите внимание, что здесь дополнительно использован виджет RepaintBoundary. Данный виджет позволяет нам ограничить область перерисовки экрана только областью в который отображается анимация. Без него будет перерисовываться весь экран при каждом тике анимации. Измененную область отрисовки мы проверяем, установив свойство

debugRepaintRainbowEnabled = true;

при старте нашего приложения (в функции main). Стоит отметить, что применение RepaintBoundary тоже имеет свою цену, выраженную в определенных ресурсах приложения для создания и работы нового слоя отрисовки, поэтому данный виджет также следует применять с осмотрительностью.

Ну вот и все, надеюсь наше путешествие в мир анимаций Flutter вам понравилось. Понимая различные типы анимаций, зная, когда их использовать, а также научившись управлять и оптимизировать их, вы можете создавать захватывающие анимации, делающие ваши приложения на Flutter по-настоящему выдающимися. Так что дерзайте, экспериментируйте с разными анимациями и позвольте вашей креативности развернуться. Удачного кодинга!

PS все проект с полными примерами кода смотрите в моем репозитории на GitHub.

Автор: Иван Левковский