Пошаговая анимация состоит из последовательных или перекрывающихся анимаций. Анимация может быть чисто последовательной, когда одно изменение происходит за другим, или она может частично или полностью перекрываться. Когда анимация последовательная, элементы анимируются последовательно с небольшой задержкой между временем начала каждого элемента. Это создает эффект каскада или пульсации, при котором анимация перемещается по элементам поэтапно, а не по всем элементам сразу.
Поэтапная анимация считается типом микровзаимодействия, поскольку она улучшает взаимодействие с пользователем, предоставляя тонкую интерактивную обратную связь, которая направляет пользователя через интерфейс. Во Flutter вы можете создавать микровзаимодействия, создавая тонкие анимации, используя неявную или явную анимацию.
Что касается контекста, неявная анимация спроектирована так, чтобы быть простой и удобной в использовании, поскольку детали анимации абстрагируются, тогда как явная анимация больше подходит для сложных анимаций, поскольку они предлагают полный контроль над процессом анимации. Это особенно полезно, когда вам нужно иметь более детальный контроль над анимацией.
В этой статье мы углубимся в концепцию микровзаимодействий, а затем в случае использования микровзаимодействий мы будем использовать явную анимацию для создания поэтапной анимации, которая анимирует дочерние элементы, содержащиеся в виджете столбца.
Отличные продукты — это продукты, которые обладают хорошими характеристиками и деталями. Особенности привлекают людей к вашим продуктам, но детали удерживают их. Эти детали выделяют ваше приложение среди других. С помощью микровзаимодействий вы можете создавать эти детали, предоставляя своим пользователям восхитительные отзывы.
Концепция микровзаимодействий основана на идее о том, что мелкие детали могут оказать большое влияние на общее впечатление пользователя. Микровзаимодействия можно использовать для выполнения важных функций, таких как передача обратной связи или результата действия. Примеры микровзаимодействий включают в себя:
Ниже мы можем увидеть реальное применение микровзаимодействий. Эти тонкие анимации созданы во Flutter для улучшения пользовательского опыта. 👇🏽
Рекомендации по дизайну для этих приложений были получены с Dribbble.
В этом примере мы создадим поэтапную анимацию, которая анимирует дочерние элементы в виджете столбца при пролистывании страницы. Он будет построен с использованием подхода явной анимации, поскольку в этом случае нам потребуется полный контроль над тем, как мы хотим, чтобы анимация запускалась.
Вот как будет выглядеть анимация, когда мы закончим 👇🏽
Чтобы максимально эффективно использовать это руководство, у вас должно быть следующее:
После того, как вы ознакомились со всеми предварительными условиями проекта, давайте углубимся.
Сначала мы создадим главный экран, содержащий эти две страницы. Эти две страницы будут заключены в виджет просмотра страниц, который контролирует, какая страница отображается при пролистывании. Кроме того, внизу главного экрана у нас есть индикатор, показывающий страницу, на которой мы сейчас находимся.
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.
Если статья оказалась для вас полезной, вы можете поддержать ее лайком или комментарием. Вы также можете подписаться на меня, чтобы увидеть больше статей по теме.