paint-brush
Создание шахматной анимации во Flutter: руководство по микровзаимодействиямк@nikkieke
460 чтения
460 чтения

Создание шахматной анимации во Flutter: руководство по микровзаимодействиям

к 19m2024/06/04
Read on Terminal Reader

Слишком долго; Читать

Концепция микровзаимодействий основана на идее о том, что мелкие детали могут оказать большое влияние на общее впечатление пользователя. Во Flutter вы можете создавать микровзаимодействия, создавая тонкие анимации, используя неявную или явную анимацию. В этой статье мы будем создавать поэтапную анимацию, которая анимирует дочерние элементы в виджете столбца при пролистывании этой страницы.
featured image - Создание шахматной анимации во Flutter: руководство по микровзаимодействиям
undefined HackerNoon profile picture
0-item

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


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


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


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

Что такое концепция микровзаимодействий?

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


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


  • Анимация кнопок: кнопка меняет цвет или размер при наведении или нажатии.


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


  • Жесты смахивания: анимация, реагирующая на жесты смахивания.


  • Переходы навигации: плавная анимация при переходе между экранами.

Микровзаимодействия в реальных приложениях

Ниже мы можем увидеть реальное применение микровзаимодействий. Эти тонкие анимации созданы во Flutter для улучшения пользовательского опыта. 👇🏽

Рекомендации по дизайну для этих приложений были получены с Dribbble.

Как создать шахматную анимацию во Flutter

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


Вот как будет выглядеть анимация, когда мы закончим 👇🏽

Предварительное условие

Чтобы максимально эффективно использовать это руководство, у вас должно быть следующее:


  • Базовое понимание того, как создается анимация во Flutter.
  • Хорошее понимание основ Flutter и Dart.
  • Редактор кода, VScode или Android Studio.
  • Эмулятор или устройство для сборки


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


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

 class MainScreen extends StatefulWidget { const MainScreen({super.key}); @override State<MainScreen> createState() => _MainScreenState(); } class _MainScreenState extends State<MainScreen>{ final controller = PageController(keepPage: true); @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: PageView( controller: controller, children: [ Page1(pageController: controller,), Page2(pageController: controller,), ], ), ), bottomNavigationBar: Container( alignment: Alignment.topCenter, padding: const EdgeInsets.only(top: 10), height: 30, child: SmoothPageIndicator( controller: controller, count: 2, effect: const JumpingDotEffect( dotHeight: 10, dotWidth: 10, activeDotColor: Colors.grey, dotColor: Colors.black12, ), ), ) ); } }


SmoothPageIndicator использованный в приведенном выше фрагменте кода, можно найти на pub.dev . Здесь SmoothPageIndicator используется для отображения текущей страницы. Вы можете добавить пакет в свой pubspec.yaml вот так 👇🏽

 dependencies: flutter: sdk: flutter smooth_page_indicator: ^1.1.0


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

 class EmptyCard extends StatelessWidget { const EmptyCard({super.key, required this.width, required this.height}); final double width; final double height; @override Widget build(BuildContext context) { return Container( height: height, width: width, decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(10)), color: Colors.blue.shade200, boxShadow: const [ BoxShadow( color: Colors.black12, blurRadius: 4, offset: Offset(0,4), ) ] ), ); } }


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

 class AnimateWidget extends StatefulWidget { const AnimateWidget({super.key, required this.duration, required this.position, required this.horizontalOffset, required this.child, required this.controller, }); final Duration duration; final int position; final double? horizontalOffset; final Widget child; final PageController controller; @override State<AnimateWidget> createState() => _AnimateWidgetState(); }


С помощью параметров AnimateWidget , как показано в приведенном выше фрагменте, мы можем успешно управлять:

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


Далее в AnimateWiget мы определим следующее:

  • AnimationController : используется для управления последовательностью анимации.

  • Текущая страница: будет использоваться для хранения данных текущей страницы.

  • Таймер: будет использоваться для запуска анимации после некоторой задержки; это то, что приведет к эффекту каскадной анимации.


     class _AnimateWidgetState extends State<AnimateWidget> with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin{ @override bool get wantKeepAlive => true; late AnimationController animationController; int currentPage = 0; Timer? _timer; @override void initState() { super.initState(); animationController = AnimationController(vsync: this, duration: widget.duration); _timer = Timer(getDelay(), animationController.forward); widget.controller.addListener(() { currentPage = widget.controller.page!.round(); if(currentPage == widget.controller.page){ _timer = Timer(getDelay(), animationController.forward); } }); } @override void dispose() { _timer?.cancel(); animationController.dispose(); super.dispose(); } Duration getDelay(){ var delayInMilliSec = widget.duration.inMilliseconds ~/ 6; int getStaggeredListDuration(){ return widget.position * delayInMilliSec; } return Duration(milliseconds: getStaggeredListDuration()); }

Разобрав приведенный выше фрагмент кода, вы заметите, что:

  • Для управления последовательностью анимации мы использовали AnimationController , поэтому ввели SingleTickerProviderStateMixin .


  • В методе init state мы инициализировали AnimationController , затем запускали анимацию с помощью animationController.forward при входе на страницу и пролистывании страницы.


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


  • В методе dispose мы очистили ресурсы.


  • Мы использовали AutomaticKeepAliveClientMixin чтобы сохранить состояние виджета. Это сделано для предотвращения удаления AnimationController , когда страница перелистывается и больше не видна.


  • В функции getDelay мы рассчитали задержку перед запуском анимации для каждого элемента. Для этого мы разделили длительность в миллисекундах на 6 и аппроксимировали результаты, а затем умножили их на позицию. Это позиция (в данном случае индекс) элемента.


В методе Build мы возвращаем AnimatedBuilder . В этом конструкторе анимированных изображений мы вернем функцию виджета под названием _slideAnimation , которая возвращает виджет Transform.translate . В функции _slideAnimation у нас есть функция offsetAnimation .


Функция offsetAnimation возвращает свойство Animation, которое используется в виджете Transform.translate . Виджет Transform.translate анимирует дочерний виджет, используя значение анимации.

 @override Widget build(BuildContext context) { super.build(context); return AnimatedBuilder( animation: animationController, builder: (context, child){ return _slideAnimation(animationController); } ); } Widget _slideAnimation(Animation<double> animationController){ Animation<double> offsetAnimation(double offset, Animation<double> animationController) { return Tween<double>(begin: offset, end: 0.0).animate( CurvedAnimation( parent: animationController, curve: const Interval(0.0, 1.0, curve: Curves.ease), ), ); } return Transform.translate( offset: Offset( widget.horizontalOffset == 0.0 ? 0.0 : offsetAnimation(widget.horizontalOffset!, animationController).value, 0.0, ), child: widget.child, ); }


Это полный код класса AnimateWidget 👇🏽

 class AnimateWidget extends StatefulWidget { const AnimateWidget({super.key, required this.duration, required this.position, required this.horizontalOffset, required this.child, required this.controller, }); final Duration duration; final int position; final double? horizontalOffset; final Widget child; final PageController controller; @override State<AnimateWidget> createState() => _AnimateWidgetState(); } class _AnimateWidgetState extends State<AnimateWidget> with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin{ @override bool get wantKeepAlive => true; late AnimationController animationController; int currentPage = 0; Timer? _timer; @override void initState() { super.initState(); animationController = AnimationController(vsync: this, duration: widget.duration); _timer = Timer(getDelay(), animationController.forward); widget.controller.addListener(() { currentPage = widget.controller.page!.round(); if(currentPage == widget.controller.page){ _timer = Timer(getDelay(), animationController.forward); } }); } @override void dispose() { _timer?.cancel(); animationController.dispose(); super.dispose(); } Duration getDelay(){ var delayInMilliSec = widget.duration.inMilliseconds ~/ 6; int getStaggeredListDuration(){ return widget.position * delayInMilliSec; } return Duration(milliseconds: getStaggeredListDuration()); } @override Widget build(BuildContext context) { super.build(context); return AnimatedBuilder( animation: animationController, builder: (context, child){ return _slideAnimation(animationController); } ); } Widget _slideAnimation(Animation<double> animationController){ Animation<double> offsetAnimation(double offset, Animation<double> animationController) { return Tween<double>(begin: offset, end: 0.0).animate( CurvedAnimation( parent: animationController, curve: const Interval(0.0, 1.0, curve: Curves.ease), ), ); } return Transform.translate( offset: Offset( widget.horizontalOffset == 0.0 ? 0.0 : offsetAnimation(widget.horizontalOffset!, animationController).value, 0.0, ), child: widget.child, ); } }

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


Далее мы сопоставим дочерние элементы, обернув каждого дочернего элемента AnimateWidget .

 class AnimateList{ static List<Widget>toStaggeredList({ required Duration duration, double? horizontalOffset, required PageController controller, required List<Widget>children, })=> children .asMap() .map((index, widget){ return MapEntry( index, AnimateWidget( duration: duration, position: index, horizontalOffset: horizontalOffset, controller: controller, child: widget, ) ); }) .values .toList(); }


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

 class Page1 extends StatelessWidget { const Page1({super.key, required this.pageController}); final PageController pageController; @override Widget build(BuildContext context) { return SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( children: AnimateList.toStaggeredList( duration: const Duration(milliseconds: 375), controller: pageController, horizontalOffset: MediaQuery.of(context).size.width / 2, children: [ const EmptyCard(width: 250, height: 50,), const Padding( padding: EdgeInsets.only(top: 20), child: EmptyCard(width: 180, height: 80,), ), const Padding( padding: EdgeInsets.only(top: 20), child: EmptyCard(width: 270, height: 50,), ), const Padding( padding: EdgeInsets.symmetric(vertical: 20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ EmptyCard(height: 50, width: 70), EmptyCard(height: 50, width: 70), EmptyCard(height: 50, width: 70), ], ), ), const EmptyCard(width: 250, height: 50,), const Padding( padding: EdgeInsets.only(top: 20), child: EmptyCard(width: 180, height: 80,), ), const Padding( padding: EdgeInsets.only(top: 20), child: EmptyCard(width: 270, height: 50,), ), const Padding( padding: EdgeInsets.symmetric(vertical: 20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ EmptyCard(height: 50, width: 70), EmptyCard(height: 50, width: 70), EmptyCard(height: 50, width: 70), ], ), ), ], ), ), ); } } class Page2 extends StatelessWidget { const Page2({super.key, required this.pageController}); final PageController pageController; @override Widget build(BuildContext context) { return SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( children: AnimateList.toStaggeredList( duration: const Duration(milliseconds: 375), controller: pageController, horizontalOffset: MediaQuery.of(context).size.width / 2, children: [ const EmptyCard(width: 220, height: 70,), const Padding( padding: EdgeInsets.only(top: 20), child: EmptyCard(width: 300, height: 70,), ), const Padding( padding: EdgeInsets.only(top: 20), child: EmptyCard(width: 200, height: 50,), ), const Padding( padding: EdgeInsets.symmetric(vertical: 20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ EmptyCard(height: 70, width: 70), EmptyCard(height: 70, width: 70), ], ), ), const EmptyCard(width: 220, height: 70,), const Padding( padding: EdgeInsets.only(top: 20), child: EmptyCard(width: 300, height: 70,), ), const Padding( padding: EdgeInsets.symmetric(vertical: 20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ EmptyCard(height: 70, width: 70), EmptyCard(height: 70, width: 70), ], ), ), ], ), ), ); } }

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


Это наш окончательный результат:

Заключение

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


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


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

Рекомендации

Пакет ступенчатой анимации Flutter