Animacje we Flutterze, czyli jak tchnąć życie w aplikację z pomocą Rive

Animacje we Flutterze, czyli jak tchnąć życie w aplikację z pomocą Rive

Powiedzmy, że implementujesz ekran odpowiedzialny za pobieranie większej ilości danych. Niestety trafiasz na problem: czym zająć użytkownika w czasie, kiedy będą przesyłane pliki?

Jeżeli wyświetlenie animacji to pierwsze rozwiązanie, które przyszło Ci do głowy – witaj w klubie! Jeśli jednak masz wątpliwości albo nie wiesz, jak zabrać się za tworzenie animacji we Flutterze, ten artykuł powinien Ci pomóc.

Dlaczego warto dodawać animacje w aplikacjach mobilnych?

Schludny, dobrze wyglądający interfejs już nie wystarczy, żeby aplikacja wzbudzała zainteresowanie. Co możesz zrobić, żeby zachęcić użytkowników do skorzystania z Twojego rozwiązania?

Zadbaj o to, żeby mieli poczucie kontroli i widzieli, że ich działania przynoszą efekty. Aplikacja powinna wysyłać użytkownikowi informację zwrotną za każdym razem, gdy podejmuje on jakąś akcję. W ten sposób szybciej nauczy się ją obsługiwać i będzie bardziej skłonny do korzystania z niej w przyszłości.

Jakie jeszcze są zalety animacji? Pozwalają skierować uwagę użytkownika w konkretne miejsce np. tam, gdzie znajduje się informacja o przecenie. Zapewniają też płynne przejścia pomiędzy następującymi po sobie ekranami. Dzięki temu rosną szanse na to, że doświadczenia użytkownika będą pozytywne. Zwłaszcza jeśli animacje są powiązane z oferowanymi produktami, a ich wygląd jest spójny z materiałami brandowymi marki.

Jak Flutter pomaga tworzyć animacje?

Flutter jest idealnym narzędziem do budowania wydajnych animacji, które na dodatek są atrakcyjne wizualnie. Przy ich tworzeniu ogranicza Cię tylko wyobraźnia. Dlatego teraz porozmawiajmy o tym, jak dodać animację do swojego projektu aplikacji cross-platformowej.

Rodzaje animacji we Flutterze

Animacje (a raczej ich typy) można podzielić na explicit animations i implicit animations. Czym się one różnią?

Implicit animations

Gdy animacja opiera się jedynie na zmiennej dostępnej w widżecie (np. kolorze albo wielkości), można wybrać animację typu implicit. Oznacza to, że korzystasz z wbudowanych w silnik Fluttera widżetów, np. takich jak: AnimatedContainer, AnimatedOpacity, AnimatedCrossFade i wielu innych.

Animacja tworzy się za każdym razem, kiedy następuje zmiana wartości zadanej.

Explicit animations

W przypadku animacji explicit musisz samodzielnie określić, w jaki sposób mają się one zachowywać. Te animacje korzystają z AnimationControllera. Ich zaletą jest to, że oferują więcej możliwości niż typ implicit i zachowują wszystkie jego funkcjonalności.

Należy zaznaczyć, że AnimationController nie jest widżetem. Pamiętajcie o pozbyciu się go z pamięci w metodzie dispose.

Trudnością jest jednak konieczność własnoręcznego zarządzania stanem, w jakim znajduje się dana animacja. Z tego powodu z explicit animations korzysta się zwykle w przypadku bardziej złożonych animacji.

Staggered animations

Co robić, gdy masz za zadanie stworzyć skomplikowaną animację? Chociażby taką, której elementy powinny zmieniać się w określonej sekwencji albo nakładać się na siebie? Odpowiedź brzmi: wykorzystaj staggered animations, które stanowią podtyp explicit animations.

Wytłumaczę, jak to zrobić, posiłkując się przykładem z dokumentacji Fluttera i animacją widoczną poniżej (jest też dostępna w serwisie YouTube). Działa ona w ten sposób, że widżet najpierw pojawia się na ekranie, a następnie zwiększa swoją szerokość. Po osiągnięciu ustalonego rozmiaru wszerz, zaczyna rosnąć wzdłuż. Do tego widget zmienia swoją pozycję na ekranie, by znaleźć się w jego centrum. Potem prostokąt przekształca się w koło i przybiera pomarańczowy kolor. ​​Na koniec animacja zaczyna się odtwarzać od tyłu.

animation

Źródło: Stagger Demo; https://docs.flutter.dev/development/ui/animations/staggered-animations

Zaimplementowanie tej animacji wymaga ustalenia sekwencji odtwarzania poszczególnych animacji po sobie. Pracę warto więc rozpocząć od rozpisania sekwencji działania (czyli wypisania co dzieje się w poszczególnych klatkach animacji).

Źródło: https://docs.flutter.dev/development/ui/animations/staggered-animations

Animowanie skaczącego prostokąta

Gdy już ustalisz, co chcesz zrobić, rozpisz swój plan.

Teraz możesz zająć się kodowaniem.

Tworzenie animacji we Flutterze zacznij od stworzenia BouncingAnimationWidget typu StatefulWidget. Używaj przy tym SingleTickerProviderStateMixin. To mixin odpowiedzialny za kontrolowanie odświeżania animacji. Zapewnia Ticker, który pozwala silnikowi Fluttera na utrzymanie 60 klatek na sekundę, wyświetlając animację. Następnie deklarujesz sam AnimationController, który ma tym wszystkim zarządzać.

Pamiętaj Dla wielu użytych obiektów AnimationControllera należy skorzystać z rozszerzenia stanu mixinem TickerProviderStateMixin, aby zapewnić poprawne działanie animacji.

class BouncingAnimationWidget extends StatefulWidget {
 const BouncingAnimationWidget({Key? key}) : super(key: key);
 @override
 State<BouncingAnimationWidget> createState() =>
     _BouncingAnimationWidgetState();
}
class _BouncingAnimationWidgetState extends State<BouncingAnimationWidget>
   with SingleTickerProviderStateMixin {
 late final AnimationController _controller;
 @override
 void initState() {
   super.initState();
   _controller = AnimationController(
     duration: const Duration(milliseconds: 500),
     vsync: this,
   );
 }
@override
 Widget build(BuildContext context) {
   return Scaffold();
 }
 @override
 void dispose() {
   _controller.dispose();
   super.dispose();
 }
}

Pora na stworzenie obiektu, który będziesz animować, czyli prostokąta z cieniem.

Stack(
         alignment: Alignment.center,
         children: [
           _boxShadow(context),
           Align(
             alignment: Alignment(0.0, _boxJumpHeight.value),
             child: _animatedBox(context),
           ),
         ],
       ),

Widget _boxShadow(BuildContext context) => Container(
       width: 180,
       height: 15,
       decoration: BoxDecoration(
         borderRadius:
             BorderRadius.all(Radius.elliptical(180, 15)),
         boxShadow: [
           BoxShadow(
             color: Colors.black.withOpacity(0.15),
             spreadRadius: 5,
             blurRadius: 4,
             offset: const Offset(0, 3),
           ),
         ],
       ),
     );

 Widget _animatedBox(BuildContext context) => Container(
         width: 160,
         height: 50,
         color: Colors.white,
       );

Statyczny obiekt aktualnie prezentuje się tak:

Teraz zadeklaruj animacje, dodając je w poszczególnych interwałach.

void _initJumpAnimation() => _boxJumpHeight = Tween<double>(
       begin: -0.07,
       end: -0.5,
     ).animate(
       CurvedAnimation(
         parent: _controller,
         curve: const Interval(
           0.0,
           1.0,
           curve: Curves.easeInOut,
         ),
       ),
     );
 
 void _initBoxRotationAnimation() => _boxRotationAngle = Tween<double>(
       begin: 0,
       end: 360,
     ).animate(
       CurvedAnimation(
         parent: _controller,
         curve: const Interval(
           0.25,
           1.0,
           curve: Curves.ease,
         ),
       ),
     );
 
 void _initBoxWidthAnimation() => _boxWidth = Tween<double>(
       begin: 160,
       end: 50,
     ).animate(
       CurvedAnimation(
         parent: _controller,
         curve: const Interval(
           0.05,
           0.3,
           curve: Curves.ease,
         ),
       ),
     );
 
 void _initBoxShadowWidthAnimation() => _boxShadowWidth = Tween<double>(
       begin: 180,
       end: 50,
     ).animate(
       CurvedAnimation(
         parent: _controller,
         curve: const Interval(
           0.05,
           0.5,
           curve: Curves.ease,
         ),
       ),
     );
 
 void _initBoxShadowIntensityAnimation() =>
     _boxShadowIntensity = Tween<double>(
       begin: 0.15,
       end: 0.05,
     ).animate(
       CurvedAnimation(
         parent: _controller,
         curve: const Interval(
           0.05,
           1.0,
           curve: Curves.ease,
         ),
       ),
     );

Gdy masz już animacje, dodaj możliwość ich uruchomienia wraz z rotacją do prostokąta oraz zmianą statycznych wartości na te, które są kontrolowane przez poszczególne animacje.

Widżet powinien aktualnie wyglądać tak:

class BouncingAnimationWidget extends StatefulWidget {
 const BouncingAnimationWidget({Key? key}) : super(key: key);
  

@override
 State<BouncingAnimationWidget> createState() =>
     _BouncingAnimationWidgetState();
}
 
class _BouncingAnimationWidgetState extends State<BouncingAnimationWidget>
   with SingleTickerProviderStateMixin {
 late final AnimationController _controller;
 late final Animation<double> _boxJumpHeight;
 late final Animation<double> _boxWidth;
 late final Animation<double> _boxShadowWidth;
 late final Animation<double> _boxShadowIntensity;
 late final Animation<double> _boxRotationAngle;
 
 @override
 void initState() {
   super.initState();
   _controller = AnimationController(
     duration: const Duration(milliseconds: 500),
     vsync: this,
   );
   _initJumpAnimation();
   _initBoxWidthAnimation();
   _initBoxShadowWidthAnimation();
   _initBoxShadowIntensityAnimation();
   _initBoxRotationAnimation();
 }
 // Insert init functions from the last paragraph here
 @override
 Widget build(BuildContext context) => AnimatedBuilder(
       builder: (context, _) => _buildAnimation(context),
       animation: _controller,
     );
 
 Widget _buildAnimation(BuildContext context) => GestureDetector(
       onTap: _playAnimation,
       child: Stack(
         alignment: Alignment.center,
         children: [
           _boxShadow(context),
           Align(
             alignment: Alignment(0.0, _boxJumpHeight.value),
             child: _animatedBox(context),
           ),
         ],
       ),
     );
 
 Future<void> _playAnimation() async {
   try {
     await _controller.forward().orCancel;
     await _controller.reverse().orCancel;
   } on TickerCanceled {
     // the animation got canceled
   }
 }
 
 Widget _boxShadow(BuildContext context) => Container(
       width: _boxShadowWidth.value,
       height: 15,
       decoration: BoxDecoration(
         borderRadius:
             BorderRadius.all(Radius.elliptical(_boxShadowWidth.value, 15)),
         boxShadow: [
           BoxShadow(
             color: Colors.black.withOpacity(_boxShadowIntensity.value),
             spreadRadius: 5,
             blurRadius: 4,
             offset: const Offset(0, 3),
           ),
         ],
       ),
     );
 
 Widget _animatedBox(BuildContext context) => Transform(
       alignment: Alignment.center,
       transform: _boxRotation(_controller.status),
       child: Container(
         width: _boxWidth.value,
         height: 50,
         color: Colors.white,
       ),
     );
 
 Matrix4 _boxRotation(AnimationStatus animationStatus) {
   // This will ensure that rotation will be in the same direction on reverse
   if (animationStatus == AnimationStatus.reverse) {
     return Matrix4.identity()..rotateZ(-_boxRotationAngle.value * pi / 180);
   } else {
     return Matrix4.identity()..rotateZ(_boxRotationAngle.value * pi / 180);
   }
 }
 
 @override
 void dispose() {
   _controller.dispose();
   super.dispose();
 }
}

 

Po zmianach w statycznych wartościach oraz dodaniu rotacji do prostokąta otrzymujesz następujący efekt:

Animacje przy wykorzystaniu Rive

Jak sugerowałem wcześniej, animacje są wspaniałe i powinny być nieodłącznym elementem każdej aplikacji (w mniejszym lub większym stopniu).

Ale co, gdy klient wraz z designerem potrzebują bardzo skomplikowanej animacji? Przed Tobą rysują się dwie drogi – albo podejmiesz wyzwanie i zaimplementujesz animację w kodzie, albo skorzystasz z możliwości, jakie daje Rive.

Rive jest platformą do tworzenia interaktywnych animacji. Pozwala na umieszczanie ich bezpośrednio w aplikacjach napisanych m.in. we Flutterze, Swifcie czy Kotlinie. Korzystając z edytora dostępnego w przeglądarce, możesz tworzyć animacje i zaciągać je do aplikacji z internetu. Możesz też dodać je do assetów.

Samo narzędzie jest bardzo proste w obsłudze. W sieci, w tym również na samej platformie Rive, znajdziesz wiele poradników i przykładów animacji gotowych do użycia w swojej aplikacji.

Wykorzystanie animacji w aplikacji

Po zrobieniu animacji musisz skorzystać z paczki stworzonej przez twórców Rive. Po otwarciu nowego projektu i dodaniu do niego assetu utwórz widżet, który pobierze animację (z pliku albo z sieci) i ją zbuduje.

class HoldappLogoRiveWidget extends StatefulWidget {
 const HoldappLogoRiveWidget({Key? key}) : super(key: key);
 
 @override
 State<HoldappLogoRiveWidget> createState() => _HoldappLogoRiveWidgetState();
}
 
class _HoldappLogoRiveWidgetState extends State<HoldappLogoRiveWidget>
   with SingleTickerProviderStateMixin {
 Artboard? _riveArtboard;
 
 @override
 void initState() {
   super.initState();
   rootBundle.load('assets/holdapp_logo.riv').then((data) {
     // Load the RiveFile from the binary data.
     final file = RiveFile.import(data);
 
     // The artboard is the root of the animation
     // and gets drawn in the Rive widget.
     final artboard = file.mainArtboard;
     var controller =
         StateMachineController.fromArtboard(artboard, 'State Machine 1');
     if (controller != null) {
       artboard.addController(controller);
     }
     setState(() => _riveArtboard = artboard);
   });
 }
 
 @override
 Widget build(BuildContext context) => Scaffold(
       backgroundColor: Colors.white,
       body: _riveArtboard != null
           ? Rive(artboard: _riveArtboard!)
           : const Center(
               child: CircularProgressIndicator(),
             ),
     );
}

Jak widzisz, niewiele było trzeba, aby dodać animację do aplikacji, a wygląda ona tak:

Metody animacji

Warto zaznaczyć, że platforma wspiera trzy metody animacji:

  • One shot – jednorazowe wyświetlenie animacji,
  • Ping-Pong – nieskończone wyświetlanie animacji w sekwencji od początku od końca,
  • Loop – nieskończone wyświetlanie animacji od początku.

Rive pozwala również ingerować w sposób odtwarzania animacji z poziomu kodu. Dzięki rozwiązaniu, jakim jest state machine, animacje stworzone w Rive dają możliwość wpływania na to, którą z wcześniej zadeklarowanych animacji (bądź jej wariant) należy wykorzystać.

Przykładowo kliknięcie przycisku zmienia animację z nocy na taką, która powinna się odtwarzać za dnia. Jednocześnie zmienia jej działanie w state machine animacji (z dnia na noc). Innym przykładem będzie np. podążanie za kursorem i reagowanie na jego ruch, tak jak zostało to zaprezentowane tutaj.

Cały kod oraz plik z animacją dostępne są na Githubie.

Jeśli masz jakieś pytania albo szukasz zespołu, który doda animacje do Twojej aplikacji – napisz do nas. Porozmawiamy o Twoim projekcie i opowiemy, jak możemy Ci pomóc.

Dowiedz się więcej:

Marcel Kozień

Flutter Developer

Wycena projektu

Opowiedz nam o swoim projekcie i napisz, jak możemy Ci pomóc.

Dlaczego warto rozwijać z nami projekty?

Logo Mobile Trends Awards

Mobile Trends Awards 2021

Wygrana w kategorii
ŻYCIE CODZIENNE

Nagroda Legalnych Bukmacherów

Nagroda Legalnych Bukmacherów 2019

Najlepsza aplikacja mobilna

Mobile Trends Awards logo

Mobile Trends Awards 2020

Nominacja w kategorii
SPORT I REKREACJA

20

opinii klientów

Clutch logo