флип-анимация / Блог компании OTUS / Хабр

0 Favorite

[

Перевод подготовлен в рамках курса Flutter Mobile Developer.

Всех желающих приглашаем на второй день двухдневного онлайн-интенсива «Создаем приложение на Flutter для Web, iOS и Android». Продолжаем писать приложение (это будет приложение с работой по сети: загрузка списка сущностей + их фильтрация + сделаем блок настроек в приложении, где можно будет менять тему приложения (цвета)). Присоединяйтесь!


Когда я впервые увидел виджет AnimationSwitcher, то подумал, что смогу его перевернуть, открыв его обратную сторону.

Я ошибался: AnimationSwitcher позволяет… переключаться между различными виджетами с заданной вами анимацией (анимация по умолчанию – затухающий переход). Этот компонент слишком универсальный для этой цели.

Я должен был внимательно читать…
Я должен был внимательно читать…

Его использование является весьма общим, поэтому я покажу вам, как можно сделать такую анимацию.

Я обнаружил пакет flutter, который может делать такую флип-анимацию, под названием animated_card_switcher, но, похоже, он не поддерживается должным образом, и код слишком сложен.

Вот шаги по разработке:

  • Создайте передний и задний виджеты

  • Используйте виджет AnimationSwitcher для анимации

  • Создайте свой собственный конструктор переходов для поворота вашей карточки.

  • Добавить кривые

Создание переднего и заднего виджетов

В этом примере я возьму упрощенные версии переднего и заднего виджетов, потому что это не так существенно.

Единственное, о чем следует помнить, это то, что вы должны установить ключ для виджетов верхнего уровня, чтобы AnimationSwitcher обнаружил, что виджет изменился (и, следовательно, выполнил анимацию).

Как кусок пирога!
Как кусок пирога!

Вот пример макета виджетов, который я буду использовать:

Widget __buildLayout({Key key, String faceName, Color backgroundColor}) {
  return Container(
    key: key,
    decoration: BoxDecoration(
      shape: BoxShape.rectangle,
      borderRadius: BorderRadius.circular(20.0),
      color: backgroundColor,
    ),
    child: Center(
      child: Text(faceName.substring(0, 1), style: TextStyle(fontSize: 80.0)),
    ),
  );

Итак, мои виджеты будут иметь вид спереди и сзади:

Widget _buildFront() {
  return __buildLayout(
    key: ValueKey(true),
    backgroundColor: Colors.blue,
    faceName: "F",
  );
}

Widget _buildRear() {
  return __buildLayout(
    key: ValueKey(false),
    backgroundColor: Colors.blue.shade700,
    faceName: "R",
  );
}

Использование виджета AnimationSwitcher для анимирования

Теперь мы можем использовать виджет AnimationSwitcher для анимирования перехода между передним и задним сторонами.

В StatefulWidget я переопределяю метод build, чтобы создать страницу, которая будет показывать анимацию в ее центре.

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  bool _displayFront;
  bool _flipXAxis;

  @override
  void initState() {
    super.initState();
    _displayFront = true;
    _flipXAxis = true;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(this.widget.title),
        centerTitle: true,
      ),
      body: Center(
        child: Container(
          constraints: BoxConstraints.tight(Size.square(200.0)),
          child: _buildFlipAnimation(),
      ),
    );
  }
}

Я отделил анимацию от страницы в методе _build Flip Animation, чтобы сделать код более понятным.

Вот первая версия этого метода:

Widget _buildFlipAnimation() {
  return GestureDetector(
    onTap: () => setState(() =>_showFrontSide = !_showFrontSide),
    child: AnimatedSwitcher(
      duration: Duration(milliseconds: 600),
      child: _showFrontSide ? _buildFront() : _buildRear(),
    ),
  );
}

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

По крайней мере, что-то происходит.
По крайней мере, что-то происходит.

Мы хотим повернуть виджеты по оси Y. Надеюсь, AnimationSwitcher позволит нам переопределить переход, благодаря входному transitionBuilder.

Закодируйте свой пользовательский конструктор переходов для вращения вашей карточки

Итак, вот план: мы хотим получить поворот на 180° (pi). Мы обернем наши виджеты в AnimatedBuidler и воспользуемся виджетом Transform для применения вращения.

Widget __transitionBuilder(Widget widget, Animation<double> animation) {
  final rotateAnim = Tween(begin: pi, end: 0.0).animate(animation);
  return AnimatedBuilder(
    animation: rotateAnim,
    child: widget,
    builder: (context, widget) {
      return Transform(
        transform: Matrix4.rotationY(value),
        child: widget,
        alignment: Alignment.center,
      );
    },
  );
}

Это хороший старт, но не совсем то, что необходимо. Мы видим, что при анимации перехода задний виджет находится сверху от начала до конца.

Финальная часть появляется слишком быстро.
Финальная часть появляется слишком быстро.

Нам нужно, чтобы передний виджет постепенно заменялся задним.

Поэтому необходимо изменить две вещи:

  • Порядок отображения должен быть обратным: заменяемый виджет должен быть на вершине стека.

  • В середине анимации заменяемый виджет должен исчезнуть.

Для этого мы изменим вход layoutBuilder нашего экземпляра AnimationSwitcher.

layoutBuilder: (widget, list) => Stack(children: [widget, ...list]),

Затем, в середине анимации, в результате поворота на pi/2 ширина виджета станет равной 0.0. Поэтому мы заблокируем это вращение (только) для анимации предыдущего виджета.

Widget __transitionBuilder(Widget widget, Animation<double> animation) {
  final rotateAnim = Tween(begin: pi, end: 0.0).animate(animation);
  return AnimatedBuilder(
    animation: rotateAnim,
    child: widget,
    builder: (context, widget) {
      final isUnder = (ValueKey(_showFrontSide) != widget.key);
      final value = isUnder ? min(rotateAnim.value, pi / 2) : rotateAnim.value;
      return Transform(
        transform: Matrix4.rotationY(value),
        child: widget,
        alignment: Alignment.center,
      );
    },
  );
}

Уже лучше, но мы еще не закончили! Чтобы усилить ощущение того, что виджет вращается, мы добавим небольшой “tilt” (наклон) на виджеты.

Нам нужна глубина... Милая маленькая пушистая глубина.
Нам нужна глубина… Милая маленькая пушистая глубина.

Это значение “наклона” должно быть равно 0,0 в начале и в конце анимации. Кроме того, поскольку мы будем применять анимацию для каждой стороны нашего виджета, то их наклон должен быть противоположным. Например, если наклон переднего виджета равен 0,2, то наклон заднего должен быть -0,2.

Чтобы применить наклон к виджету, мы вручную задаем одно конкретное значение объекту Matrix4, определяющему поворот.

Widget __transitionBuilder(Widget widget, Animation<double> animation) {
  final rotateAnim = Tween(begin: pi, end: 0.0).animate(animation);
  return AnimatedBuilder(
    animation: rotateAnim,
    child: widget,
    builder: (context, widget) {
      final isUnder = (ValueKey(_showFrontSide) != widget.key);
      var tilt = ((animation.value - 0.5).abs() - 0.5) * 0.003;
      tilt *= isUnder ? -1.0 : 1.0;
      final value = isUnder ? min(rotateAnim.value, pi / 2) : rotateAnim.value;
      return Transform(
        transform: Matrix4.rotationY(value)..setEntry(3, 0, tilt),
        child: widget,
        alignment: Alignment.center,
      );
    },
  );
}

Более подробную информацию о Matrix4 можно получить здесь: https://medium.com/flutter-community/advanced-flutter-matrix4-and-perspective-transformations-a79404a0d828.

Добавить кривые

Наконец, чтобы добавить немного энергии и динамики в анимацию, вы можете изменить параметры кривых входа AnimationSwitcher.

С кривыми всегда лучше!
С кривыми всегда лучше!

Вот моя первая попытка:

Widget _buildFlipAnimation() {
  return GestureDetector(
    onTap: _switchCard,
    child: AnimatedSwitcher(
      duration: Duration(milliseconds: 4600),
      transitionBuilder: __transitionBuilder,
      layoutBuilder: (widget, list) => Stack(children: [widget, ...list]),
      child: _showFrontSide ? _buildFront() : _buildRear(),
      switchInCurve: Curves.easeInBack,
      switchOutCurve: Curves.easeOutBack,
    ),
  );
}

Я должен показать вам в замедленном режиме в чем проблема.

О Нееееееет….
О Нееееееет….

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

Анимация в режиме нескольких кадров…
Анимация в режиме нескольких кадров…

Чтобы избежать этого, мы должны использовать свойство flipped для кривой, которую мы хотим использовать.

switchInCurve: Curves.easeInBack,
switchOutCurve: Curves.easeInBack.flipped,
Sloooow Mooooo (теперь идеально)
Sloooow Mooooo (теперь идеально)

Заключение

Как видите, ничего особенного: я сделал эту анимацию примерно из 30 строк кода (только анимация), используя только один атрибут (отображаемая сторона виджета).

Я не думаю, что имеет смысл создавать пакет для этого. Добавление зависимости в ваш код означает, что если он не будет работать при обновлении версии (например), ваш проект застопорится на определенное время. Хуже того, если зависимость больше не поддерживается, вы не сможете гарантировать, что ваш проект будет правильно компилироваться в следующие 6 месяцев, 1 или 2 года…

Копируйте и вставляйте этот пример с умом :)
Копируйте и вставляйте этот пример с умом 🙂

Так что не стесняйтесь использовать мой код. Я не поклонник копипаста, поэтому точнее выразиться (не стесняйтесь использовать код) “соответствующий моему коду”, то есть скопируйте-вставьте его, поймите и измените в соответствии с вашими потребностями!

Демонстрацию, использованную в этой статье, вы найдете здесь:

https://github.com/GONZALEZD/flutter_demos/tree/main/flip_animation

Следите за сообществом Flutter в Twitter


Узнать подробнее о курсе “Flutter Mobile Developer

Участвовать в двухдневном онлайн-интенсиве «Создаем приложение на Flutter для Web, iOS и Android»





Перейти в источник

Похожие статьи

О классах Program и Startup — инициализация ASP.NET приложения. Часть II: IWebHostBuilder и Startup / Хабр

0 Favorite [ Введение Это – продолжение статьи, первая часть которой была опубликована ранее. В той части был рассмотрен процесс инициализации, общий для любого приложения…

Микрофронтенды и виджеты в 2021-м. Доклад Яндекса / Блог компании Яндекс / Хабр

0 Favorite [ Давайте поговорим о микрофронтендах и о встраиваемых виджетах, которые, по сути, были предшественниками концепции микрофронтендов. В докладе я рассказал о способах встраивать…

Углубленный анализ тестирования виджетов во Flutter. Часть II. Классы Finder и WidgetTester

0 Favorite [ Перевод материала подготовлен в рамках онлайн-курса “Flutter Mobile Developer“. Приглашаем также всех желающих на бесплатный двухдневный интенсив «Создаем приложение на Flutter для…

Ответы