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. Was ist das Konzept der Mikrointeraktionen? 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: Schaltflächenanimationen: Die Schaltfläche ändert ihre Farbe oder Größe, wenn Sie mit der Maus darüber fahren oder darauf drücken. Ladeindikatoren: Animationen, die einem Benutzer anzeigen, dass ein Vorgang ausgeführt wird. Wischbewegungen: Animationen, die auf Wischbewegungen reagieren. Navigationsübergänge: Flüssige Animationen beim Übergang zwischen Bildschirmen. Mikrointeraktionen in realen Anwendungen 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. So erstellen Sie gestaffelte Animationen in Flutter 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 👇🏽 Voraussetzung Um dieses Tutorial optimal nutzen zu können, sollten Sie über Folgendes verfügen: Ein grundlegendes Verständnis davon, wie Animationen in Flutter erstellt werden Gute Kenntnisse der Flutter- und Dart-Grundlagen Ein Code-Editor, entweder VScode oder Android Studio Ein Emulator oder Gerät zum Aufbauen 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 ist auf zu finden. Hier wird der verwendet, um die aktuelle Seite in der Ansicht anzuzeigen. Sie können das Paket wie folgt zu Ihrer hinzufügen 👇🏽 SmoothPageIndicator pub.dev SmoothPageIndicator pubspec.yaml 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 nennen. Es verfügt über mehrere Parameter, mit denen sich steuern lässt, wie die Animation ablaufen soll. 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(); } Mit den Parametern von , wie sie im obigen Snippet zu sehen sind, können wir Folgendes erfolgreich steuern: AnimateWidget Dauer der Animation. Der sorgt dafür, dass die Animation beim Wischen über die Seite ausgelöst wird. pageController Der Betrag des horizontalen Versatzes des animierten Elements. Als nächstes definieren wir in Folgendes: AnimateWiget : Wird zur Steuerung der Animationssequenz verwendet. AnimationController 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: Wir haben verwendet, um die Animationssequenz zu steuern, deshalb haben wir eingeführt. AnimationController SingleTickerProviderStateMixin In der Methode haben wir den initialisiert und dann die Animation mit “ beim Seitenaufruf und beim Seitenwischen ausgelöst. init state AnimationController animationController.forward Wir haben den Timer verwendet, um das Auslösen der Animation basierend auf der Verzögerung zu steuern. In der -Methode haben wir Ressourcen bereinigt. dispose Wir haben verwendet, um den Status des Widgets beizubehalten. Dies soll verhindern, dass der gelöscht wird, wenn eine Seite durchgewischt wird und nicht mehr sichtbar ist. AutomaticKeepAliveClientMixin AnimationController In der Funktion 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. getDelay In der Build-Methode geben wir einen zurück. In diesem animierten Builder geben wir eine Widget-Funktion namens zurück, die ein -Widget zurückgibt. In der Funktion haben wir die Funktion. AnimatedBuilder _slideAnimation Transform.translate _slideAnimation offsetAnimation Die Funktion gibt die Animationseigenschaft zurück, die im Widget verwendet wird. Das Widget animiert das untergeordnete Widget unter Verwendung des Werts aus der Animation. offsetAnimation 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, ); } Dies ist der vollständige Code für die Klasse 👇🏽 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, ); } } Um diese Klasse als Nächstes in einem Spalten-Widget zu verwenden, erstellen wir eine Klasse mit einer statischen Methode namens , die eine Liste zurückgibt. In dieser Methode übergeben wir alle erforderlichen Parameter, einschließlich einer Liste . An den Parameter übergeben wir die Liste der Elemente, die wir animieren werden. AnimateWidget toStaggeredList children children 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 übergeben wir die erforderlichen Parameter, um jedes untergeordnete Element in der Liste erfolgreich zu animieren. Mit der Methode können wir es jetzt auf den beiden Seiten implementieren, an denen wir arbeiten. 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), ], ), ), ], ), ), ); } } In den untergeordneten Elementen des Spalten-Widgets übergeben wir die 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 einsehen. AnimateList.toStaggeredList hier Dies ist unser Endergebnis: Abschluss 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. Verweise Flutter Staggered Animation-Paket