Анимация приложений с помощью Flutter

Опубликовано: 2022-03-10
Краткое резюме ↬ Flutter обеспечивает отличную поддержку анимации для кроссплатформенных приложений. В этой статье рассматривается новый нетрадиционный и более простой способ добавления анимации в приложения. Что представляют собой эти новые виджеты «Анимация и движение» и как мы можем использовать их для добавления простых и сложных анимаций?

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

Flutter, кроссплатформенный фреймворк, за последние два года стал более зрелым и теперь включает поддержку веб-приложений и настольных компьютеров. Он заработал репутацию благодаря тому, что приложения, разработанные с его помощью, гладкие и красивые. Благодаря богатой поддержке анимации, декларативному способу написания пользовательского интерфейса, «горячей перезагрузке» и другим функциям теперь он представляет собой полноценную кроссплатформенную структуру.

Если вы начинаете работать с Flutter и хотите изучить нетрадиционный способ добавления анимации, то вы попали в нужное место: мы исследуем область анимации и виджетов движения, неявный способ добавления анимации.

Flutter основан на концепции виджетов. Каждый визуальный компонент приложения представляет собой виджет — думайте о них как о представлениях в Android. Flutter обеспечивает поддержку анимации с помощью класса Animation, объекта «AnimationController» для управления и «Tween» для интерполяции диапазона данных. Эти три компонента работают вместе, чтобы обеспечить плавную анимацию. Поскольку это требует ручного создания и управления анимацией, он известен как явный способ анимации.

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

Иерархия анимации во Flutter. (Большой превью)

На приведенной выше диаграмме примерно показана иерархия анимации во Flutter, как поддерживается как явная, так и неявная анимация.

Вот некоторые из анимированных виджетов, описанных в этой статье:

  • AnimatedOpacity
  • AnimatedCrossFade
  • AnimatedAlign
  • AnimatedPadding
  • AnimatedSize
  • AnimatedPositioned .

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

Позвольте мне объяснить эти общие свойства сейчас, так как они будут использоваться позже во всех примерах.

  • duration
    Длительность анимации параметров.
  • reverseDuration
    Продолжительность обратной анимации.
  • curve
    Кривая, применяемая при анимации параметров. Интерполированные значения могут быть взяты из линейного распределения или, если это указано, могут быть взяты из кривой.

Давайте начнем путешествие с создания простого приложения, которое мы назовем «Quoted». Он будет отображать случайную цитату каждый раз, когда приложение запускается. Следует отметить две вещи: во-первых, все эти цитаты будут жестко закодированы в приложении; и во-вторых, никакие пользовательские данные не будут сохранены.

Примечание . Все файлы для этих примеров можно найти на GitHub.

Еще после прыжка! Продолжить чтение ниже ↓

Начиная

Flutter должен быть установлен, и вам потребуется некоторое знакомство с основным потоком, прежде чем двигаться дальше. Хорошее место для начала — «Использование Flutter от Google для действительно кроссплатформенной мобильной разработки».

Создайте новый проект Flutter в Android Studio.

Новое меню проекта флаттера в Android Studio. (Большой превью)

Откроется мастер создания нового проекта, в котором можно настроить основные параметры проекта.

Экран выбора типа проекта Flutter. (Большой превью)

На экране выбора типа проекта представлены различные типы проектов Flutter, каждый из которых соответствует определенному сценарию. Для этого руководства выберите Приложение Flutter и нажмите « Далее ».

Теперь вам нужно ввести некоторую информацию о проекте: имя и путь проекта, домен компании и т. д. Посмотрите на изображение ниже.

Экран конфигурации приложения Flutter. (Большой превью)

Добавьте имя проекта, путь к Flutter SDK, местоположение проекта и необязательное описание проекта. Нажмите Далее .

Экран имени пакета приложения Flutter. (Большой превью)

Каждое приложение (будь то Android или iOS) требует уникального имени пакета. Как правило, вы используете обратный домен вашего веб-сайта; например, com.google или com.yahoo. Нажмите Finish , чтобы создать работающее приложение Flutter.

Сгенерированный образец проекта. (Большой превью)

После создания проекта вы должны увидеть экран, показанный выше. Откройте файл main.dart (выделен на скриншоте). Это основной файл приложения. Пример проекта завершен сам по себе и может быть запущен непосредственно на эмуляторе или физическом устройстве без каких-либо изменений.

Замените содержимое файла main.dart следующим фрагментом кода:

 import 'package:animated_widgets/FirstPage.dart'; import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Animated Widgets', debugShowCheckedModeBanner: false, theme: ThemeData( primarySwatch: Colors.blue, accentColor: Colors.redAccent, ), home: FirstPage(), ); } }

Этот код очищает файл main.dart , просто добавляя простую информацию, относящуюся к созданию нового приложения. Класс MyApp возвращает объект: виджет MaterialApp , который обеспечивает базовую структуру для создания приложений, соответствующих Material Design. Чтобы сделать код более структурированным, создайте два новых файла dart внутри папки lib : FirstPage.dart и Quotes.dart .

Файл FirstPage.dart. (Большой превью)

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

Примечание . Далее в этой статье все фрагменты кода для каждого анимированного виджета добавляются в этот файл как дочерние элементы виджета Scaffold. Для получения дополнительной информации может быть полезен этот пример на GitHub.

Начните с добавления следующего кода в FirstPage.dart . Это частичный код, в который позже будут добавлены другие вещи.

 import 'dart:math'; import 'package:animated_widgets/Quotes.dart'; import 'package:flutter/material.dart'; class FirstPage extends StatefulWidget { @override State createState() { return FirstPageState(); } } class FirstPageState extends State with TickerProviderStateMixin { bool showNextButton = false; bool showNameLabel = false; bool alignTop = false; bool increaseLeftPadding = false; bool showGreetings = false; bool showQuoteCard = false; String name = ''; double screenWidth; double screenHeight; String quote; @override void initState() { super.initState(); Random random = new Random(); int quoteIndex = random.nextInt(Quotes.quotesArray.length); quote = Quotes.quotesArray[quoteIndex]; } @override Widget build(BuildContext context) { screenWidth = MediaQuery.of(context).size.width; screenHeight = MediaQuery.of(context).size.height; return Scaffold( appBar: _getAppBar(), body: Stack( children: [ // All other children will be added here. // In this article, all the children widgets are contained // in their own separate methods. // Just method calls should be added here for the respective child. ], ), ); } } import 'dart:math'; import 'package:animated_widgets/Quotes.dart'; import 'package:flutter/material.dart'; class FirstPage extends StatefulWidget { @override State createState() { return FirstPageState(); } } class FirstPageState extends State with TickerProviderStateMixin { bool showNextButton = false; bool showNameLabel = false; bool alignTop = false; bool increaseLeftPadding = false; bool showGreetings = false; bool showQuoteCard = false; String name = ''; double screenWidth; double screenHeight; String quote; @override void initState() { super.initState(); Random random = new Random(); int quoteIndex = random.nextInt(Quotes.quotesArray.length); quote = Quotes.quotesArray[quoteIndex]; } @override Widget build(BuildContext context) { screenWidth = MediaQuery.of(context).size.width; screenHeight = MediaQuery.of(context).size.height; return Scaffold( appBar: _getAppBar(), body: Stack( children: [ // All other children will be added here. // In this article, all the children widgets are contained // in their own separate methods. // Just method calls should be added here for the respective child. ], ), ); } } import 'dart:math'; import 'package:animated_widgets/Quotes.dart'; import 'package:flutter/material.dart'; class FirstPage extends StatefulWidget { @override State createState() { return FirstPageState(); } } class FirstPageState extends State with TickerProviderStateMixin { bool showNextButton = false; bool showNameLabel = false; bool alignTop = false; bool increaseLeftPadding = false; bool showGreetings = false; bool showQuoteCard = false; String name = ''; double screenWidth; double screenHeight; String quote; @override void initState() { super.initState(); Random random = new Random(); int quoteIndex = random.nextInt(Quotes.quotesArray.length); quote = Quotes.quotesArray[quoteIndex]; } @override Widget build(BuildContext context) { screenWidth = MediaQuery.of(context).size.width; screenHeight = MediaQuery.of(context).size.height; return Scaffold( appBar: _getAppBar(), body: Stack( children: [ // All other children will be added here. // In this article, all the children widgets are contained // in their own separate methods. // Just method calls should be added here for the respective child. ], ), ); } } import 'dart:math'; import 'package:animated_widgets/Quotes.dart'; import 'package:flutter/material.dart'; class FirstPage extends StatefulWidget { @override State createState() { return FirstPageState(); } } class FirstPageState extends State with TickerProviderStateMixin { bool showNextButton = false; bool showNameLabel = false; bool alignTop = false; bool increaseLeftPadding = false; bool showGreetings = false; bool showQuoteCard = false; String name = ''; double screenWidth; double screenHeight; String quote; @override void initState() { super.initState(); Random random = new Random(); int quoteIndex = random.nextInt(Quotes.quotesArray.length); quote = Quotes.quotesArray[quoteIndex]; } @override Widget build(BuildContext context) { screenWidth = MediaQuery.of(context).size.width; screenHeight = MediaQuery.of(context).size.height; return Scaffold( appBar: _getAppBar(), body: Stack( children: [ // All other children will be added here. // In this article, all the children widgets are contained // in their own separate methods. // Just method calls should be added here for the respective child. ], ), ); } }
Файл Quotes.dart. (Большой превью)

Файл Quotes.dart содержит список всех жестко запрограммированных котировок. Здесь следует отметить, что список является статическим объектом. Это означает, что его можно использовать в других местах без создания нового объекта класса Quotes. Это выбрано по дизайну, так как приведенный выше список действует просто как утилита.

Добавьте в этот файл следующий код:

 class Quotes { static const quotesArray = [ "Good, better, best. Never let it rest. 'Til your good is better and your better is best", "It does not matter how slowly you go as long as you do not stop.", "Only I can change my life. No one can do it for me." ]; }

Каркас проекта готов, так что давайте немного уточним Quoted.

AnimatedOpacity

Чтобы придать приложению индивидуальность, было бы неплохо знать имя пользователя, поэтому давайте запросим его и покажем кнопку «Далее». Пока пользователь не введет свое имя, эта кнопка будет скрыта, и она изящно появится, когда будет указано имя. Нам нужна какая-то анимация видимости для кнопки, но есть ли для этого виджет? Да, есть.

Введите AnimatedOpacity . Этот виджет основывается на виджете «Непрозрачность», добавляя неявную поддержку анимации. Как мы это используем? Помните наш сценарий: нам нужно показать кнопку «Далее» с анимированной видимостью. Мы оборачиваем виджет кнопки внутри виджета AnimatedOpacity , вводим некоторые правильные значения и добавляем условие для запуска анимации — а Flutter справится с остальным.

 _getAnimatedOpacityButton() { return AnimatedOpacity( duration: Duration(seconds: 1), reverseDuration: Duration(seconds: 1), curve: Curves.easeInOut, opacity: showNextButton ? 1 : 0, child: _getButton(), ); }
Анимация непрозрачности следующей кнопки. (Большой превью)

Виджет AnimatedOpacity имеет два обязательных свойства:

  • opacity
    Значение 1 означает полностью видимый; 0 (ноль) означает скрытый. Во время анимации Flutter интерполирует значения между этими двумя крайностями. Вы можете видеть, как ставится условие для изменения видимости, тем самым запуская анимацию.
  • child
    Дочерний виджет, видимость которого будет анимированной.

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

AnimatedCrossFade

У нас есть имя пользователя, но виджет все еще ожидает ввода. На предыдущем шаге, когда пользователь вводит свое имя, мы отображаем кнопку «Далее». Теперь, когда пользователь нажимает кнопку, я хочу перестать принимать ввод и показать введенное имя. Конечно, есть много способов сделать это, но, возможно, мы можем скрыть виджет ввода и показать нередактируемый текстовый виджет. Давайте попробуем это с помощью виджета AnimatedCrossFade .

Этот виджет требует двух дочерних элементов, так как виджет переключается между ними в зависимости от некоторого условия. При использовании этого виджета следует помнить одну важную вещь: оба дочерних элемента должны иметь одинаковую ширину. Если высота отличается, то более высокий виджет обрезается снизу. В этом сценарии в качестве дочерних будут использоваться два виджета: input и label.

 _getAnimatedCrossfade() { return AnimatedCrossFade( duration: Duration(seconds: 1), alignment: Alignment.center, reverseDuration: Duration(seconds: 1), firstChild: _getNameInputWidget(), firstCurve: Curves.easeInOut, secondChild: _getNameLabelWidget(), secondCurve: Curves.easeInOut, crossFadeState: showNameLabel ? CrossFadeState.showSecond : CrossFadeState.showFirst, ); }
Перекрестное затухание между виджетом ввода и виджетом имени. (Большой превью)

Для этого виджета требуется другой набор обязательных параметров:

  • crossFadeState
    Это состояние решает, какой дочерний элемент показать.
  • firstChild
    Определяет первого потомка для этого виджета.
  • secondChild
    Указывает второго дочернего элемента.

AnimatedAlign

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

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

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

 _getAnimatedAlignWidget() { return AnimatedAlign( duration: Duration(seconds: 1), curve: Curves.easeInOut, alignment: alignTop ? Alignment.topLeft : Alignment.center, child: _getAnimatedCrossfade(), ); }
Анимация выравнивания виджета имени. (Большой превью)

У него всего два обязательных свойства:

  • ребенок:
    Дочерний элемент, выравнивание которого будет изменено.
  • выравнивание:
    Требуемое значение выравнивания.

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

AnimatedPadding

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

Виджеты приветствия и имени перекрывают друг друга. (Большой превью)

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

 _getAnimatedPaddingWidget() { return AnimatedPadding( duration: Duration(seconds: 1), curve: Curves.fastOutSlowIn, padding: increaseLeftPadding ? EdgeInsets.only(left: 28.0) : EdgeInsets.only(left: 0), child: _getAnimatedCrossfade(), ); }

Поскольку приведенная выше анимация должна происходить только после завершения предыдущего анимированного выравнивания, нам нужно отложить запуск этой анимации. Ненадолго отвлекшись от темы, это хороший момент, чтобы поговорить о популярном механизме добавления задержки. Flutter предоставляет несколько таких методов, но конструктор Future.delayed — один из более простых, понятных и удобочитаемых подходов. Например, чтобы выполнить часть кода через 1 секунду:

 Future.delayed(Duration(seconds: 1), (){ sum = a + b; // This sum will be calculated after 1 second. print(sum); });

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

 // Showing “Hi” after 1 second - greetings visibility trigger. _showGreetings() { Future.delayed(Duration(seconds: 1), () { setState(() { showGreetings = true; }); }); } // Increasing the padding for name label widget after 1 second - increase padding trigger. _increaseLeftPadding() { Future.delayed(Duration(seconds: 1), () { setState(() { increaseLeftPadding = true; }); }); }
Анимация заполнения виджета имени. (Большой превью)

Этот виджет имеет только два обязательных свойства:

  • child
    Дочерний элемент внутри этого виджета, к которому будет применяться отступ.
  • padding
    Количество места для добавления.

AnimatedSize

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

Этот виджет немного отличается от других, когда речь идет о необходимых параметрах. Нам нужно то, что Flutter называет «Ticker». У Flutter есть метод, позволяющий объектам узнавать всякий раз, когда запускается новое событие кадра. Это можно представить как нечто, что посылает сигнал: «Сделай это сейчас! … Сделай это сейчас! … Сделай это сейчас! …”

Виджету AnimatedSize требуется свойство — vsync — которое принимает поставщика тикера. Самый простой способ получить поставщика тикеров — добавить в класс Mixin . Существует две основные реализации поставщика тикеров: SingleTickerProviderStateMixin , который предоставляет один тикер; и TickerProviderStateMixin , который предоставляет несколько.

Реализация Ticker по умолчанию используется для маркировки кадров анимации. В данном случае используется последний. Подробнее о миксинах.

 // Helper method to create quotes card widget. _getQuoteCardWidget() { return Card( color: Colors.green, elevation: 8.0, child: _getAnimatedSizeWidget(), ); } // Helper method to create animated size widget and set its properties. _getAnimatedSizeWidget() { return AnimatedSize( duration: Duration(seconds: 1), curve: Curves.easeInOut, vsync: this, child: _getQuoteContainer(), ); } // Helper method to create the quotes container widget with different sizes. _getQuoteContainer() { return Container( height: showQuoteCard ? 100 : 0, width: showQuoteCard ? screenWidth - 32 : 0, child: Center( child: Padding( padding: EdgeInsets.symmetric(horizontal: 16), child: Text(quote, style: TextStyle(color: Colors.white, fontWeight: FontWeight.w400, fontSize: 14),), ), ), ); } // Trigger used to show the quote card widget. _showQuote() { Future.delayed(Duration(seconds: 2), () { setState(() { showQuoteCard = true; }); }); }
Масштабирование анимации виджета котировок. (Большой превью)

Обязательные свойства этого виджета:

  • vsync
    Требуемый поставщик тикера для координации анимации и смены кадров.<
  • child
    Ребенок, размер которого изменяется, будет анимирован.

Анимацию увеличения и уменьшения теперь легко приручить.

AnimatedPositioned

Здорово! Кавычки увеличиваются от центра, чтобы привлечь внимание пользователя. Что, если он соскользнул снизу при увеличении? Давай попробуем. Это движение включает в себя игру с позицией виджета котировок и анимацию изменений свойств позиции. AnimatedPositioned — идеальный кандидат.

Этот виджет автоматически изменяет положение дочернего элемента в течение заданного времени всякий раз, когда указанное положение изменяется. Одно замечание: он работает только в том случае, если его родительский виджет является «стеком». Этот виджет довольно прост и понятен в использовании. Давайте посмотрим.

 // Helper method to create the animated positioned widget. // With position changes based on “showQuoteCard” flag. _getAnimatedPositionWidget() { return AnimatedPositioned( duration: Duration(seconds: 1), curve: Curves.easeInOut, child: _getQuoteCardWidget(), top: showQuoteCard ? screenHeight/2 - 100 : screenHeight, left: !showQuoteCard ? screenWidth/2 : 12, ); }
Позиция с масштабированием анимации котировок. (Большой превью)

Этот виджет имеет только одно обязательное свойство:

  • child
    Виджет, положение которого будет изменено.

Если ожидается, что размер дочернего элемента не изменится вместе с его положением, более эффективной альтернативой этому виджету будет SlideTransition .

Вот наша полная анимация:

Все анимированные виджеты вместе. (Большой превью)

Заключение

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

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

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

Дополнительные ресурсы

  • «Виджеты анимации и движения», Flutter Docs
  • «Введение в анимацию», Flutter Docs
  • Флаттер Codelabs

Примечание редактора : огромное спасибо Ахмаду Аваису за помощь в рецензировании этой статьи.