Начинаем работать с неявной анимацией в Flutter

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

Скачать материалы

Версии программного обеспечения: Dart 2.10, Flutter 1.22, VS Code

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

  1. Анимированные эффекты в Flutter на основе неявной анимации.
  2. Создание отзывчивого текстового поля.
  3. Добавление к виджетам эффекта плавного исчезновения.
  4. Анимация перехода между страницами.
  5. Создание радиального расширяющегося меню.
  6. Реализация круглого индикатора прогресса.

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

Приступаем к работе

Загрузите стартовый проект, нажав на кнопку «Скачать материалы». Распакуйте файл после загрузки. В данном руководстве использованы скриншоты из Visual Studio Code, при желании вы можете использовать Android Studio и адаптировать инструкции под него.

Запустите Visual Studio Code, выберите на стартовой странице или в меню «Файл» пункт «Открыть», затем выберите папку стартового проекта Starter. После открытия проекта вы увидите массу ошибок в коде. Чтобы избавиться от ошибок, дважды кликните по pubspec.yaml на левой панели, а после этого нажмите кнопку «Загрузить пакеты», расположенную вверху окна. Другой вариант – открыть терминал в Visual Studio Code, и выполнить в нем команду flutter pub get.

Теперь запустите эмулятор по выбору. Запустите сборку и выполнение приложения. Вы увидите следующее:

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

Структура проекта

Главные файлы приложения, которые будут рассматриваться в этом руководстве:

  1. lib/screens/greetings_screen.dart – страница, которая загружается при открытии программы. Включает в себя текстовое поле для ввода имени пользователя и приветствие.
  2. lib/screens/overview_screen.dart – обзорная страница, на которой выводится количество купленных вами канцтоваров и прогресс на пути к цели по посадке деревьев.
  3. lib/widgets/stationery_list.dart – виджет для показа статистики по видам канцтоваров.
  4. lib/widgets/statistics_widget.dart – сводный виджет, содержащий в себе все статистические виджеты с обзорной страницы, кроме сведений по типам купленных канцтоваров.

Прежде, чем мы перейдем к написанию кода, давайте посмотрим, как работает анимация в Flutter.

Анимация в Flutter

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

Таким образом, во время просмотра более 12 последовательных изображений в секунду мы видим движение, а не отдельные картинки. Чем больше картинок в секунду, тем более плавным будет казаться движение. Современные смартфоны поддерживают частоту обновления экрана на уровне 60 кадров в секунду и выше.

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

Просмотр анимации в замедленном режиме

Откройте файл lib/screens/demo/demo_screen.dart и замените код на приведенный ниже:

//1
import 'dart:async';
import 'package:flutter/material.dart';

class DemoScreen extends StatefulWidget {
  const DemoScreen({Key key}) : super(key: key);
  @override
  State createState() {
    return DemoScreenState();
  }
}

class DemoScreenState extends State {
  int steps = 0;

  void update() {
    //2
    Timer.periodic(
      const Duration(seconds: 1),
       //3
      (timer) => setState(
        () {
          steps < 20 ? steps = steps + 1 : timer.cancel();
        },
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Animations Mystery!'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Stack(
          children: [
            Center(
              child: Container(
                //4
                width: 50.0 + steps * 5,
                height: 50.0 + steps * 5,
                color: Colors.blue[900],
              ),
            ),
            Align(
              alignment: Alignment.bottomRight,
              child: FloatingActionButton(
                  child: const Icon(Icons.add), onPressed: update),
            )
          ],
        ),
      ),
    );
  }
}

Этот код работает следующим образом:

  1. Выполняет импорт библиотеки dart:async, которая предназначена для асинхронного программирования и дает возможность использовать класс Timer.
  2. Вызывает метод Timer.periodic(), который позволяет многократно вызывать функцию – до тех пор, пока код не остановит процесс вызовом Timer.cancel().
  3. Объявляет функцию обратного вызова, которой будет управлять Timer.periodic().
  4. Перечисляет параметры виджета, к которому добавляется эффект анимации.

Чтобы увидеть код в действии, откройте файл lib/main.dart и замените home: const GreetingsScreen() на следующую строку:

home: const DemoScreen(), 

Затем импортируйте файл demo_screen.dart:

import 'package:green_stationery/screens/demo/demo_screen.dart';

Сохраните проект, запустите сборку и выполнение:

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

Это была демонстрация, поэтому после просмотра результата верните в файл lib/main.dart строку home: const DemoScreen(), которую вы заменили на home: const GreetingsScreen(). Затем удалите строку импорта:

import 'package:green_stationery/screens/demo/demo_screen.dart';

Запустите приложение, чтобы убедиться, что все работает, как надо:

Все в порядке. Пора приступать к изучению типов анимации в Flutter.

Явная и неявная анимация

В самом широком смысле, анимация во Flutter- приложениях делится на два типа:

  1. Рисованная – выглядит, как созданная художником — мультипликатором. Программно реализовать такую анимацию очень сложно.
  2. Программная – основанная на манипуляциях с виджетами. Легко создается программными средствами. В данном руководстве мы будем рассматривать одну из разновидностей программной анимации.

Программная анимация, в свою очередь, делится на явную и неявную:

  1. Неявная анимация – предполагает изменение некоторых свойств и параметров готовых элементов. Код не нужно писать с нуля.
  2. Явная анимация – создается с помощью виджета AnimatedBuilder. Предполагает кастомизацию свойств анимированных элементов и написание кода.

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

Неявная анимация подразделяется на два типа:

  1. Анимация на базе виджета AnimatedFoo. Позволяет анимировать отдельные параметры элемента – например, AnimatedSize отвечает за манипуляции с размером объекта. Реализовать такую анимацию очень просто.
  2. Кастомная неявная анимация. Используется в том случае, если не подходит ни один из встроенных методов; создается в конструкторе TweenAnimationBuilder.

Анимация кнопок

AnimatedContainer – версия виджета Container, которая позволяет анимировать большинство свойств Container. Мы воспользуемся этим виджетом для динамичного изменения цвета кнопки Let’s go («Поехали») с серого на зеленый.

Откройте файл greetings_screen.dart в папке screens. Прокрутите код вниз, найдите строку _userNameInput. Внутри этого метода вы увидите вызов onChanged для элемента TextFieldForm с комментарием // TODO. Сразу под этим комментарием вставьте фрагмент кода, приведенный ниже:

if (v.length > 2) {
  setState(() {
    _color = Colors.green;
  });
} else {
  setState(() {
    _color = Colors.grey;
  });
}

Текстовое поле вызывает метод onChanged каждый раз, когда пользователь изменяет текст. В нашем случае, параметр _color обеспечит изменение цвета на зеленый, как только в текстовое поле будет введено более двух символов. Если количество символов меньше либо равно 2, цвет останется серым. В коде используется метод setState для уведомления фреймворка о необходимости перерисовки пользовательского интерфейса.

В том же файле greetings_screen.dart замените код _getButton на следующий:

Widget _getButton() {
  // 1
  return AnimatedContainer(
    margin: const EdgeInsets.only(top: 10.0),
    // 2
    duration: const Duration(milliseconds: 900),
    padding: const EdgeInsets.all(16.0),
    decoration: BoxDecoration(
      // 3
      color: _color,
      borderRadius: const BorderRadius.all(
        Radius.circular(15.0),
      ),
      boxShadow: [
        BoxShadow(
            color: Colors.black.withOpacity(0.2),
            offset: const Offset(1.1, 1.1),
            blurRadius: 10.0),
      ],
    ),
    child: InkWell(
      onTap: () {
        if (name.length > 2) {
          if (!_currentFocus.hasPrimaryFocus) {
            _currentFocus.unfocus();
          }
          setState(() {
            // TODO
            _showNameLabel = true;
          });
        }
      },
      child: const Padding(
        padding: EdgeInsets.symmetric(horizontal: 6.0, vertical: 3.0),
        child: Text('Let\'s go'),
      ),
    ),
  );
}

Пояснения к пронумерованным комментариям в коде:

  1. AnimatedContainer – название анимированной версии виджета Container.
  2. Продолжительность – обязательный параметр для каждого анимированного виджета. Используя объект Duration, параметр передает в анимированный виджет сведения о времени, необходимом для завершения анимационного эффекта.
  3. Здесь мы назначаем цвет с помощью _color. Изменение этого параметра запускает анимацию – анимированный виджет поддерживает изменение цвета в течение заданного времени.

Теперь мы заменим изначальный цвет кнопки на серый. Для этого изменим объявление цвета _color вверху GreeetingsScreenState на следующую строку:

Color _color = Colors.grey;

Выполните горячую перезагрузку и оцените произошедшие изменения:

Поскольку мы используем класс AnimatedContainer, каждое изменение параметров автоматически создаст анимированный эффект. Здорово, правда?

Создаем отзывчивое текстовое поле TextField

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

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

Для этого заменим строку:

class GreetingsScreenState extends State<GreetingsScreen> {

на этот код:

class GreetingsScreenState extends State<GreetingsScreen>
    with TickerProviderStateMixin {

Для передачи продолжительности воспользуемся методом TickerProviderStateMixin. Добавьте следующее свойство в GreetingsScreenState:

bool _textFieldSelected = false; 

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

Widget _userNameInput() {
  return AnimatedSize(
    // 1
    duration: const Duration(milliseconds: 500),
    // 2
    vsync: this,
    child: Container(
      width: _textFieldSelected ? double.infinity : 3 * _screenWidth / 4,
      padding: const EdgeInsets.symmetric(horizontal: 16.0),
      child: TextFieldForm(
        onTap: () {
          setState(() {
            // 3
            _textFieldSelected = true;
          });
        },
        onChanged: (v) {
          name = v;
          if (v.length > 2) {
            setState(() {
              _color = Colors.green;
            });
          } else {
            setState(() {
              _color = Colors.grey;
            });
          }
        },
      ),
    ),
  );
}

Рассмотрим, что делает данный код:

  1. duration устанавливает время, за которое текстовое поле расширяется до максимума, а затем возвращается к исходному виду.
  2. vsync обеспечивает передачу информации в TickerProvider о времени, прошедшем с последнего обновления экрана. Объекты класса Ticker обеспечивают нужную частоту обновления для создания анимационных эффектов. В нашем случае за обновление отвечает TickerProviderStateMixin.

Эффект запускается, как только изменяется состояние _textFieldSelected. Для отслеживания нажатия на экран за пределами текстового поля мы будем использовать детектор жестов.

Откройте GreetingsScreenState и найдите параметр onTap детектора жестов GestureDetector. Добавьте следующий код в верхней части функции onTap:

setState(() {
  _textFieldSelected = false;
});

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

Запустите сборку и выполнение:

Теперь все работает, как надо. Здорово!

Использование плавного исчезновения

Перейдем к следующему анимированному виджету, AnimationCrossFade. Он позволяет быстро и просто создавать эффекты плавного исчезновения при переходе между виджетами. Если виджеты имеют одинаковый размер, реализовать эффект можно очень просто; для виджетов разного размера необходимо использовать кастомный layoutBuilder, чтобы избежать появления внезапных передергиваний экрана во время анимации.

Мы воспользуемся AnimationCrossFade для исчезновения приветственного сообщения и текстового поля. Добавьте приведенный ниже код в GreetingsScreenState:

bool _showWelcomeMessage = false;

Widget _getAnimatedCrossFade() {
  return AnimatedCrossFade(
    //1
    firstChild: _userInputField(),
    secondChild: _welcomeMessage(),
    //2
    crossFadeState: _showWelcomeMessage
        ? CrossFadeState.showSecond
        : CrossFadeState.showFirst,
    //3
    duration: const Duration(milliseconds: 900),
  );
}

Код работает так:

  1. Параметры firstChild и secondChild принимают названия виджетов, при переходе между которыми возникает эффект исчезновения.
  2. В crossFadeState передается название элемента, который будет видимым после завершения эффекта. В случае с _showWelcomeMessage это будет виджет приветственного сообщения, в любом другом случае, видимым станет поле ввода данных _userInputField
  3. Этот фрагмент определяет продолжительность эффекта.

Внесем несколько изменений в код. Перейдите в _getButton и найдите // TODO комментарий. Замените строку_showNameLabel = true; на следующую:

_showWelcomeMessage = true;

Теперь перейдите в метод build, найдите строку:

!_showNameLabel ? _userInputField() : _welcomeMessage(),

И замените ее на следующую:

_getAnimatedCrossFade(),

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

Анимация переходов между страницами

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

Импортируем библиотеку Cupertino в файл greeting_screen.dart:

import 'package:flutter/cupertino.dart';

Внутри _welcomeMessage найдите комментарий // TODO: navigation, входящий в метод onPressed, и замените его на следующий код:

Navigator.of(context).pushReplacement(
  CupertinoPageRoute(
    builder: (context) => const OverviewScreen(),
  ),
);

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

Создаем радиальное меню

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

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

  1. Не надо прокручивать меню в поисках нужной опции – все пункты находятся на виду, на равном расстоянии друг от друга.
  2. Круглое меню лучше подходит для сенсорных экранов.
  3. Радиальные меню удобнее при постоянном использовании приложения.

Начнем работу с изменения свойств круглой кнопки для переключения между открытым и закрытым состояниями. Откройте файл overview_screen.dart, удалите строку _radialMenuCenterButton и поместите на ее место следующий код:

bool _opened = false;

Widget _radialMenuCenterButton() {
  //1
  return InkWell(
    //2
    key: UniqueKey(),
    child: Padding(
      padding: const EdgeInsets.all(8.0),
      child: Container(
        height: 80.0,
        width: 80.0,
        decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(40.0),
            color: Colors.green[600]),
        child: Center(
          child: Icon(_opened == false ? Icons.add : Icons.close,
              color: Colors.white),
        ),
      ),
    ),
    onTap: () {
      //3
      setState(() {
        _opened == false ? _opened = true : _opened = false;
      });
    },
  );
}

Данный код делает следующее:

  1. Создает отзывчивую область с помощью InkWell.
  2. Использует UniqueKey() для получения уникального ключа при каждой сборке виджета. Чуть позже мы рассмотрим, для чего это нужно.
  3. Изменяет параметр Boolean _opened кнопки RadialMenuCenterButton, после чего фреймворк запускает сборку этого виджета и всех вложенных в него элементов.

Добавление AnimatedSwitcher в радиальное меню

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

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

Для реализации эффекта добавьте виджет _getRadialMenu в OverviewScreenState в файле overview_screen.dart, как показано ниже:

Widget _getRadialMenu() {
  return AnimatedSwitcher(
    // 1
    duration: const Duration(milliseconds: 300),
    // 2
    transitionBuilder: (child, animation) {
      return ScaleTransition(child: child, scale: animation);
    },
    // 3
    child: radialMenuCenterButton(),
  );
}

Этот код делает следующее:

Определяет продолжительность эффекта, как и в предыдущих случаях.

Выбирает тип анимации ScaleTransition. Наличие выбора между эффектами перехода – одно из преимуществ AnimatedSwitcher по сравнению с AnimationCrossFade.

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

Все почти готово – остался один шаг. Перейдите в build, найдите комментарий // TODO: Radial menu, замените строку radialMenuCenterButton(); на следующий код:

Align(
  alignment: Alignment.bottomRight,
  child: _getRadialMenu(),
),

Сохраните файл, используйте «Горячую перезагрузку», оцените результат:

Заметьте, что кнопка изменяется между состояниями «Добавить» и «Закрыть».

Создание виджета радиального меню

Переходим к созданию виджетов меню. На следующем этапе добавим в меню пункты. Начнем работу с создания файла под названием radial_button.dart, вставим в него приведенный ниже код:

import 'package:flutter/material.dart';

class RadialButton extends StatefulWidget {
  final double hiddenHorizontalPlacement;
  final double hiddenVerticalPlacement;
  final double visibleHorizontalPlacement;
  final double visibleVerticalPlacement;
  final Color color;
  final IconData image;
  final Function onTap;
  final bool opened;
  const RadialButton(
      {Key key,
      this.hiddenHorizontalPlacement,
      this.hiddenVerticalPlacement,
      this.visibleHorizontalPlacement,
      this.visibleVerticalPlacement,
      this.color,
      this.image,
      this.onTap,
      this.opened})
      : super(key: key);
  @override
  State createState() {
    return RadialButtonState();
  }
}

class RadialButtonState extends State<RadialButton> {
  @override
  Widget build(BuildContext context) {
    return Positioned(
      left: widget.opened == false
          ? widget.hiddenHorizontalPlacement
          : widget.visibleHorizontalPlacement,
      bottom: widget.opened == false
          ? widget.hiddenVerticalPlacement
          : widget.visibleVerticalPlacement,
      child: InkWell(
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: Container(
              height: 60.0,
              width: 60.0,
              decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(40.0),
                  color: widget.color),
              child: Center(
                child: Icon(widget.image, color: Colors.white),
              ),
            ),
          ),
          onTap: widget.onTap),
    );
  }
}

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

Теперь вернитесь к файлу overview_screen.dart и добавьте в него следующий код:

import 'package:green_stationery/data/services/plant_statiotionery_convertor.dart';
import 'package:green_stationery/widgets/radial_button.dart';
import 'package:flutter_icons/flutter_icons.dart';

Таким образом вы сделали радиальные кнопки дочерними виджетами Stack, прямо перед виджетом выравнивания Align. Фрагмент кода располагается в build под комментарием // TODO: Radial menu.

RadialButton(
  // 1
  hiddenHorizontalPlacement: _screenWidth - 90.0,
  visibleHorizontalPlacement: _screenWidth - 250.0,
  // 2
  hiddenVerticalPlacement: 10.0,
  visibleVerticalPlacement: 10.0,
  color: Colors.green[400],
  image: FontAwesome.book,
  opened: _opened,
  onTap: () {
    setState(() {
      //3
      PlantStationeryConvertor.addStationery(stationeryType: 'Books');
    });
  },
),

RadialButton(
  hiddenHorizontalPlacement: _screenWidth - 90.0,
  visibleHorizontalPlacement: _screenWidth - 90.0 - 139.0,
  hiddenVerticalPlacement: 10.0,
  visibleVerticalPlacement: 10.0 + 80,
  color: Colors.green[400],
  image: Entypo.brush,
  opened: _opened,
  onTap: () {
    setState(() {
      PlantStationeryConvertor.addStationery(stationeryType: 'Pens');
    });
  },
),

RadialButton(
  hiddenHorizontalPlacement: _screenWidth - 90.0,
  visibleHorizontalPlacement: _screenWidth - 90.0 - 80.0,
  hiddenVerticalPlacement: 10.0,
  visibleVerticalPlacement: 10.0 + 139.0,
  color: Colors.green[400],
  image: SimpleLineIcons.notebook,
  opened: _opened,
  onTap: () {
    setState(() {
      PlantStationeryConvertor.addStationery(
          stationeryType: 'Notebooks');
    });
  },
),

RadialButton(
  hiddenHorizontalPlacement: _screenWidth - 90.0,
  visibleHorizontalPlacement: _screenWidth - 90.0,
  hiddenVerticalPlacement: 10.0,
  visibleVerticalPlacement: 10.0 + 160.0,
  color: Colors.green[400],
  image: FontAwesome.archive,
  opened: _opened,
  onTap: () {
    setState(() {
      PlantStationeryConvertor.addStationery(
          stationeryType: 'Assiting Materials');
    });
  },
),

Этот фрагмент кода кажется большим, но он всего лишь добавляет в меню четыре виджета RadialButtons, располагая их по верхней дуге плавающей кнопки. Обратите внимание на пронумерованные комментарии:

  1. Расположение кнопок по горизонтали определяется как расстояние до левой границы экрана, вычисляемое на основе ширины экрана. Вертикальное расположение вычисляется на основе высоты экрана и определяется как расстояние до нижней границы экрана.
  2. Здесь определяются свойства радиальной кнопки меню – видна ли она на экране или невидима. Невидимость предполагает, что кнопка временно скрывается под меню.
  3. Функция addStationery принимает аргументы класса stationeryType и увеличивает количество объектов на единицу.

Запустите сборку и выполнение. Перейдите на страницу статистики и нажмите на плавающую кнопку – сверху появятся четыре дополнительные кнопки.

Дополнительная анимация для радиального меню

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

При работе с этим виджетом следует помнить следующее:

  1. Он срабатывает только в том случае, если родительским виджетом является Stack.
  2. Хотя AnimatedPositioned – очень полезный виджет, он также потребляет слишком много ресурсов, поскольку вызывает переборку всего макета при каждом обновлении кадра.

Вернитесь к файлу radial_button и замените виджет Positioned вверху Build на AnimatedPositioned. Затемдобавьте к AnimatedPositioned два новых аргумента:

duration: const Duration(milliseconds: 500),
curve: Curves.elasticIn,

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

Теперь работа по анимированию меню закончена. Сохраните проект, запустите сборку и выполнение:

Создаем круглый индикатор прогресса

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

Мы будем использовать приведенный ниже код для создания круглого индикатора прогресса. Откройте виджет statistics_widget.dart, добавьте новый метод в StatisticsWidgetState:

Widget _tweenBuilder({
  double endValue,
  String statsName,
  Color color,
  double textSize,
}) {
  return TweenAnimationBuilder(
    // 1
    tween: Tween(begin: 0.0, end: endValue),
    // 2
    duration: const Duration(milliseconds: 1500),
    // 3
    curve: Curves.easeOutBack,
    // 4
    child: _labelWidget(statsName, textSize),
    // 5
    builder: (
      context,
      value,
      child,
    ) {
      return _circularProgressBar(
            color: color,
            value: value,
            labelWidget: child);
    },
  );
}

Рассмотрим пронумерованные этапы выполнения:

  1. Код передает объект Tween, который представляет собой линейную интерполяцию между начальным и конечным параметрами. Параметры передаются в качестве свойств анимируемому виджету.
  2. Здесь определяется продолжительность анимации.
  3. easeOutBlack изменяет частоту обновления на протяжении времени анимации.
  4. TweenAnimationBuilder принимает дочерние виджеты, которые не зависят от процесса анимации и являются вложенными элементами для анимируемых виджетов. Этот прием ускоряет рендеринг, потому что дочерние виджеты конструируются лишь однажды, в отличие от анимируемых.

Прежде, чем мы продолжим работу, обратите внимание на некоторые важные особенности, касающиеся TweenAnimationBuilder:

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

Пока у вас открыт StatisticsWidgetState, перейдите в build и найдите комментарий //TODO: tweenBuilder. Удалите этот код:

_circularProgressBar(
  value: 0.4,
  color: Colors.blue,
  labelWidget: _labelWidget('Stationery Purchased', _textSize)),
_circularProgressBar(
  value: 0.3,
  color: Colors.green,
  labelWidget: _labelWidget('Planting Goal', _textSize)),

Вставьте вместо удаленного фрагмента новый код:

_tweenBuilder(
    statsName: 'Stationery Purchased',
    endValue: PlantStationeryConvertor.plantFraction,
    color: Colors.blue,
    textSize: _textSize),
_tweenBuilder(
    statsName: 'Planting Goal',
    endValue: PlantStationeryConvertor
            .plantingStatus.getPlantationGoal / 10,
    color: Colors.green,
    textSize: _textSize),

Теперь все готово. Сохраните проект, запустите сборку и выполнение приложения и посмотрите на все эффекты, которые вы создали в рамках прохождения этого руководства.

Что делать дальше

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

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

Надеюсь, статья вам понравилась. Если вы хотели бы прочитать другие статьи на эту тему, или у вас есть какие-то вопросы – расскажите об этом в комментариях.

Скачать материалы

Данная публикация является переводом статьи «Implicit Animations in Flutter: Getting Started» , подготовленная редакцией проекта.

Меню