Реализовываем CircularRevealAnimation на Flutter и попутно публикуем библиотеку на pub.dev +14


В Android есть очень интересная возможность анимации View, которая называется CircularRevealAnimation — дословно "круговое раскрытие". Flutter, в свою очередь, хотя и обладает богатыми возможностями для анимирования виджетов, не предоставляет такую анимацию из коробки.



В этой статье будет показано как реализовать такую анимацию средствами Flutter и опубликовать библиотеку на pub.dev для удобного доступа и распространения.


Реализация анимации


В Flutter всё — это виджет. И анимация не является исключением. Поэтому создадим класс CircularRevealAnimation, который будет расширять класс StatelessWidget.


Запуск, остановка и прочее управление анимацией осуществляется с помощью AnimationController. Для создания AnimationController нужно наследоваться от StatefulWidget и добавить к State специальный класс SingleTickerProviderStateMixin.


Наш класс анимации CircularRevealAnimation не будет самостоятельно заниматься управлением анимацией, а будет получать Animation<double> в качестве обязательного параметра конструктора, поэтому нет необходимости наследоваться от StatefulWidget. Это сделано для того, чтобы CircularRevealAnimation можно было легко совмещать с другими анимациями, использующими тот же AnimationController. Например, совместить анимацию раскрытия с анимацией изменения прозрачности.


Другой важный параметр конструктора CircularRevealAnimation — это child, который является дочерним виджетом нашей анимации, и который будет появляться или исчезать. Вообще, в Flutter очень много виджетов имеют параметр child. Такие виджеты позволяют изменить поведение, отрисовку или расположение дочернего виджета. Или же добавить анимацию, как это происходит с CircularRevealAnimation.


Кроме того, для задания анимации потребуются такие параметры, как центр раскрытия (или закрытия) анимации, а также минимальный и максимальный радиусы раскрытия. Эти параметры не являются обязательными и могут быть указаны как null или вовсе не указаны при создании анимации. В этом случае будут использоваться значения по умолчанию: центр раскрытия будет находиться в центре виджета, минимальный радиус будет ранен нулю, а максимальный радиус будет равен расстоянию от центра раскрытия до той вершины виджета, которая наиболее удалена от центра раскрытия.


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


static double calcMaxRadius(Size size, Offset center) {
  final w = max(center.dx, size.width - center.dx);
  final h = max(center.dy, size.height - center.dy);
  return sqrt(w * w + h * h);
}

Теперь нужно реализовать обрезку виджета в пределах круга во время отрисовки. В этом нам поможет класс ClipPath, позволяющий обрезать виджет по произвольному шаблону. В качестве параметров этому виджету передаются clipper (о нём чуть позже) и child — дочерний виджет, который нужно обрезать.


Параметр clipper виджета ClipPath определяет то, как будет обрезан дочерний виджет. Для создания собственного шаблона обрезки создадим класс CircularRevealClipper, наследующий класс CustomClipper<Path> и переопределим метод Path getClip(Size size). Этот метод возвращает Path, ограничивающий область обрезки. В нашем случае эта область — окружность с заданным центром. Для вычисления радиуса окружности нужно знать текущее значение анимации. Это значение передается в CircularRevealClipper в качестве параметра fraction. Вычисление радиуса окружности осуществляется с помощью линейной интерполяции между минимальным и максимальным радиусами.


После этого перейдем к реализации виджета. Для создания анимации удобно использовать AnimatedBuilder. Конструктор AnimatedBuilder принимает объект Animation<double> и builder, используемый для построения виджетов с учетом текущего значения анимации. В builder мы создаем ClipPath и передаем текущее значение анимации (fraction) в CircularRevealClipper.


class CircularRevealAnimation extends StatelessWidget {
  ...

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: animation,
      builder: (BuildContext context, Widget _) {
        return ClipPath(
          clipper: CircularRevealClipper(
            fraction: animation.value,
            center: center,
            minRadius: minRadius,
            maxRadius: maxRadius,
          ),
          child: this.child,
        );
      },
    );
  }
}

На этом создание CircularRevealAnimation завершено. Осталось использовать его. Для этого нужно создать StatefulWidget, AnimationController и передать AnimationController в CircularRevealAnimation.


Пример использования
import 'package:flutter/material.dart';
import 'package:circular_reveal_animation/circular_reveal_animation.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'CRA Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage>
    with SingleTickerProviderStateMixin {
  AnimationController animationController;
  Animation<double> animation;

  @override
  void initState() {
    super.initState();
    animationController = AnimationController(
      vsync: this,
      duration: Duration(milliseconds: 1000),
    );
    animation = CurvedAnimation(
      parent: animationController,
      curve: Curves.easeIn,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("CRA Demo"),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: CircularRevealAnimation(
          minRadius: 12,
          maxRadius: 200,
          center: Offset(0, 300),
          child: Container(color: Colors.red),
          animation: animation,
        ),
      ),
      floatingActionButton: FloatingActionButton(onPressed: () {
        if (animationController.status == AnimationStatus.forward ||
            animationController.status == AnimationStatus.completed) {
          animationController.reverse();
        } else {
          animationController.forward();
        }
      }),
    );
  }
}

Демо-приложение на github.


Создание библиотеки


Для создания Dart или Flutter библиотеки нужно добавить файл pubspec.yaml в ту же директорию, в которой находится директория lib с Dart-файлами. Этот файл содержит описание библиотеки, информацию об авторах и зависимостях.


Также хорошей практикой является создание файла, определяющего публичный API. Этот файл находится в папке lib и включает название библиотеки и список файлов, которые нужно включить в публичный API. Все остальные Dart-файлы помещаются в директорию src. Это не только скрывает файлы, не включенные в публичный API, но и позволяет импортировать библиотеку с помощью единственного import выражения. Содержимое данного файла:


library circular_reveal_animation;

export 'package:circular_reveal_animation/src/circular_reveal_animation.dart';

Более подробно о создании библиотек на Dart можно почитать тут.


Публикация на pub.dev


Публикация Dart библиотеки на pub.dev это очень просто. Все, что нужно сделать — это запустить команду flutter packages pub publish из корневой директории библиотеки. Публикация осуществляется от имени аккаунта Google, поэтому в процессе публикации будет дана ссылка, которую надо открыть в браузере и авторизоваться в Google. Впоследствии публиковать обновления можно будет только с использованием того аккаунта, от имени которого была выложена первая версия.


Перед публикацией рекомендуется проверить корректность библиотеки с помощью команды flutter packages pub publish --dry-run.


После выполнения flutter packages pub publish библиотека сразу станет доступна на pub.dev. И, как написано в документации, "Publishing is forever" — в последствии Вы сможете только выкладывать новые версии. Старые же версии будут также доступны.


Хотя публикация библиотек выглядит просто, у нее тоже могут быть подводные камни. Например, при публикации первой версии мне сняли несколько очков в рейтинге потому что описание библиотеки (в pubspec.yaml) было слишком коротким.


Более подробно о публикации библиотек можно почитать тут.


Собственно, библиотека circular_reveal_animation на pub.dev и github.com.


P. S.: Я использовал ```java {...} ```, чтобы подсветить код на Dart. Неплохо было бы добавить подсветку кода на Dart на habr.com.




К сожалению, не доступен сервер mySQL