ReactJS, TypeScript, Micro FrontendsReact fwdays | 27 березня. Долучайся!
×Закрыть

Виды анимаций во Flutter и их реализация

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Всем доброго времени суток, меня зовут Андрей, я являюсь Flutter-разработчиком. В прошлой статье, мы с вами рассмотрели создание объекта при помощи canvas и его анимирование. Поэтому в этот раз, я решил детальнее рассказать о различных видах анимации во flutter, их принципах работы и реализации.

Для начала, задавались ли вы когда-то вопросом, а зачем в целом нужна анимация в мобильном приложение? Это ведь не является, необходимой деталью в приложении, тогда зачем тратить на это время? Вряд-ли пользователь обратит внимание, на то как какой-то объект плавно исчезает. Однако дело в том что хоть пользователь может и не заметить плавное понижение прозрачности, но он точно обратит внимание, на то если это произойдет резко. Вы можете сами сравнить это на небольшом примере и сделать для себя выводы.

У хорошей анимации есть 2-е основные функции:

  1. Улучшение UI(user interface) путем повышение плавности функционирования приложения.
  2. На самом деле, анимирование не только улучшает UI, но занимаеться тем что учит пользователя пользоваться нашим приложением. Именно благодаря анимации можно дать понять пользователю то, что произошло какое-либо действие, например о том что пользователь нажал кнопку. Это мелочь на которую никогда не обратишь внимания, но сталкиваешься с ней каждый день.

Сам flutter, в плане анимации и плавность очень хорош. Главными его преимуществами в плане анимации является, огромное количество заранее готовых виджетов, которые легко позволяют работать с анимацией и его работа с анимациями под «капотом».

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

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

Как я и упоминал ранее, в flutter, огромное количество заранее готовых виджетов для создания анимация. На самом деле их настолько много, что для большинства задач связанных с анимацией их вполне хватает.

Так как рассмотреть каждый виджет мы не сможем, остановимся на нескольких самых часто используемых и самых интересных. Однако ознакомиться с каждым вы сможете на официальном сайте flutter в каталоге виджетов для анимации.

И так, для начала давайте детально разберем виджет AnimatedOpacity. Как и следует из названия, данный виджет отвечает за прозрачность объекта, который будет передан в параметр child. По сути является анимированным аналогом виджета Opactity. Почти все анимированные виджеты имеют обязательный параметр, duration, который отвечает за время перехода от изначального состояния до конечного. Интересным является то что если к примеру во время перехода из стартового состояния в конечное, которое длиться 4 секунды, прошла 1 секунда и мы опять изменим состояния на стартовое, то время обратной анимации будет занимать 1 секунду, а не 4.

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(title: Text('Animation example')),
     body: Center(
       child: AnimatedOpacity(
         duration: Duration(milliseconds: 500),
         opacity: isView ? 0.0 : 1.0,
         child: InkWell(
           onTap: () => setState(() => isView = !isView),
           child: Container(
             width: 200.0,
             height: 200.0,
             color: Colors.green,
           ),
         ),
       ),
     ),
   );
 }
}

Следующим важным параметром является curve. Это кривая, которая отвечает, за то как будут изменяться параметр, в нашем случае opactity, во время выполнения анимаций.

Используются для регулировки скорости изменения анимации с течением времени, позволяя им ускоряться и замедляться, а не двигаться с постоянной скоростью. Чтобы использовать кривую, вы можете выбрать одну из многих заранее готовых, которые находяться в классе Curves. По умолчанию используется Curves.linear, тоесть просто прямая линейная анимация. Ознакомиться с полным списком кривых вы можете на официальном сайте flutter.

@override
Widget build(BuildContext context) {
 return Scaffold(
   appBar: AppBar(title: Text('Animation example')),
   body: Center(
     child: AnimatedOpacity(
       duration: Duration(milliseconds: 2000),
       opacity: isView ? 0.0 : 1.0,
       curve: Curves.bounceIn,
       child: InkWell(
         onTap: () => setState(() => isView = !isView),
         child: Container(
           width: 200.0,
           height: 200.0,
           color: Colors.green,
         ),
       ),
     ),
   ),
 );
}

Вторым вариантом, будет создание собственных curves, однако это очень редко используемый вариант, так-как, нам как правило хватает и стандартных кривых. Для создания собственных кривых мы должны создать новый класс на основе класса Curve.

class _MyAppState extends State<MyApp> {
 bool isView = false;

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(title: Text('Animation example')),
     body: Center(
       child: AnimatedOpacity(
         duration: Duration(milliseconds: 400),
         opacity: isView ? 0.0 : 1.0,
         curve: ExampleCurve(),
         child: GestureDetector(
           onTap: () => setState(() => isView = !isView),
           child: Container(
             width: 200.0,
             height: 200.0,
             color: Colors.green,
           ),
         ),
       ),
     ),
   );
 }
}

class ExampleCurve extends Curve {
 final double count;

 ExampleCurve({this.count = 3});

 @override
 double transformInternal(double t) {
   var val = sin(count * 2 * pi * t) * 0.5 + 0.5;
   return val;
 }
}  

Таким образом на примере виджета AnimatedOpacity, мы рассмотрели основные параметры для любой анимации, давайте менее детально рассмотрим еще несколько виджетов.

AnimatedPosition, как и AnimatedOpacity, является анимированной версией другого виджета, а именно Position. Выполняет анимацию, если изменилась позицию одной или нескольких сторон.

@override
Widget build(BuildContext context) {
 return Scaffold(
   appBar: AppBar(title: Text('Animation example')),
   body: Stack(
     alignment: Alignment.centerLeft,
     children: [
       AnimatedPositioned(
         left: position,
         duration: Duration(milliseconds: 400),
         child: Image.network(    'https://cdn.pixabay.com/photo/2012/04/11/17/34/car-29078_640.png',
           width: 200.0,
         ),
       ),
     ],
   ),
   floatingActionButton: FloatingActionButton(
     child: Text('GO!'),
     onPressed: () {
       setState(() {
         position += 40;
       });
     },
   ),
 );
}

Следующим полезным виджетом, будет AnimatedCrossFade, с его помощью можно создать анимированный переход с понижающейся прозрачностью, между двумя виджетами. Он имеет несколько особых параметров, а именно firstChild и secondChild, что соответственно представляют из себя первый виджет и второй. Для их изменения мы должны изменять, параметр crossFadeState, указывая какой виджет мы должны отображать. Рассмотрим небольшой пример:

@override
Widget build(BuildContext context) {
 return Scaffold(
   appBar: AppBar(title: Text('Animation example')),
   body: Center(
     child: SizedBox(
       width: 200.0,
       height: 200.0,
       child: InkWell(
         onTap: () => setState(() => isChangeWidget = !isChangeWidget),
         child: AnimatedCrossFade(
           crossFadeState: isChangeWidget ? CrossFadeState.showFirst : CrossFadeState.showSecond,
           duration: const Duration(seconds: 2),
           firstChild: Container(color: Colors.green),
           secondChild: Container(color: Colors.red),
         ),
       ),
     ),
   ),
 );
}

При нажатии выбранный виджет будет меняться в зависимости от переменной isChangeWidget.

AnimateContainer, очень простой для понимания виджет, представляет из себя аналог обычного виджета Container, с разницей в том, что почти каждый его параметр анимирован. В случае, если у вас есть Container, который имеет изменяемый параметр, к примеру, цвет или размер, рекомендую заменить его на AnimatedContainer, по логике, ничего не измениться, зато для пользователь все будет происходить плавнее.

Рассмотрим пример:

bool startAnimation = false;
@override
Widget build(BuildContext context) {
 return Scaffold(
   appBar: AppBar(title: Text('Animation example')),
   body: Center(
     child: InkWell(
       onTap: () => setState(() => startAnimation = !startAnimation),
       child: AnimatedContainer(
         decoration: BoxDecoration(
           color: startAnimation ? Colors.lightGreen : Colors.red,
           borderRadius: BorderRadius.circular(startAnimation ? 15.0 : 0.0),
         ),
         width: startAnimation ? 100 : 200,
         height: startAnimation ? 100 : 200,
         curve: Curves.easeInOutCubic,
         duration: Duration(seconds: 1),
       ),
     ),
   ),
 );
}

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

Нет конечно можно решить эту проблема исспользуя несколько переменных и множество виджетов или же иные «костыли», однако наилучшим и наипростейшим решением будет использование AnimationController и виджет Animation. Однако для начала наш класс мы должны создать с помощью SingleTickerProviderStateMixin.

Он необходим для создания класса, в котором используется только один AnimationController. При инициализацию, которую необходимо делать в initState, в параметр vsync, мы передает текущий объект, this. Этот миксин поддерживает только один тикер. Если за время существования состояния у вас может быть несколько объектов AnimationController, используйте вместо этого TickerProviderStateMixin.

В flutter анимационный объект ничего не знает о том, что находится на экране. Анимация — это абстрактный класс, который понимает своё текущее значение и свое состояние. Одним из наиболее часто используемых типов анимации является Animation.

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

class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
 AnimationController _animationController;

 @override
 void initState() {
   _animationController = AnimationController(duration: Duration(milliseconds: 1000), vsync: this);
   _animationController.addListener(() => setState(() {}));
   super.initState();
 }

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(title: Text('Animation example')),
     body: InkWell(
       onTap: () => _animationController.forward(),
       child: Opacity(
         opacity: 1.0 - _animationController.value,
         child: Container(
           color: Colors.orange,
           height: 200.0,
           width: 200.0 + (200 * _animationController.value),
         ),
       ),
     ),
   );
 }
}

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

_animationController.addListener(() => setState(() {}));

Это необходимо, для того чтобы обновлять состояния, по ходу выполнения анимации. Для запуска анимации, при нажатии на кнопку вызывается метод forward:

onTap: () => _animationController.forward(),

Это позволяет запустить анимацию, для запуска в обратную сторону, используется метод reverse:

onTap: () => _animationController.reverse(),

По умолчанию объект AnimationController находится в диапазоне от 0.0 до 1.0. Если Вам нужен другой диапазон или другой тип данных, Вы можете использовать Tween для настройки анимации для интерполяции на другой диапазон или другой тип данных.

Например, следующий Tween переходит от −200.0 к 0.0:

tween = Tween<double>(begin: -200, end: 0);

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

Сначала мы создали контроллер и анимацию, в методе initState, мы инициализируем контролер. Далее на основе ColorTween, которому мы задали изначальное значение синий, а конечный результат красный, мы инициализируем animate и связываем его с контроллером. Далее просто создаем слушателя, который будет обновлять состояния, каждый раз при изменение состояния.

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

class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
 AnimationController _animationController;
 Animation<Color> _animation;

 @override
 void initState() {
   _animationController = AnimationController(duration: Duration(milliseconds: 400), vsync: this);
   _animation = ColorTween(begin: Colors.blue, end: Colors.red).animate(_animationController);
   _animationController.addListener(() => setState(() {}));
   super.initState();
 }

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(title: Text('Animation example')),
     body: Center(
       child: InkWell(
         onTap: () => _animationController.repeat(),
         child: Container(
           width: 200.0,
           height: 200.0,
           color: _animation.value,
         ),
       ),
     ),
   );
 }
}

Давайте под конец попробуем используя несколько контролеров, изменить размер виджета, и его цвет. При этом изменение размера, должно произойти после изменения цвета. Для этого нам потребуется использовать TickerProviderStateMixin, а не SIngleTickerProviderStateMixin, как ранее. Также нам придется использовать новый вид слушателя, а именно addStatusListener, который будет вызываться только при изменение состояние, нас интересует чтобы при конце первой анимации, был вызван второй контроллер.

class _MyAppState extends State<MyApp> with TickerProviderStateMixin {
 AnimationController _colorController;
 AnimationController _sizeController;

 Animation<Color> _colorAnimation;
 Animation<double> _sizeAnimation;

 @override
 void initState() {
   _colorController = AnimationController(duration: Duration(milliseconds: 1000), vsync: this);
   _sizeController = AnimationController(duration: Duration(milliseconds: 1000), vsync: this);

   _colorAnimation = ColorTween(begin: Colors.blue, end: Colors.red).animate(_colorController);
   _sizeAnimation = Tween<double>(begin: 200.0, end: 300.0).animate(_sizeController);

   _colorController.addListener(() => setState(() {}));
   _sizeController.addListener(() => setState(() {}));

   _colorController.addStatusListener((status) {
     if (status == AnimationStatus.completed) _sizeController.forward();
   });
   super.initState();
 }

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(title: Text('Animation example')),
     body: Center(
       child: InkWell(
         onTap: () => _colorController.forward(),
         child: Container(
           color: _colorAnimation.value,
           height: _sizeAnimation.value,
           width: _sizeAnimation.value,
         ),
       ),
     ),
   );
 }
}

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

👍НравитсяПонравилось0
В избранноеВ избранном0
Подписаться на автора
LinkedIn

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

Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

у вас там 23й, 24й и 25й кадр занят какойто черной херней

Да, извиняюсь, проблема при записи в эмулятор, в следующей статье, постараюсь исправить.

ТС издевается? А понадеялся увидеть отображение разных красивых графиков да еще и с кодом на 10 строчек. Как же я обманулся.

О, еще один джун из AppVesto c недостатьей)

Я так розумію, нам слід очікувати непервершену по актуальності та фактажу статтю про збір грибів у лісі... ;))

Нет, это не соответствует тематике данного сайта

Оффтоп: ох и безграммотно написано!

Есть ошибки, в следующей статье постраюсь исправить.

Не стоит анимировать через setState в addListener, для этого есть AnimatedBuilder, который не будет ребилдить всё дерево виджета с каждым тиком.

Действительно, моей ошибкой было не исспользовать AnimatedBuilder, в данной статье. Благодарю за ваш коментарий.

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

Благодарю за хороший отзыв.

Подписаться на комментарии