Animationen in Flutter

1. Einführung

Animationen sind eine gute Möglichkeit, die Nutzerfreundlichkeit Ihrer App zu verbessern, wichtige Informationen zu vermitteln und Ihre App ansprechender und nutzerfreundlicher zu gestalten.

Übersicht über das Animationsframework von Flutter

Flutter zeigt Animationseffekte an, indem in jedem Frame ein Teil des Widget-Baums neu erstellt wird. Es bietet vorgefertigte Animationseffekte und andere APIs, die das Erstellen und Komponieren von Animationen erleichtern.

  • Implizite Animationen sind vordefinierte Animationseffekte, bei denen die gesamte Animation automatisch ausgeführt wird. Wenn sich der Zielwert der Animation ändert, wird die Animation vom aktuellen Wert zum Zielwert ausgeführt und alle dazwischen liegenden Werte werden angezeigt, damit das Widget flüssig animiert wird. Beispiele für implizite Animationen sind AnimatedSize, AnimatedScale und AnimatedPositioned.
  • Explizite Animationen sind ebenfalls vordefinierte Animationen, die jedoch ein Animation-Objekt erfordern. Beispiele: SizeTransition, ScaleTransition oder PositionedTransition.
  • Animation ist eine Klasse, die eine laufende oder angehaltene Animation darstellt. Sie besteht aus einem Wert, der den Zielwert der Animation darstellt, und dem Status, der den aktuellen Wert darstellt, der von der Animation zu einem bestimmten Zeitpunkt auf dem Bildschirm angezeigt wird. Es ist eine Unterklasse von Listenable und benachrichtigt seine Listener, wenn sich der Status während der Ausführung der Animation ändert.
  • Mit AnimationController können Sie eine Animation erstellen und ihren Status steuern. Mithilfe von Methoden wie forward(), reset(), stop() und repeat() können Sie die Animation steuern, ohne den angezeigten Animationseffekt wie Skalierung, Größe oder Position definieren zu müssen.
  • Mit Tweens werden Werte zwischen einem Anfangs- und einem Endwert interpoliert. Sie können jeden Typ haben, z. B. „double“, Offset oder Color.
  • Mit Kurven lässt sich die Änderungsrate eines Parameters im Zeitverlauf anpassen. Wenn eine Animation ausgeführt wird, wird häufig eine Easing-Kurve angewendet, um die Änderungsrate zu Beginn oder am Ende der Animation schneller oder langsamer zu gestalten. Kurven nehmen einen Eingabewert zwischen 0,0 und 1,0 an und geben einen Ausgabewert zwischen 0,0 und 1,0 zurück.

Umfang

In diesem Codelab erstellen Sie ein Quizspiel mit Multiple-Choice-Fragen, das verschiedene Animationseffekte und ‑techniken enthält.

3026390ad413769c.gif

Sie erfahren, wie Sie…

  • Widget erstellen, das Größe und Farbe animiert
  • 3D-Kartenumdrehen-Effekt erstellen
  • Ausgefallene vorgefertigte Animationseffekte aus dem Animationspaket verwenden
  • Unterstützung für die vorhersagende Zurück-Geste hinzufügen, die in der neuesten Android-Version verfügbar ist

Aufgaben in diesem Lab

In diesem Codelab lernen Sie Folgendes:

  • Wie Sie mit implizit animierten Effekten ansprechende Animationen erstellen, ohne viel Code schreiben zu müssen.
  • Wie Sie mit ausdrücklich animierten Effekten eigene Effekte mit vorgefertigten animierten Widgets wie AnimatedSwitcher oder AnimationController konfigurieren.
  • So definieren Sie mit AnimationController ein eigenes Widget mit einem 3D-Effekt.
  • Wie Sie mit dem animations-Paket mit minimaler Einrichtung ausgefallene Animationseffekte anzeigen.

Voraussetzungen

  • Das Flutter SDK
  • Eine IDE wie VSCode oder Android Studio / IntelliJ

2. Flutter-Entwicklungsumgebung einrichten

Für dieses Lab benötigen Sie zwei Softwareprogramme: das Flutter SDK und einen Editor.

Sie können das Codelab auf einem der folgenden Geräte ausführen:

  • Ein physisches Android (für die Implementierung der Vorhersagefunktion in Schritt 7 empfohlen) oder iOSGerät, das mit Ihrem Computer verbunden und auf den Entwicklermodus gesetzt ist.
  • Der iOS-Simulator (erfordert die Installation von Xcode-Tools).
  • Android-Emulator (erfordert Einrichtung in Android Studio)
  • Einen Browser (Chrome ist für die Fehlerbehebung erforderlich)
  • Sie haben einen Windows-, Linux- oder macOS-Computer. Sie müssen die Entwicklung auf der Plattform durchführen, auf der Sie die Bereitstellung planen. Wenn Sie also eine Windows-Desktopanwendung entwickeln möchten, müssen Sie die Entwicklung unter Windows durchführen, um auf die entsprechende Build-Kette zugreifen zu können. Es gibt betriebssystemspezifische Anforderungen, die unter docs.flutter.dev/desktop ausführlich beschrieben werden.

Installation prüfen

Mit dem Flutter Doctor-Tool können Sie prüfen, ob Ihr Flutter SDK richtig konfiguriert ist und mindestens eine der oben genannten Zielplattformen installiert ist:

$ flutter doctor

Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.24.2, on macOS 14.6.1 23G93 darwin-arm64, locale
    en)
[✓] Android toolchain - develop for Android devices
[✓] Xcode - develop for iOS and macOS
[✓] Chrome - develop for the web
[✓] Android Studio
[✓] IntelliJ IDEA Ultimate Edition
[✓] VS Code
[✓] Connected device (4 available)
[✓] Network resources

• No issues found!

3. Start-App ausführen

Auslöser-App herunterladen

Klonen Sie die Start-App mit git aus dem flutter/samples-Repository auf GitHub.

$ git clone https://github.com/flutter/codelabs.git
$ cd codelabs/animations/step_01/

Alternativ können Sie den Quellcode als ZIP-Datei herunterladen.

App ausführen

Verwenden Sie den Befehl flutter run, um die App auszuführen, und geben Sie ein Zielgerät wie android, ios oder chrome an. Eine vollständige Liste der unterstützten Plattformen finden Sie auf der Seite Unterstützte Plattformen.

$ flutter run -d android

Sie können die App auch mit der IDE Ihrer Wahl ausführen und beheben. Weitere Informationen finden Sie in der offiziellen Flutter-Dokumentation.

Code ansehen

Die Starter-App ist ein Multiple-Choice-Quizspiel mit zwei Bildschirmen, das dem Designmuster „Model View View Model“ (MVVM) folgt. Die QuestionScreen (Ansicht) verwendet die Klasse QuizViewModel (Ansichtsmodell), um dem Nutzer Multiple-Choice-Fragen aus der Klasse QuestionBank (Modell) zu stellen.

  • home_screen.dart: Zeigt einen Bildschirm mit der Schaltfläche Neues Spiel an.
  • main.dart: Konfiguriert die MaterialApp für die Verwendung von Material 3 und zeigt den Startbildschirm an.
  • model.dart: Hier werden die Hauptklassen definiert, die in der gesamten App verwendet werden.
  • question_screen.dart: Zeigt die Benutzeroberfläche für das Quiz an
  • view_model.dart: Speichert den Status und die Logik für das Quizspiel, die von der QuestionScreen angezeigt wird.

fbb1e1f7b6c91e21.png

Die App unterstützt noch keine animierten Effekte, mit Ausnahme des Standard-Ansichtsübergangs, der von der Navigator-Klasse von Flutter angezeigt wird, wenn der Nutzer auf die Schaltfläche Neues Spiel klickt.

4. Implizite Animationseffekte verwenden

Implizite Animationen sind in vielen Situationen eine gute Wahl, da sie keine spezielle Konfiguration erfordern. In diesem Abschnitt aktualisieren Sie das StatusBar-Widget, damit eine animierte Anzeigetafel angezeigt wird. Informationen zu gängigen impliziten Animationseffekten finden Sie in der API-Dokumentation für ImplicitlyAnimatedWidget.

206dd8d9c1fae95.gif

Widget für das unbewegliche Punkteboard erstellen

Erstellen Sie eine neue Datei vom Typ lib/scoreboard.dart mit dem folgenden Code:

lib/scoreboard.dart

import 'package:flutter/material.dart';

class Scoreboard extends StatelessWidget {
  final int score;
  final int totalQuestions;

  const Scoreboard({
    super.key,
    required this.score,
    required this.totalQuestions,
  });

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          for (var i = 0; i < totalQuestions; i++)
            Icon(
              Icons.star,
              size: 50,
              color:
                  score < i + 1 ? Colors.grey.shade400 : Colors.yellow.shade700,
            )
        ],
      ),
    );
  }
}

Fügen Sie dann das Scoreboard-Widget als untergeordnetes Element des StatusBar-Widgets hinzu und ersetzen Sie damit die Text-Widgets, in denen zuvor die Punktzahl und die Gesamtzahl der Fragen angezeigt wurden. Der erforderliche import "scoreboard.dart"-Befehl sollte oben in der Datei automatisch hinzugefügt werden.

lib/question_screen.dart

class StatusBar extends StatelessWidget {
  final QuizViewModel viewModel;

  const StatusBar({required this.viewModel, super.key});

  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 4,
      child: Padding(
        padding: EdgeInsets.all(8.0),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: [
            Scoreboard(                                        // NEW
              score: viewModel.score,                          // NEW
              totalQuestions: viewModel.totalQuestions,        // NEW
            ),
          ],
        ),
      ),
    );
  }
}

Dieses Widget zeigt für jede Frage ein Sternsymbol an. Wenn eine Frage richtig beantwortet wird, leuchtet sofort ohne Animation ein weiterer Stern auf. In den folgenden Schritten informieren Sie den Nutzer über die Änderung seiner Punktzahl, indem Sie Größe und Farbe animieren.

Einen impliziten Animationseffekt verwenden

Erstellen Sie ein neues Widget namens AnimatedStar, das ein AnimatedScale-Widget verwendet, um den scale-Wert von 0.5 in 1.0 zu ändern, wenn der Stern aktiv wird:

lib/scoreboard.dart

​​import 'package:flutter/material.dart';

class Scoreboard extends StatelessWidget {
  final int score;
  final int totalQuestions;

  const Scoreboard({
    super.key,
    required this.score,
    required this.totalQuestions,
  });

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          for (var i = 0; i < totalQuestions; i++)
            AnimatedStar(                                      // NEW
              isActive: score > i,                             // NEW
            )                                                  // NEW
        ],
      ),
    );
  }
}

class AnimatedStar extends StatelessWidget {                   // Add from here...
  final bool isActive;
  final Duration _duration = const Duration(milliseconds: 1000);
  final Color _deactivatedColor = Colors.grey.shade400;
  final Color _activatedColor = Colors.yellow.shade700;

  AnimatedStar({super.key, required this.isActive});

  @override
  Widget build(BuildContext context) {
    return AnimatedScale(
      scale: isActive ? 1.0 : 0.5,
      duration: _duration,
      child: Icon(
        Icons.star,
        size: 50,
        color: isActive ? _activatedColor : _deactivatedColor,
      ),
    );
  }
}                                                              // To here.

Wenn der Nutzer eine Frage richtig beantwortet, ändert das AnimatedStar-Widget seine Größe jetzt mithilfe einer impliziten Animation. Der color von Icon ist hier nicht animiert, nur der scale, was vom AnimatedScale-Widget übernommen wird.

84aec4776e70b870.gif

Mit einem Tween zwischen zwei Werten interpolieren

Die Farbe des AnimatedStar-Widgets ändert sich sofort, nachdem das Feld isActive auf „wahr“ gesetzt wurde.

Wenn Sie einen animierten Farbeffekt erzielen möchten, können Sie ein AnimatedContainer-Widget verwenden (eine weitere Unterklasse von ImplicitlyAnimatedWidget). Damit lassen sich alle Attribute, einschließlich der Farbe, automatisch animieren. Leider muss unser Widget ein Symbol und keinen Container anzeigen.

Sie können auch AnimatedIcon ausprobieren, mit dem Übergangseffekte zwischen den Formen der Symbole implementiert werden. Es gibt jedoch keine Standardimplementierung eines Sternsymbols in der Klasse AnimatedIcons.

Stattdessen verwenden wir eine andere Unterklasse von ImplicitlyAnimatedWidget namens TweenAnimationBuilder, die einen Tween als Parameter annimmt. Ein Tween ist eine Klasse, die zwei Werte (begin und end) annimmt und die Zwischenwerte berechnet, damit sie in einer Animation dargestellt werden können. In diesem Beispiel verwenden wir ein ColorTween, das die Tween<Color>-Schnittstelle erfüllt, die für die Erstellung des Animationseffekts erforderlich ist.

Wählen Sie das Icon-Widget aus und verwenden Sie die Schnellaktion „Mit Builder umschließen“ in Ihrer IDE. Ändern Sie den Namen in TweenAnimationBuilder. Geben Sie dann die Dauer und ColorTween an.

lib/scoreboard.dart

class AnimatedStar extends StatelessWidget {
  final bool isActive;
  final Duration _duration = const Duration(milliseconds: 1000);
  final Color _deactivatedColor = Colors.grey.shade400;
  final Color _activatedColor = Colors.yellow.shade700;

  AnimatedStar({super.key, required this.isActive});

  @override
  Widget build(BuildContext context) {
    return AnimatedScale(
      scale: isActive ? 1.0 : 0.5,
      duration: _duration,
      child: TweenAnimationBuilder(                            // Add from here...
        duration: _duration,
        tween: ColorTween(
          begin: _deactivatedColor,
          end: isActive ? _activatedColor : _deactivatedColor,
        ),
        builder: (context, value, child) {                     // To here.
          return Icon(
            Icons.star,
            size: 50,
            color: value,                                      // Modify from here...
          );
        },                                                     // To here.
      ),
    );
  }
}

Führen Sie jetzt einen Hot-Reload der App durch, um die neue Animation zu sehen.

8b0911f4af299a60.gif

Beachten Sie, dass sich der end-Wert von ColorTween je nach Wert des Parameters isActive ändert. Das liegt daran, dass TweenAnimationBuilder die Animation jedes Mal neu ausführt, wenn sich der Wert von Tween.end ändert. In diesem Fall wird die neue Animation vom aktuellen Animationswert bis zum neuen Endwert ausgeführt. So können Sie die Farbe jederzeit ändern (auch während der Animation) und einen flüssigen Animationseffekt mit den richtigen Zwischenwerten anzeigen.

Kurve anwenden

Beide Animationseffekte laufen mit einer konstanten Geschwindigkeit ab. Animationen sind jedoch oft visuell interessanter und informativer, wenn sie beschleunigt oder verlangsamt werden.

Bei einem Curve wird eine Ausblendungsfunktion angewendet, die die Änderungsrate eines Parameters im Zeitverlauf definiert. Flutter enthält eine Sammlung vordefinierter Glättungskurven in der Klasse Curves, z. B. easeIn oder easeOut.

5dabe68d1210b8a1.gif

3a9e7490c594279a.gif

Diese Diagramme (auf der Seite Curves API-Dokumentation verfügbar) geben einen Hinweis darauf, wie Kurven funktionieren. Mithilfe von Kurven wird ein Eingabewert zwischen 0,0 und 1,0 (auf der X-Achse) in einen Ausgabewert zwischen 0,0 und 1,0 (auf der Y-Achse) umgewandelt. Diese Diagramme zeigen auch eine Vorschau, wie verschiedene Animationseffekte aussehen, wenn eine Ease-Kurve verwendet wird.

Erstellen Sie in „AnimatedStar“ ein neues Feld namens _curve und übergeben Sie es als Parameter an die Widgets AnimatedScale und TweenAnimationBuilder.

lib/scoreboard.dart

class AnimatedStar extends StatelessWidget {
  final bool isActive;
  final Duration _duration = const Duration(milliseconds: 1000);
  final Color _deactivatedColor = Colors.grey.shade400;
  final Color _activatedColor = Colors.yellow.shade700;
  final Curve _curve = Curves.elasticOut;                       // NEW

  AnimatedStar({super.key, required this.isActive});

  @override
  Widget build(BuildContext context) {
    return AnimatedScale(
      scale: isActive ? 1.0 : 0.5,
      curve: _curve,                                           // NEW
      duration: _duration,
      child: TweenAnimationBuilder(
        curve: _curve,                                         // NEW
        duration: _duration,
        tween: ColorTween(
          begin: _deactivatedColor,
          end: isActive ? _activatedColor : _deactivatedColor,
        ),
        builder: (context, value, child) {
          return Icon(
            Icons.star,
            size: 50,
            color: value,
          );
        },
      ),
    );
  }
}

In diesem Beispiel sorgt die elasticOut-Kurve für einen übertriebenen Federeffekt, der mit einer Federbewegung beginnt und sich gegen Ende ausgleicht.

8f84142bff312373.gif

Führen Sie einen Hot-Reload der App durch, um zu sehen, wie diese Kurve auf AnimatedSize und TweenAnimationBuilder angewendet wird.

206dd8d9c1fae95.gif

Langsame Animationen mit den DevTools aktivieren

Zum Beheben von Animationseffekten können Sie mit den Flutter DevTools alle Animationen in Ihrer App verlangsamen, damit Sie sie besser sehen können.

Damit die DevTools geöffnet werden, muss die App im Debug-Modus ausgeführt werden. Öffnen Sie dann den Widget-Inspektor, indem Sie ihn in der Debug-Symbolleiste in VSCode auswählen oder die Schaltfläche Flutter DevTools öffnen im Debug-Toolfenster in IntelliJ / Android Studio anklicken.

3ce33dc01d096b14.png

363ae0fbcd0c2395.png

Klicken Sie in der Symbolleiste des Widget-Inspektors auf die Schaltfläche Animationen verlangsamen.

adea0a16d01127ad.png

5. Explizite Animationseffekte verwenden

Wie implizite Animationen sind auch explizite Animationen vordefinierte Animationseffekte. Statt eines Zielwerts wird jedoch ein Animation-Objekt als Parameter verwendet. Das ist in Situationen hilfreich, in denen die Animation bereits durch einen Navigationsübergang wie AnimatedSwitcher oder AnimationController definiert ist.

Einen expliziten Animationseffekt verwenden

Wenn Sie einen expliziten Animationseffekt verwenden möchten, schließen Sie das Card-Widget in ein AnimatedSwitcher ein.

lib/question_screen.dart

class QuestionCard extends StatelessWidget {
  final String? question;

  const QuestionCard({
    required this.question,
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return AnimatedSwitcher(                                 // NEW
      duration: const Duration(milliseconds: 300),           // NEW
      child: Card(
        key: ValueKey(question),
        elevation: 4,
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Text(
            question ?? '',
            style: Theme.of(context).textTheme.displaySmall,
          ),
        ),
      ),                                                     // NEW
    );
  }
}

Für AnimatedSwitcher wird standardmäßig ein Überblendungseffekt verwendet. Du kannst das aber mit dem Parameter transitionBuilder überschreiben. Der Übergangs-Builder stellt das untergeordnete Widget bereit, das an AnimatedSwitcher übergeben wurde, und ein Animation-Objekt. Hier bietet sich eine gute Gelegenheit, eine explizite Animation zu verwenden.

In diesem Codelab verwenden wir als erste explizite Animation SlideTransition. Sie nimmt ein Animation<Offset> an, das den Anfangs- und Endoffset definiert, zwischen dem sich die ein- und ausgehenden Widgets bewegen.

Tweens haben eine Hilfsfunktion, animate(), die jeden Animation in einen anderen Animation mit dem angewendeten Tween konvertiert. Das bedeutet, dass mit einer Tween<Offset> die vom AnimatedSwitcher bereitgestellten Animation<double> in eine Animation<Offset> umgewandelt werden können, die an das SlideTransition-Widget übergeben wird.

lib/question_screen.dart

class QuestionCard extends StatelessWidget {
  final String? question;

  const QuestionCard({
    required this.question,
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return AnimatedSwitcher(
      transitionBuilder: (child, animation) {               // Add from here...
        final curveAnimation =
            CurveTween(curve: Curves.easeInCubic).animate(animation);
        final offsetAnimation =
            Tween<Offset>(begin: Offset(-0.1, 0.0), end: Offset.zero)
                .animate(curveAnimation);
        return SlideTransition(position: offsetAnimation, child: child);
      },                                                    // To here.
      duration: const Duration(milliseconds: 300),
      child: Card(
        key: ValueKey(question),
        elevation: 4,
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Text(
            question ?? '',
            style: Theme.of(context).textTheme.displaySmall,
          ),
        ),
      ),
    );
  }
}

Beachten Sie, dass hier mit Tween.animate eine Curve auf die Animation angewendet und dann von einer Tween<double>, die von 0,0 bis 1,0 reicht, in eine Tween<Offset> konvertiert wird, die auf der X-Achse von -0,1 zu 0,0 übergeht.

Alternativ gibt es in der Animation-Klasse die Funktion drive(), die jeden Tween (oder Animatable) in einen neuen Animation umwandelt. So können Tweens „verkettet“ werden, was den resultierenden Code prägnanter macht:

lib/question_screen.dart

transitionBuilder: (child, animation) {
  var offsetAnimation = animation
      .drive(CurveTween(curve: Curves.easeInCubic))
      .drive(Tween<Offset>(begin: Offset(-0.1, 0.0), end: Offset.zero));
  return SlideTransition(position: offsetAnimation, child: child);
},

Ein weiterer Vorteil von expliziten Animationen ist, dass sie sich leicht zusammenstellen lassen. Fügen Sie eine weitere explizite Animation hinzu, die die gleiche gekrümmte Animation verwendet. Hierzu fügen Sie das Widget „Wechsel zur nächsten Folie“ ein.

lib/question_screen.dart

return AnimatedSwitcher(
  transitionBuilder: (child, animation) {
    final curveAnimation =
        CurveTween(curve: Curves.easeInCubic).animate(animation);
    final offsetAnimation =
        Tween<Offset>(begin: Offset(-0.1, 0.0), end: Offset.zero)
            .animate(curveAnimation);
    final fadeInAnimation = curveAnimation;                            // NEW
    return FadeTransition(                                             // NEW
      opacity: fadeInAnimation,                                        // NEW
      child: SlideTransition(position: offsetAnimation, child: child), // NEW
    );                                                                 // NEW
  },

layoutBuilder anpassen

Möglicherweise fällt Ihnen ein kleines Problem mit dem AnimationSwitcher auf. Wenn eine Fragekarte zu einer neuen Frage wechselt, wird sie während der laufenden Animation in der Mitte des verfügbaren Bereichs angezeigt. Wenn die Animation jedoch angehalten wird, springt das Widget an den oberen Bildschirmrand. Das führt zu einer ruckeligen Animation, da die endgültige Position der Fragekarte nicht mit der Position übereinstimmt, die sie während der Animation hat.

d77de181bdde58f7.gif

Um das zu beheben, hat der AnimatedSwitcher auch einen Parameter „layoutBuilder“, mit dem das Layout definiert werden kann. Mit dieser Funktion können Sie den Layout-Builder so konfigurieren, dass die Karte oben auf dem Bildschirm ausgerichtet wird:

lib/question_screen.dart

@override
Widget build(BuildContext context) {
  return AnimatedSwitcher(
    layoutBuilder: (currentChild, previousChildren) {
      return Stack(
        alignment: Alignment.topCenter,
        children: <Widget>[
          ...previousChildren,
          if (currentChild != null) currentChild,
        ],
      );
    },

Dieser Code ist eine modifizierte Version des defaultLayoutBuilder aus der Klasse „AnimatedSwitcher“, verwendet aber Alignment.topCenter anstelle von Alignment.center.

Zusammenfassung

  • Explizite Animationen sind Animationseffekte, die ein Animationsobjekt verwenden (im Gegensatz zu ImplicitlyAnimatedWidgets, die einen Zielwert und eine Dauer verwenden).
  • Die Klasse „Animation“ stellt eine laufende Animation dar, definiert aber keinen bestimmten Effekt.
  • Mit Tween().animate oder Animation.drive() kannst du einer Animation Tweens und Kurven (mit CurveTween) hinzufügen.
  • Mit dem Parameter „layoutBuilder“ von AnimatedSwitcher können Sie die Anordnung der untergeordneten Elemente anpassen.

6. Status einer Animation steuern

Bisher wurden alle Animationen automatisch vom Framework ausgeführt. Implizite Animationen werden automatisch ausgeführt. Für explizite Animationen ist eine Animation erforderlich, damit sie richtig funktionieren. In diesem Abschnitt erfahren Sie, wie Sie mit einem AnimationController eigene Animationen erstellen und mit einer TweenSequence Tweens kombinieren.

Animation mit einem AnimationController ausführen

So erstellen Sie eine Animation mit einem AnimationController:

  1. StatefulWidget erstellen
  2. Verwenden Sie den Mixin „SingleTickerProviderStateMixin“ in Ihrer State-Klasse, um Ihrem AnimationController einen Ticker zur Verfügung zu stellen.
  3. Initialisieren Sie den AnimationController in der Lebenszyklusmethode „initState“ und geben Sie dem Parameter vsync (TickerProvider) das aktuelle State-Objekt an.
  4. Achten Sie darauf, dass Ihr Widget neu erstellt wird, sobald der AnimationController seine Listener benachrichtigt. Verwenden Sie dazu entweder AnimatedBuilder oder rufen Sie listen() und setState manuell auf.

Erstellen Sie eine neue Datei mit dem Namen „flip_effect.dart“ und fügen Sie den folgenden Code ein:

lib/flip_effect.dart

import 'dart:math' as math;

import 'package:flutter/widgets.dart';

class CardFlipEffect extends StatefulWidget {
  final Widget child;
  final Duration duration;

  const CardFlipEffect({
    super.key,
    required this.child,
    required this.duration,
  });

  @override
  State<CardFlipEffect> createState() => _CardFlipEffectState();
}

class _CardFlipEffectState extends State<CardFlipEffect>
    with SingleTickerProviderStateMixin {
  late final AnimationController _animationController;
  Widget? _previousChild;

  @override
  void initState() {
    super.initState();

    _animationController =
        AnimationController(vsync: this, duration: widget.duration);

    _animationController.addListener(() {
      if (_animationController.value == 1) {
        _animationController.reset();
      }
    });
  }

  @override
  void didUpdateWidget(covariant CardFlipEffect oldWidget) {
    super.didUpdateWidget(oldWidget);

    if (widget.child.key != oldWidget.child.key) {
      _handleChildChanged(widget.child, oldWidget.child);
    }
  }

  void _handleChildChanged(Widget newChild, Widget previousChild) {
    _previousChild = previousChild;
    _animationController.forward();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animationController,
      builder: (context, child) {
        return Transform(
          alignment: Alignment.center,
          transform: Matrix4.identity()
            ..rotateX(_animationController.value * math.pi),
          child: _animationController.isAnimating
              ? _animationController.value < 0.5
                  ? _previousChild
                  : Transform.flip(flipY: true, child: child)
              : child,
        );
      },
      child: widget.child,
    );
  }
}

Diese Klasse richtet einen AnimationController ein und führt die Animation jedes Mal neu aus, wenn das Framework „didUpdateWidget“ aufruft, um es darüber zu informieren, dass sich die Widget-Konfiguration geändert hat und es möglicherweise ein neues untergeordnetes Widget gibt.

Der AnimatedBuilder sorgt dafür, dass der Widget-Baum neu erstellt wird, sobald der AnimationController seine Listener benachrichtigt. Mit dem Transform-Widget wird ein 3D-Dreheffekt angewendet, um das Umblättern einer Karte zu simulieren.

Wenn Sie dieses Widget verwenden möchten, müssen Sie jede Antwortkarte in ein CardFlipEffect-Widget einbetten. Geben Sie für das Karten-Widget ein key an:

lib/question_screen.dart

@override
Widget build(BuildContext context) {
  return GridView.count(
    shrinkWrap: true,
    crossAxisCount: 2,
    childAspectRatio: 5 / 2,
    children: List.generate(answers.length, (index) {
      var color = Theme.of(context).colorScheme.primaryContainer;
      if (correctAnswer == index) {
        color = Theme.of(context).colorScheme.tertiaryContainer;
      }
      return CardFlipEffect(                                    // NEW
        duration: const Duration(milliseconds: 300),            // NEW
        child: Card.filled(                                     // NEW
          key: ValueKey(answers[index]),                        // NEW
          color: color,
          elevation: 2,
          margin: EdgeInsets.all(8),
          clipBehavior: Clip.hardEdge,
          child: InkWell(
            onTap: () => onTapped(index),
            child: Padding(
              padding: EdgeInsets.all(16.0),
              child: Center(
                child: Text(
                  answers.length > index ? answers[index] : '',
                  style: Theme.of(context).textTheme.titleMedium,
                  overflow: TextOverflow.clip,
                ),
              ),
            ),
          ),
        ),                                                      // NEW
      );
    }),
  );
}

Führen Sie jetzt einen Hot-Reload der App durch, damit die Antwortkarten mit dem CardFlipEffect-Widget umgedreht werden.

5455def725b866f6.gif

Diese Klasse ähnelt einem expliziten Animationseffekt. Es ist oft sinnvoll, die Klasse „AnimatedWidget“ direkt zu erweitern, um eine eigene Version zu implementieren. Da diese Klasse das vorherige Widget in ihrem Status speichern muss, muss sie ein StatefulWidget verwenden. Weitere Informationen zum Erstellen eigener expliziter Animationseffekte finden Sie in der API-Dokumentation für AnimatedWidget.

Mit TweenSequence eine Verzögerung hinzufügen

In diesem Abschnitt fügen Sie dem CardFlipEffect-Widget eine Verzögerung hinzu, damit die einzelnen Karten nacheinander umgedreht werden. Fügen Sie zuerst ein neues Feld namens delayAmount hinzu.

lib/flip_effect.dart

class CardFlipEffect extends StatefulWidget {
  final Widget child;
  final Duration duration;
  final double delayAmount;                      // NEW

  const CardFlipEffect({
    super.key,
    required this.child,
    required this.duration,
    required this.delayAmount,                   // NEW
  });

  @override
  State<CardFlipEffect> createState() => _CardFlipEffectState();
}

Fügen Sie dann die delayAmount der Build-Methode AnswerCards hinzu.

lib/question_screen.dart

@override
Widget build(BuildContext context) {
  return GridView.count(
    shrinkWrap: true,
    crossAxisCount: 2,
    childAspectRatio: 5 / 2,
    children: List.generate(answers.length, (index) {
      var color = Theme.of(context).colorScheme.primaryContainer;
      if (correctAnswer == index) {
        color = Theme.of(context).colorScheme.tertiaryContainer;
      }
      return CardFlipEffect(
        delayAmount: index.toDouble() / 2,                     // NEW
        duration: const Duration(milliseconds: 300),
        child: Card.filled(
          key: ValueKey(answers[index]),

Erstellen Sie dann in _CardFlipEffectState eine neue Animation, bei der die Verzögerung mithilfe eines TweenSequence angewendet wird. Hinweis: Hier werden keine Dienstprogramme aus der dart:async-Bibliothek wie Future.delayed verwendet. Das liegt daran, dass die Verzögerung Teil der Animation ist und nicht vom Widget explizit gesteuert wird, wenn es den AnimationController verwendet. So lässt sich der Animationseffekt leichter beheben, wenn Sie in den DevTools langsame Animationen aktivieren, da derselbe TickerProvider verwendet wird.

Wenn Sie ein TweenSequence verwenden möchten, erstellen Sie zwei TweenSequenceItems. Eines davon enthält ein ConstantTween, das die Animation für eine relative Dauer auf 0 hält, und eine normale Tween, die von 0.0 bis 1.0 reicht.

lib/flip_effect.dart

class _CardFlipEffectState extends State<CardFlipEffect>
    with SingleTickerProviderStateMixin {
  late final AnimationController _animationController;
  Widget? _previousChild;
  late final Animation<double> _animationWithDelay; // NEW

  @override
  void initState() {
    super.initState();

    _animationController = AnimationController(
        vsync: this, duration: widget.duration * (widget.delayAmount + 1));

    _animationController.addListener(() {
      if (_animationController.value == 1) {
        _animationController.reset();
      }
    });

    _animationWithDelay = TweenSequence<double>([   // NEW
      if (widget.delayAmount > 0)                   // NEW
        TweenSequenceItem(                          // NEW
          tween: ConstantTween<double>(0.0),        // NEW
          weight: widget.delayAmount,               // NEW
        ),                                          // NEW
      TweenSequenceItem(                            // NEW
        tween: Tween(begin: 0.0, end: 1.0),         // NEW
        weight: 1.0,                                // NEW
      ),                                            // NEW
    ]).animate(_animationController);               // NEW
  }

Ersetzen Sie abschließend in der Build-Methode die Animation des AnimationControllers durch die neue verzögerte Animation.

lib/flip_effect.dart

@override
Widget build(BuildContext context) {
  return AnimatedBuilder(
    animation: _animationWithDelay,                            // Modify this line
    builder: (context, child) {
      return Transform(
        alignment: Alignment.center,
        transform: Matrix4.identity()
          ..rotateX(_animationWithDelay.value * math.pi),      // And this line
        child: _animationController.isAnimating
            ? _animationWithDelay.value < 0.5                  // And this one.
                ? _previousChild
                : Transform.flip(flipY: true, child: child)
            : child,
      );
    },
    child: widget.child,
  );
}

Führen Sie jetzt ein Hot Reload der App durch und sehen Sie sich an, wie die Karten nacheinander umgedreht werden. Sie können auch versuchen, die Perspektive des 3D-Effekts des Transform-Widgets zu ändern.

28b5291de9b3f55f.gif

7. Benutzerdefinierte Navigationsübergänge verwenden

Bisher haben wir gesehen, wie Sie Effekte auf einem einzelnen Bildschirm anpassen. Sie können Animationen aber auch für den Übergang zwischen Bildschirmen verwenden. In diesem Abschnitt erfahren Sie, wie Sie Bildschirmübergänge mithilfe von integrierten und vorgefertigten Animationseffekten aus dem offiziellen Animationspaket auf pub.dev animieren.

Navigationsübergang animieren

Die Klasse PageRouteBuilder ist eine Route, mit der Sie die Übergangsanimation anpassen können. Sie können den transitionBuilder-Callback überschreiben, der zwei Animationsobjekte bereitstellt, die die eingehende und ausgehende Animation darstellen, die vom Navigator ausgeführt wird.

Ersetzen Sie MaterialPageRoute durch PageRouteBuilder, um die Übergangsanimation anzupassen, wenn der Nutzer von HomeScreen zu QuestionScreen wechselt. Verwenden Sie einen FadeTransition (ein explizit animiertes Widget), um den neuen Bildschirm über dem vorherigen Bildschirm einzublenden.

lib/home_screen.dart

ElevatedButton(
  onPressed: () {
    // Show the question screen to start the game
    Navigator.push(
      context,
      PageRouteBuilder(                                         // NEW
        pageBuilder: (context, animation, secondaryAnimation) { // NEW
          return QuestionScreen();                              // NEW
        },                                                      // NEW
        transitionsBuilder:                                     // NEW
            (context, animation, secondaryAnimation, child) {   // NEW
          return FadeTransition(                                // NEW
            opacity: animation,                                 // NEW
            child: child,                                       // NEW
          );                                                    // NEW
        },                                                      // NEW
      ),                                                        // NEW
    );
  },
  child: Text('New Game'),
),

Das Animationspaket bietet praktische vorgefertigte Animationseffekte wie „FadeThroughTransition“. Importieren Sie das Animationspaket und ersetzen Sie das Widget „FadeTransition“ durch das Widget „FadeThroughTransition“:

lib/home_screen.dart

import 'package;animations/animations.dart';

ElevatedButton(
  onPressed: () {
    // Show the question screen to start the game
    Navigator.push(
      context,
      PageRouteBuilder(
        pageBuilder: (context, animation, secondaryAnimation) {
          return const QuestionScreen();
        },
        transitionsBuilder:
            (context, animation, secondaryAnimation, child) {
          return FadeThroughTransition(                          // NEW
            animation: animation,                                // NEW
            secondaryAnimation: secondaryAnimation,              // NEW
            child: child,                                        // NEW
          );                                                     // NEW
        },
      ),
    );
  },
  child: Text('New Game'),
),

Animation für intelligente „Zurück“-Touch-Geste anpassen

1c0558ffa3b76439.gif

Die Funktion „Vorhersage für Zurück“ ist eine neue Android-Funktion, mit der Nutzer einen Blick auf die aktuelle Route oder App werfen können, bevor sie weitergeleitet werden. Die Peek-Animation wird durch die Position des Fingers des Nutzers gesteuert, während er den Finger über das Display zieht.

Flutter unterstützt die vorausschauende Navigation zurück, indem die Funktion auf Systemebene aktiviert wird, wenn Flutter keine Routen im Navigationsstack hat, die eingeblendet werden können, oder wenn durch die Schaltfläche „Zurück“ die App geschlossen wird. Diese Animation wird vom System und nicht von Flutter selbst verwaltet.

Flutter unterstützt auch die Vorhersage von „Zurück“-Gesten beim Wechseln zwischen Routen innerhalb einer Flutter-App. Ein spezieller PageTransitionsBuilder namens PredictiveBackPageTransitionsBuilder überwacht die Vorhersage von „Zurück“-Gesten des Systems und steuert den Seitenübergang anhand des Fortschritts der Geste.

Die Vorhersage der Rückwärtsnavigation wird nur in Android U und höher unterstützt. Flutter wechselt aber nahtlos zum ursprünglichen Verhalten der Rückwärtsgeste und zum ZoomPageTransitionBuilder. Weitere Informationen finden Sie in unserem Blogpost, einschließlich eines Abschnitts zur Einrichtung in Ihrer eigenen App.

Konfigurieren Sie in der ThemeData-Konfiguration Ihrer App das PageTransitionsTheme so, dass auf Android-Geräten „PredictiveBack“ und auf anderen Plattformen der Überblendungseffekt aus dem Animationspaket verwendet wird:

lib/main.dart

import 'package:animations/animations.dart';                                 // NEW
import 'package:flutter/material.dart';

import 'home_screen.dart';

void main() {
  runApp(MainApp());
}

class MainApp extends StatelessWidget {
  const MainApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
        pageTransitionsTheme: PageTransitionsTheme(
          builders: {
            TargetPlatform.android: PredictiveBackPageTransitionsBuilder(),  // NEW
            TargetPlatform.iOS: FadeThroughPageTransitionsBuilder(),         // NEW
            TargetPlatform.macOS: FadeThroughPageTransitionsBuilder(),       // NEW
            TargetPlatform.windows: FadeThroughPageTransitionsBuilder(),     // NEW
            TargetPlatform.linux: FadeThroughPageTransitionsBuilder(),       // NEW
          },
        ),
      ),
      home: HomeScreen(),
    );
  }
}

Jetzt können Sie den Rückruf von Navigator.push() in eine MaterialPageRoute ändern.

lib/home_screen.dart

ElevatedButton(
  onPressed: () {
    // Show the question screen to start the game
    Navigator.push(
      context,
      MaterialPageRoute(builder: (context) {       // NEW
        return const QuestionScreen();             // NEW
      }),                                          // NEW
    );
  },
  child: Text('New Game'),
),

Mit FadeThroughTransition die aktuelle Frage ändern

Das AnimatedSwitcher-Widget bietet nur eine Animation in seinem Builder-Callback. Um dies zu ermöglichen, bietet das Paket animations einen PageTransitionSwitcher.

lib/question_screen.dart

class QuestionCard extends StatelessWidget {
  final String? question;

  const QuestionCard({
    required this.question,
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return PageTransitionSwitcher(                                          // NEW
      layoutBuilder: (entries) {                                            // NEW
        return Stack(                                                       // NEW
          alignment: Alignment.topCenter,                                   // NEW
          children: entries,                                                // NEW
        );                                                                  // NEW
      },                                                                    // NEW
      transitionBuilder: (child, animation, secondaryAnimation) {           // NEW
        return FadeThroughTransition(                                       // NEW
          animation: animation,                                             // NEW
          secondaryAnimation: secondaryAnimation,                           // NEW
          child: child,                                                     // NEW
        );                                                                  // NEW
      },                                                                    // NEW
      duration: const Duration(milliseconds: 300),
      child: Card(
        key: ValueKey(question),
        elevation: 4,
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Text(
            question ?? '',
            style: Theme.of(context).textTheme.displaySmall,
          ),
        ),
      ),
    );
  }
}

OpenContainer verwenden

77358e5776eb104c.png

Das Widget OpenContainer aus dem Paket animations bietet einen Containertransformations-Animationseffekt, der sich ausweitet, um eine visuelle Verbindung zwischen zwei Widgets herzustellen.

Das von closedBuilder zurückgegebene Widget wird zuerst angezeigt und maximiert sich zu dem von openBuilder zurückgegebenen Widget, wenn auf den Container getippt oder der openContainer-Callback aufgerufen wird.

Um den openContainer-Callback mit dem View-Modell zu verknüpfen, fügen Sie dem QuestionCard-Widget ein neues View-Modell hinzu und speichern Sie einen Callback, mit dem der Bildschirm „Game Over“ angezeigt wird:

lib/question_screen.dart

class QuestionScreen extends StatefulWidget {
  const QuestionScreen({super.key});

  @override
  State<QuestionScreen> createState() => _QuestionScreenState();
}

class _QuestionScreenState extends State<QuestionScreen> {
  late final QuizViewModel viewModel =
      QuizViewModel(onGameOver: _handleGameOver);
  VoidCallback? _showGameOverScreen;                                    // NEW

  @override
  Widget build(BuildContext context) {
    return ListenableBuilder(
      listenable: viewModel,
      builder: (context, child) {
        return Scaffold(
          appBar: AppBar(
            actions: [
              TextButton(
                onPressed:
                    viewModel.hasNextQuestion && viewModel.didAnswerQuestion
                        ? () {
                            viewModel.getNextQuestion();
                          }
                        : null,
                child: const Text('Next'),
              )
            ],
          ),
          body: Center(
            child: Column(
              children: [
                QuestionCard(                                           // NEW
                  onChangeOpenContainer: _handleChangeOpenContainer,    // NEW
                  question: viewModel.currentQuestion?.question,        // NEW
                  viewModel: viewModel,                                 // NEW
                ),                                                      // NEW
                Spacer(),
                AnswerCards(
                  onTapped: (index) {
                    viewModel.checkAnswer(index);
                  },
                  answers: viewModel.currentQuestion?.possibleAnswers ?? [],
                  correctAnswer: viewModel.didAnswerQuestion
                      ? viewModel.currentQuestion?.correctAnswer
                      : null,
                ),
                StatusBar(viewModel: viewModel),
              ],
            ),
          ),
        );
      },
    );
  }

  void _handleChangeOpenContainer(VoidCallback openContainer) {        // NEW
    _showGameOverScreen = openContainer;                               // NEW
  }                                                                    // NEW

  void _handleGameOver() {                                             // NEW
    if (_showGameOverScreen != null) {                                 // NEW
      _showGameOverScreen!();                                          // NEW
    }                                                                  // NEW
  }                                                                    // NEW
}

Fügen Sie ein neues Widget hinzu, „GameOverScreen“:

lib/question_screen.dart

class GameOverScreen extends StatelessWidget {
  final QuizViewModel viewModel;
  const GameOverScreen({required this.viewModel, super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        automaticallyImplyLeading: false,
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Scoreboard(
              score: viewModel.score,
              totalQuestions: viewModel.totalQuestions,
            ),
            Text(
              'You Win!',
              style: Theme.of(context).textTheme.displayLarge,
            ),
            Text(
              'Score: ${viewModel.score} / ${viewModel.totalQuestions}',
              style: Theme.of(context).textTheme.displaySmall,
            ),
            ElevatedButton(
              child: Text('OK'),
              onPressed: () {
                Navigator.popUntil(context, (route) => route.isFirst);
              },
            ),
          ],
        ),
      ),
    );
  }
}

Ersetzen Sie im Widget „QuestionCard“ die Karte durch ein „OpenContainer“-Widget aus dem Animationspaket und fügen Sie zwei neue Felder für das View-Modell und den Open-Container-Callback hinzu:

lib/question_screen.dart

class QuestionCard extends StatelessWidget {
  final String? question;

  const QuestionCard({
    required this.onChangeOpenContainer,
    required this.question,
    required this.viewModel,
    super.key,
  });

  final ValueChanged<VoidCallback> onChangeOpenContainer;
  final QuizViewModel viewModel;

  static const _backgroundColor = Color(0xfff2f3fa);

  @override
  Widget build(BuildContext context) {
    return PageTransitionSwitcher(
      duration: const Duration(milliseconds: 200),
      transitionBuilder: (child, animation, secondaryAnimation) {
        return FadeThroughTransition(
          animation: animation,
          secondaryAnimation: secondaryAnimation,
          child: child,
        );
      },
      child: OpenContainer(                                         // NEW
        key: ValueKey(question),                                    // NEW
        tappable: false,                                            // NEW
        closedColor: _backgroundColor,                              // NEW
        closedShape: const RoundedRectangleBorder(                  // NEW
          borderRadius: BorderRadius.all(Radius.circular(12.0)),    // NEW
        ),                                                          // NEW
        closedElevation: 4,                                         // NEW
        closedBuilder: (context, openContainer) {                   // NEW
          onChangeOpenContainer(openContainer);                     // NEW
          return ColoredBox(                                        // NEW
            color: _backgroundColor,                                // NEW
            child: Padding(                                         // NEW
              padding: const EdgeInsets.all(16.0),                  // NEW
              child: Text(
                question ?? '',
                style: Theme.of(context).textTheme.displaySmall,
              ),
            ),
          );
        },
        openBuilder: (context, closeContainer) {                    // NEW
          return GameOverScreen(viewModel: viewModel);              // NEW
        },                                                          // NEW
      ),
    );
  }
}

4120f9395857d218.gif

8. Glückwunsch

Glückwunsch, Sie haben einer Flutter-App Animationseffekte hinzugefügt und sich mit den Hauptkomponenten des Flutter-Animationssystems vertraut gemacht. Sie haben unter anderem Folgendes gelernt:

  • ImplicitlyAnimatedWidget verwenden
  • ExplicitlyAnimatedWidget verwenden
  • Kurven und Tweens auf eine Animation anwenden
  • Vordefinierte Übergangs-Widgets wie AnimatedSwitcher oder PageRouteBuilder verwenden
  • Vordefinierte Animationseffekte aus dem animations-Paket verwenden, z. B. „FadeThroughTransition“ und „OpenContainer“
  • Informationen zum Anpassen der Standardübergangsanimation, einschließlich der Unterstützung der Funktion „Vorhersagen für Zurück“ auf Android-Geräten

3026390ad413769c.gif

Was liegt als Nächstes an?

Sehen Sie sich diese Codelabs an:

Sie können auch die Beispielanwendung für Animationen herunterladen, in der verschiedene Animationstechniken gezeigt werden.

Weitere Informationen

Weitere Ressourcen zu Animationen finden Sie auf flutter.dev:

Oder lesen Sie diese Artikel auf Medium:

Referenzdokumente