Animarea aplicațiilor cu flutter
Publicat: 2022-03-10Aplicațiile pentru orice platformă sunt lăudate atunci când sunt intuitive, arătoase și oferă feedback plăcut interacțiunilor utilizatorilor. Animația este una dintre modalitățile de a face tocmai asta.
Flutter, un cadru multiplatform, sa maturizat în ultimii doi ani pentru a include suport web și desktop. Și-a câștigat reputația că aplicațiile dezvoltate cu el sunt netede și arătoase. Cu suportul bogat de animație, modul declarativ de a scrie UI, „Hot Reload” și alte caracteristici, este acum un cadru multiplatform complet.
Dacă începi cu Flutter și vrei să înveți un mod neconvențional de a adăuga animație, atunci ești la locul potrivit: vom explora tărâmul animației și al widget-urilor de mișcare, o modalitate implicită de a adăuga animații.
Flutter se bazează pe conceptul de widget-uri. Fiecare componentă vizuală a unei aplicații este un widget - gândiți-vă la ele ca vizualizări în Android. Flutter oferă suport pentru animație folosind o clasă Animation, un obiect „AnimationController” pentru gestionare și „Tween” pentru a interpola intervalul de date. Aceste trei componente lucrează împreună pentru a oferi o animație fluidă. Deoarece aceasta necesită crearea manuală și gestionarea animației, este cunoscută ca o modalitate explicită de animare.
Acum permiteți-mi să vă prezint widget-urile de animație și mișcare. Flutter oferă numeroase widget-uri care susțin în mod inerent animația. Nu este nevoie să creați un obiect de animație sau orice controler, deoarece toată animația este gestionată de această categorie de widget-uri. Doar alegeți widgetul potrivit pentru animația necesară și transmiteți valorile proprietăților widgetului pentru a anima. Această tehnică este un mod implicit de animare.

Graficul de mai sus prezintă aproximativ ierarhia animației în Flutter, modul în care sunt acceptate atât animația explicită, cât și cea implicită.
Unele dintre widget-urile animate abordate în acest articol sunt:
-
AnimatedOpacity
-
AnimatedCrossFade
-
AnimatedAlign
-
AnimatedPadding
-
AnimatedSize
-
AnimatedPositioned
.
Flutter oferă nu numai widget-uri animate predefinite, ci și un widget generic numit AnimatedWidget
, care poate fi folosit pentru a crea widget-uri personalizate implicit animate. După cum reiese din nume, aceste widget-uri aparțin categoriei de widget-uri animate și în mișcare și, prin urmare, au unele proprietăți comune care ne permit să facem animații mult mai netede și mai arate.
Permiteți-mi să explic acum aceste proprietăți comune, deoarece vor fi folosite mai târziu în toate exemplele.
-
duration
Durata pe care se vor anima parametrii. -
reverseDuration
Durata animației inverse. -
curve
Curba de aplicat la animarea parametrilor. Valorile interpolate pot fi luate dintr-o distribuție liniară sau, dacă și când sunt specificate, pot fi luate dintr-o curbă.
Să începem călătoria creând o aplicație simplă pe care o vom numi „Citat”. Va afișa o cotație aleatorie de fiecare dată când pornește aplicația. Două lucruri de reținut: în primul rând, toate aceste citate vor fi codificate în aplicație; iar în al doilea rând, nu vor fi salvate date despre utilizator.
Notă : Toate fișierele pentru aceste exemple pot fi găsite pe GitHub.
Noțiuni de bază
Flutter ar trebui să fie instalat și veți avea nevoie de puțină familiaritate cu fluxul de bază înainte de a continua. Un loc bun pentru a începe este „Folosirea Flutterului Google pentru o dezvoltare mobilă cu adevărat multiplatformă”.
Creați un nou proiect Flutter în Android Studio.

Aceasta va deschide un nou expert pentru proiect, unde puteți configura elementele de bază ale proiectului.

În ecranul de selecție a tipului de proiect, există diferite tipuri de proiecte Flutter, fiecare găzduind un scenariu specific.. Pentru acest tutorial, alegeți Aplicația Flutter și apăsați Next .
Acum trebuie să introduceți câteva informații specifice proiectului: numele și calea proiectului, domeniul companiei și așa mai departe. Aruncă o privire la imaginea de mai jos.

Adăugați numele proiectului, calea SDK Flutter, locația proiectului și o descriere opțională a proiectului. Apăsați Următorul .

Fiecare aplicație (fie că este Android sau iOS) necesită un nume de pachet unic. De obicei, utilizați reversul domeniului site-ului dvs.; de exemplu, com.google sau com.yahoo. Apăsați Terminare pentru a genera o aplicație Flutter funcțională.

Odată ce proiectul este generat, ar trebui să vedeți ecranul prezentat mai sus. Deschideți fișierul main.dart (evidențiat în captură de ecran). Acesta este fișierul principal al aplicației. Proiectul eșantion este complet în sine și poate fi rulat direct pe un emulator sau un dispozitiv fizic fără nicio modificare.
Înlocuiți conținutul fișierului main.dart cu următorul fragment de cod:
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(), ); } }
Acest cod curăță fișierul main.dart adăugând doar informații simple relevante pentru crearea unei noi aplicații. Clasa MyApp returnează un obiect: un widget MaterialApp
, care oferă structura de bază pentru crearea de aplicații conform Material Design. Pentru a face codul mai structurat, creați două fișiere dart noi în folderul lib : FirstPage.dart și Quotes.dart .

FirstPage.dart va conține tot codul responsabil pentru toate elementele vizuale (widgeturi) necesare pentru aplicația noastră Quoted. Toată animația este gestionată în acest fișier.
Notă : mai târziu în articol, toate fragmentele de cod pentru fiecare widget animat sunt adăugate la acest fișier ca copii ale widget-ului Scaffold. Pentru mai multe informații, acest exemplu pe GitHub ar putea fi util.
Începeți prin a adăuga următorul cod la FirstPage.dart . Acesta este codul parțial în care alte lucruri vor fi adăugate mai târziu.
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. ], ), ); } }

Fișierul Quotes.dart conține o listă cu toate citatele codificate. Un punct de remarcat aici este că lista este un obiect static. Aceasta înseamnă că poate fi folosit în alte locuri fără a crea un nou obiect al clasei Citate. Acesta este ales prin proiectare, deoarece lista de mai sus acționează pur și simplu ca o utilitate.

Adăugați următorul cod la acest fișier:
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." ]; }
Scheletul proiectului este acum gata, așa că haideți să dezvăluim un pic mai mult Quoted.
AnimatedOpacity
Pentru a da o notă personală aplicației, ar fi bine să cunoaștem numele utilizatorului, așa că haideți să-l cerem și să arătăm un buton următor. Până când utilizatorul își introduce numele, acest buton este ascuns și va apărea cu grație atunci când este dat un nume. Avem nevoie de un fel de animație de vizibilitate pentru buton, dar există un widget pentru asta? Da este.
Introduceți AnimatedOpacity
. Acest widget se bazează pe widgetul Opacitate adăugând suport implicit pentru animație. Cum îl folosim? Amintiți-vă scenariul nostru: trebuie să arătăm următorul buton cu vizibilitate animată. Încapsulăm widgetul butonului în widget-ul AnimatedOpacity
, introducem câteva valori adecvate și adăugăm o condiție pentru a declanșa animația - iar Flutter se poate ocupa de restul.
_getAnimatedOpacityButton() { return AnimatedOpacity( duration: Duration(seconds: 1), reverseDuration: Duration(seconds: 1), curve: Curves.easeInOut, opacity: showNextButton ? 1 : 0, child: _getButton(), ); }

Widgetul AnimatedOpacity
are două proprietăți obligatorii:
-
opacity
O valoare de 1 înseamnă complet vizibil; 0 (zero) înseamnă ascuns. În timpul animației, Flutter interpolează valori între aceste două extreme. Puteți vedea cum este plasată o condiție pentru a modifica vizibilitatea, declanșând astfel animația. -
child
Widgetul copil care va avea vizibilitatea animată.
Acum ar trebui să înțelegeți cât de simplu este să adăugați animație de vizibilitate cu widget-ul implicit. Și toate astfel de widget-uri urmează aceleași linii directoare și sunt ușor de utilizat. Să trecem la următorul.
AnimatedCrossFade
Avem numele utilizatorului, dar widget-ul încă așteaptă intrare. În pasul anterior, pe măsură ce utilizatorul își introduce numele, afișăm butonul următor. Acum, când utilizatorul apasă butonul, vreau să nu mai accept intrarea și să arăt numele introdus. Există multe modalități de a face acest lucru, desigur, dar poate putem ascunde widget-ul de intrare și arăta un widget text care nu poate fi editat. Să-l încercăm folosind widget-ul AnimatedCrossFade
.
Acest widget necesită doi copii, deoarece widget-ul trece încrucișat între ei în funcție de anumite condiții. Un lucru important de reținut atunci când utilizați acest widget este că ambii copii ar trebui să aibă aceeași lățime. Dacă înălțimea este diferită, atunci widgetul mai înalt este tăiat din partea de jos. În acest scenariu, două widget-uri vor fi folosite ca copii: intrare și etichetă.
_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, ); }

Acest widget necesită un set diferit de parametri obligatorii:
-
crossFadeState
Această stare stabilește ce copil să arate. -
firstChild
Specifică primul copil pentru acest widget. -
secondChild
Specifică al doilea copil.
AnimatedAlign
În acest moment, eticheta de nume este poziționată în centrul ecranului. Va arăta mult mai bine în partea de sus, deoarece avem nevoie de centrul ecranului pentru a afișa ghilimele. Mai simplu spus, alinierea widget-ului etichetei numelui ar trebui schimbată de la centru în sus. Și nu ar fi frumos să animați această schimbare de aliniere împreună cu animația anterioară cu decolorare încrucișată? S-o facem.
Ca întotdeauna, mai multe tehnici pot fi folosite pentru a realiza acest lucru. Deoarece widget-ul cu etichetă de nume este deja aliniat la centru, animarea alinierii acestuia ar fi mult mai simplă decât manipularea valorilor de sus și stânga ale widget-ului. Widgetul AnimatedAlign
este perfect pentru acest job.
Pentru a iniția această animație, este necesar un declanșator. Singurul scop al acestui widget este de a anima schimbarea alinierii, deci are doar câteva proprietăți: adăugați un copil, setați alinierea acestuia, declanșați schimbarea de aliniere și atât.
_getAnimatedAlignWidget() { return AnimatedAlign( duration: Duration(seconds: 1), curve: Curves.easeInOut, alignment: alignTop ? Alignment.topLeft : Alignment.center, child: _getAnimatedCrossfade(), ); }

Are doar două proprietăți obligatorii:
- copil:
Copilul căruia i se va modifica aliniamentul. - aliniere:
Valoarea de aliniere necesară.
Acest widget este foarte simplu, dar rezultatele sunt elegante. Mai mult, am văzut cât de ușor putem folosi două widget-uri animate diferite pentru a crea o animație mai complexă. Aceasta este frumusețea widget-urilor animate.
AnimatedPadding
Acum avem numele utilizatorului în partea de sus, animat fără prea mult efort, folosind diferite tipuri de widget-uri animate. Să adăugăm un salut, „Bună”, înaintea numelui. Adăugarea unui widget text cu valoarea „Bună”, în partea de sus, îl va face să se suprapună cu widgetul text de salut, arătând ca imaginea de mai jos.

Ce se întâmplă dacă widget-ul text al numelui ar avea o umplutură în partea stângă? Creșterea umpluturii din stânga va funcționa cu siguranță, dar așteptați: putem crește umplutura cu ceva animație? Da, și asta face AnimatedPadding
. Pentru ca toate acestea să arate mult mai bine, să facem ca widgetul de text de salut să se estompeze și să crească în același timp umplutura pentru textul cu nume.
_getAnimatedPaddingWidget() { return AnimatedPadding( duration: Duration(seconds: 1), curve: Curves.fastOutSlowIn, padding: increaseLeftPadding ? EdgeInsets.only(left: 28.0) : EdgeInsets.only(left: 0), child: _getAnimatedCrossfade(), ); }
Deoarece animația de mai sus ar trebui să apară numai după ce alinierea animată anterioară este finalizată, trebuie să amânăm declanșarea acestei animații. Abaterea de la subiect pe scurt, acesta este un moment bun pentru a vorbi despre un mecanism popular pentru a adăuga întârziere. Flutter oferă mai multe astfel de tehnici, dar constructorul Future.delayed este una dintre abordările mai simple, mai curate și mai lizibile. De exemplu, pentru a executa o bucată de cod după 1 secundă:
Future.delayed(Duration(seconds: 1), (){ sum = a + b; // This sum will be calculated after 1 second. print(sum); });
Deoarece durata de întârziere este deja cunoscută (calculată din duratele anterioare ale animației), animația poate fi declanșată după acest interval.
// 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; }); }); }

Acest widget are doar două proprietăți obligatorii:
-
child
Copilul din acest widget, căruia i se va aplica umplutura. -
padding
Cantitatea de spațiu de adăugat.
AnimatedSize
Astăzi, orice aplicație care are un fel de animație va include mărirea sau micșorarea componentelor vizuale pentru a atrage atenția utilizatorului (denumită în mod obișnuit animație de scalare). De ce să nu folosești aceeași tehnică aici? Putem arăta utilizatorului un citat motivațional care mărește din centrul ecranului. Permiteți-mi să vă prezint widget-ul AnimatedSize
, care permite efectele de mărire și micșorare, controlate prin schimbarea dimensiunii copilului său.
Acest widget este puțin diferit de celelalte când vine vorba de parametrii necesari. Avem nevoie de ceea ce Flutter numește „Ticker”. Flutter are o metodă pentru a informa obiectele ori de câte ori este declanșat un nou eveniment cadru. Poate fi considerat ca ceva care trimite un semnal care spune: „Fă-o acum! … Fa-o acum! … Fa-o acum! …”
Widgetul AnimatedSize
necesită o proprietate — vsync — care acceptă un furnizor de ticker. Cel mai simplu mod de a obține un furnizor de ticker este să adăugați un Mixin la clasă. Există două implementări de bază ale furnizorului de ticker: SingleTickerProviderStateMixin
, care oferă un singur ticker; și TickerProviderStateMixin
, care oferă mai multe.
Implementarea implicită a unui Ticker este utilizată pentru a marca cadrele unei animații. În acest caz, acesta din urmă este angajat. Mai multe despre mixinuri.
// 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; }); }); }

Proprietăți obligatorii pentru acest widget:
-
vsync
Furnizorul de ticker necesar pentru a coordona animația și modificările cadrelor.< -
child
Copilul a cărui mărime se modifică va fi animat.
Animația de mărire și micșorare este acum ușor de îmblânzit.
AnimatedPositioned
Grozav! Citatele se mărit din centru pentru a atrage atenția utilizatorului. Ce se întâmplă dacă a alunecat în sus de jos în timp ce măriți? Hai sa incercam. Această mișcare implică jocul cu poziția widgetului citat și animarea modificărilor proprietăților poziției. AnimatedPositioned
este candidatul perfect.
Acest widget face tranziție automată poziția copilului pe o anumită durată ori de câte ori poziția specificată se schimbă. Un punct de remarcat: funcționează numai dacă widgetul său părinte este o „stivă”. Acest widget este destul de simplu și ușor de utilizat. Să vedem.
// 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, ); }

Acest widget are o singură proprietate obligatorie:
-
child
Widgetul a cărui poziție va fi schimbată.
Dacă dimensiunea copilului nu este de așteptat să se schimbe odată cu poziția sa, o alternativă mai performativă la acest widget ar fi SlideTransition
.
Iată animația noastră completă:

Concluzie
Animațiile sunt o parte integrantă a experienței utilizatorului. Aplicațiile statice sau aplicațiile cu animație neplăcută nu numai că reduc reținerea utilizatorilor, ci și reputația unui dezvoltator de a oferi rezultate.
Astăzi, cele mai populare aplicații au un fel de animație subtilă pentru a încânta utilizatorii. Feedback-ul animat la solicitările utilizatorilor îi poate implica și pe aceștia să exploreze mai multe. Flutter oferă o mulțime de funcții pentru dezvoltarea multiplatformă, inclusiv suport bogat pentru animații fluide și receptive.
Flutter are un suport excelent pentru plug-in, care ne permite să folosim animații de la alți dezvoltatori. Acum că s-a maturizat la versiunea 1.9, cu atâta dragoste din partea comunității, Flutter este obligat să se îmbunătățească în viitor. Aș spune că acum este un moment grozav pentru a învăța Flutter!
Resurse suplimentare
- „Widget-uri de animație și mișcare”, Flutter Docs
- „Introducere în animații”, Flutter Docs
- Flutter Codelabs
Nota editorului : Mulțumiri imense lui Ahmad Awais pentru ajutorul acordat în revizuirea acestui articol.