Eine gestaffelte Animation besteht aus aufeinanderfolgenden oder sich überlappenden Animationen. Die Animation kann rein sequentiell sein, wobei eine Änderung nach der anderen erfolgt, oder sie kann sich teilweise oder vollständig überlappen. Bei einer sequentiellen Animation werden die Elemente nacheinander mit einer leichten Verzögerung zwischen den Startzeiten der einzelnen Elemente animiert. Dadurch entsteht ein Kaskaden- oder Welleneffekt, bei dem die Animation die Elemente scheinbar schrittweise und nicht auf einmal durchläuft.
Versetzte Animationen gelten als eine Art Mikrointeraktion, da sie das Benutzererlebnis verbessern, indem sie subtiles, interaktives Feedback liefern, das den Benutzer durch eine Benutzeroberfläche führt. In Flutter können Sie Mikrointeraktionen erstellen, indem Sie subtile Animationen erstellen, indem Sie entweder die implizite oder explizite Animation verwenden.
Implizite Animationen sind so konzipiert, dass sie einfach und benutzerfreundlich sind, da die Animationsdetails abstrahiert werden, während explizite Animationen besser für komplexe Animationen geeignet sind, da sie eine vollständige Kontrolle über den Animationsprozess bieten. Dies wird insbesondere dann verwendet, wenn Sie eine feinere Kontrolle über die Animation benötigen.
In diesem Artikel werden wir uns eingehend mit dem Konzept der Mikrointeraktionen befassen. Anschließend werden wir für einen Anwendungsfall von Mikrointeraktionen mithilfe expliziter Animationen eine gestaffelte Animation erstellen, die die in einem Spalten-Widget enthaltenen untergeordneten Elemente animiert.
Tolle Produkte sind Produkte, die sowohl hinsichtlich der Funktionen als auch der Details überzeugen. Funktionen ziehen Menschen zu Ihren Produkten, aber Details halten sie. Diese Details sind Dinge, die Ihre App von anderen abheben. Mit Mikrointeraktionen können Sie diese Details schaffen, indem Sie Ihren Benutzern erfreuliches Feedback geben.
Das Konzept der Mikrointeraktionen basiert auf der Idee, dass kleine Details einen großen Einfluss auf das gesamte Benutzererlebnis haben können. Mikrointeraktionen können für wichtige Funktionen wie die Übermittlung von Feedback oder das Ergebnis einer Aktion verwendet werden. Beispiele für Mikrointeraktionen sind:
Unten sehen wir reale Anwendungen von Mikrointeraktionen. Diese subtilen Animationen werden in Flutter erstellt, um das Benutzererlebnis zu verbessern. 👇🏽
Designreferenzen für diese Apps wurden von Dribbble bezogen.
In diesem Beispiel erstellen wir eine gestaffelte Animation, die die untergeordneten Elemente in einem Spalten-Widget animiert, wenn über diese Seite gewischt wird. Dies wird mit dem expliziten Animationsansatz erstellt, da wir in diesem Fall die volle Kontrolle darüber haben müssen, wie die Animation ablaufen soll.
So wird die Animation aussehen, wenn wir fertig sind 👇🏽
Um dieses Tutorial optimal nutzen zu können, sollten Sie über Folgendes verfügen:
Nachdem Sie alle Voraussetzungen des Projekts überprüft haben, können wir loslegen.
Zuerst erstellen wir den Hauptbildschirm, der diese beiden Seiten enthält. Diese beiden Seiten werden in ein Seitenansichts-Widget eingebettet, das steuert, welche Seite beim Wischen angezeigt wird. Außerdem haben wir unten auf dem Hauptbildschirm einen Indikator, der uns die Seite anzeigt, auf der wir uns gerade befinden.
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, ), ), ) ); } }
Der im obigen Codeausschnitt verwendete SmoothPageIndicator
ist auf pub.dev zu finden. Hier wird der SmoothPageIndicator
verwendet, um die aktuelle Seite in der Ansicht anzuzeigen. Sie können das Paket wie folgt zu Ihrer pubspec.yaml
hinzufügen 👇🏽
dependencies: flutter: sdk: flutter smooth_page_indicator: ^1.1.0
Auf den beiden Seiten haben wir ein Spalten-Widget mit mehreren leeren Karten-Widgets. Diese leeren Karten-Widgets werden verwendet, um die Spalte zu füllen, damit wir die gestaffelte Animation vollständig sehen und würdigen können. Wir erstellen das leere Karten-Widget als wiederverwendbare Komponente, damit wir es nicht an jeder Stelle, an der wir es verwenden, von Grund auf neu erstellen müssen.
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), ) ] ), ); } }
Nachdem wir alle diese Boilerplate-Codes aus dem Weg geräumt haben, beginnen wir nun mit dem Erstellen der Animation. Zuerst erstellen wir ein neues Stateful-Widget, das wir AnimateWidget
nennen. Es verfügt über mehrere Parameter, mit denen sich steuern lässt, wie die Animation ablaufen soll.
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(); }
Mit den Parametern von AnimateWidget
, wie sie im obigen Snippet zu sehen sind, können wir Folgendes erfolgreich steuern:
pageController
sorgt dafür, dass die Animation beim Wischen über die Seite ausgelöst wird.
Als nächstes definieren wir in AnimateWiget
Folgendes:
AnimationController
: Wird zur Steuerung der Animationssequenz verwendet.
Aktuelle Seite: wird verwendet, um die aktuellen Seitendaten zu speichern.
Timer: Wird verwendet, um die Animation nach einer gewissen Verzögerung auszulösen. Dadurch entsteht der gestaffelte Kaskadeneffekt der Animation.
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()); }
Wenn Sie den obigen Codeausschnitt analysieren, werden Sie Folgendes feststellen:
AnimationController
verwendet, um die Animationssequenz zu steuern, deshalb haben wir SingleTickerProviderStateMixin
eingeführt.
init state
haben wir den AnimationController
initialisiert und dann die Animation mit animationController.forward
“ beim Seitenaufruf und beim Seitenwischen ausgelöst.
dispose
-Methode haben wir Ressourcen bereinigt.
AutomaticKeepAliveClientMixin
verwendet, um den Status des Widgets beizubehalten. Dies soll verhindern, dass der AnimationController
gelöscht wird, wenn eine Seite durchgewischt wird und nicht mehr sichtbar ist.
In der Funktion getDelay
haben wir die Verzögerung berechnet, bevor die Animation für jedes Element ausgelöst wurde. Dazu haben wir die Dauer in Millisekunden durch 6 geteilt, die Ergebnisse angenähert und sie dann mit der Position multipliziert. Dies ist die Position (in diesem Fall der Index) des Elements.
In der Build-Methode geben wir einen AnimatedBuilder
zurück. In diesem animierten Builder geben wir eine Widget-Funktion namens _slideAnimation
zurück, die ein Transform.translate
-Widget zurückgibt. In der _slideAnimation
Funktion haben wir die offsetAnimation
Funktion.
Die Funktion offsetAnimation
gibt die Animationseigenschaft zurück, die im Widget Transform.translate
verwendet wird. Das Widget Transform.translate
animiert das untergeordnete Widget unter Verwendung des Werts aus der Animation.
@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, ); }
Dies ist der vollständige Code für die AnimateWidget
Klasse 👇🏽
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, ); } }
Um diese AnimateWidget
Klasse als Nächstes in einem Spalten-Widget zu verwenden, erstellen wir eine Klasse mit einer statischen Methode namens toStaggeredList
, die eine Liste zurückgibt. In dieser Methode übergeben wir alle erforderlichen Parameter, einschließlich einer Liste children
. An den Parameter children
übergeben wir die Liste der Elemente, die wir animieren werden.
Als Nächstes ordnen wir untergeordnete Elemente zu und umschließen jedes untergeordnete Element mit dem 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(); }
Im AnimateWidget
übergeben wir die erforderlichen Parameter, um jedes untergeordnete Element in der Liste erfolgreich zu animieren. Mit der Methode AnimateList.toStaggeredList
können wir es jetzt auf den beiden Seiten implementieren, an denen wir arbeiten.
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), ], ), ), ], ), ), ); } }
In den untergeordneten Elementen des Spalten-Widgets übergeben wir die AnimateList.toStaggeredList
und die erforderlichen Parameter, einschließlich der Widgets, die in der Spalte angezeigt werden sollen. Damit haben wir erfolgreich eine gestaffelte Animation erstellt, die beim Wischen ausgelöst wird. Den vollständigen Code können Sie hier einsehen.
Dies ist unser Endergebnis:
Wir sind am Ende dieses Tutorials angelangt. An diesem Punkt haben wir das Konzept der Mikrointeraktionen und ihre Auswirkungen auf die Benutzererfahrung behandelt. Wir haben auch den Prozess der Erstellung einer gestaffelten Animation von Elementen in einem Spalten-Widget durchlaufen, das beim Wischen auf der Seite ausgelöst wird.
Es gibt so viele Arten von Mikrointeraktionen, die Sie Ihrer App hinzufügen können, um die Benutzererfahrung Ihres Projekts zu verbessern; Sie können experimentieren, indem Sie in Flutter interaktivere Animationen erstellen.
Wenn Sie diesen Artikel hilfreich fanden, können Sie ihn unterstützen, indem Sie ein „Gefällt mir“ oder einen Kommentar hinterlassen. Sie können mir auch folgen, um weitere verwandte Artikel zu erhalten.