Animationen in Flutter

1. Einführung

Animationen sind eine gute Möglichkeit, die Nutzerfreundlichkeit Ihrer App zu verbessern, wichtige Informationen an den Nutzer zu kommunizieren und Ihre App ansprechender und angenehmer zu gestalten.

Übersicht über das Animations-Framework von Flutter

In Flutter werden Animationseffekte angezeigt, indem in jedem Frame ein Teil des Widget-Baums neu erstellt wird. Es bietet vorgefertigte Animationseffekte und andere APIs, um das Erstellen und Zusammensetzen von Animationen zu erleichtern.

  • Implizite Animationen sind vorgefertigte Animationseffekte, die die gesamte Animation automatisch ausführen. Wenn sich der target-Wert der Animation ändert, wird die Animation vom aktuellen Wert zum Zielwert ausgeführt und jeder Wert dazwischen angezeigt, sodass das Widget flüssig animiert wird. Beispiele für implizite Animationen sind AnimatedSize, AnimatedScale und AnimatedPositioned.
  • Explizite Animationen sind ebenfalls vorgefertigte Animationseffekte, für die jedoch ein Animation-Objekt erforderlich ist. Beispiele sind 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, den die Animation zu einem bestimmten Zeitpunkt auf dem Bildschirm anzeigt. Sie ist eine Unterklasse von Listenable und benachrichtigt ihre Listener, wenn sich der Status während der Animation ändert.
  • Mit AnimationController können Sie eine Animation erstellen und ihren Status steuern. Mit den Methoden wie forward(), reset(), stop() und repeat() kann die Animation gesteuert werden, ohne dass der angezeigte Animationseffekt wie Skalierung, Größe oder Position definiert werden muss.
  • Tweens werden verwendet, um Werte zwischen einem Anfangs- und einem Endwert zu interpolieren. Sie können jeden Typ darstellen, 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 am Anfang oder Ende der Animation zu beschleunigen oder zu verlangsamen. Für Kurven wird ein Eingabewert zwischen 0,0 und 1,0 verwendet und ein Ausgabewert zwischen 0,0 und 1,0 zurückgegeben.

Aufgaben

In diesem Codelab erstellen Sie ein Multiple-Choice-Quizspiel mit verschiedenen Animationseffekten und ‑techniken.

3026390ad413769c.gif

Sie erfahren, wie Sie…

  • Widget erstellen, dessen Größe und Farbe animiert werden
  • 3D-Karten-Umblättereffekt erstellen
  • 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

Lerninhalte

In diesem Codelab lernen Sie Folgendes:

  • Wie Sie implizit animierte Effekte verwenden, um ansprechende Animationen zu erstellen, ohne viel Code schreiben zu müssen.
  • Explizit animierte Effekte verwenden, um eigene Effekte mit vorgefertigten animierten Widgets wie AnimatedSwitcher oder AnimationController zu konfigurieren.
  • So definieren Sie mit AnimationController ein eigenes Widget, das einen 3D-Effekt anzeigt.
  • Wie Sie mit dem animations-Paket mit minimalem Aufwand ansprechende Animationseffekte anzeigen lassen.

Voraussetzungen

  • Flutter SDK
  • Eine IDE wie VS Code oder Android Studio / IntelliJ

2. Flutter-Entwicklungsumgebung einrichten

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

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

  • Ein physisches Android-Gerät (empfohlen für die Implementierung der Funktion „Vorhersagende Zurück-Geste“ in Schritt 7) oder iOS-Gerät, das mit Ihrem Computer verbunden und auf den Entwicklermodus eingestellt ist.
  • Der iOS-Simulator (erfordert die Installation von Xcode-Tools).
  • Android Emulator (muss in Android Studio eingerichtet werden)
  • Ein Browser (für das Debugging ist Chrome erforderlich).
  • Ein Windows-, Linux- oder macOS-Desktopcomputer. Sie müssen auf der Plattform entwickeln, auf der Sie die Bereitstellung planen. Wenn Sie also eine Windows-Desktop-App entwickeln möchten, müssen Sie unter Windows entwickeln, um auf die entsprechende Build-Kette zuzugreifen. Es gibt betriebssystemspezifische Anforderungen, die auf docs.flutter.dev/desktop ausführlich beschrieben werden.

Installation prüfen

So prüfen Sie, ob Ihr Flutter SDK richtig konfiguriert ist und Sie mindestens eine der oben genannten Zielplattformen installiert haben:

$ 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

Start-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.

Anwendung ausführen

Verwenden Sie zum Ausführen der App den Befehl flutter run und geben Sie ein Zielgerät an, z. B. android, ios oder chrome. 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 Ihrer bevorzugten IDE ausführen und debuggen. Weitere Informationen finden Sie in der offiziellen Flutter-Dokumentation.

Code ansehen

Die Starter-App ist ein Multiple-Choice-Quizspiel, das aus zwei Bildschirmen besteht, die dem MVVM-Designmuster (Model-View-ViewModel) folgen. In der QuestionScreen (Ansicht) wird die Klasse QuizViewModel (View-Model) verwendet, um dem Nutzer Multiple-Choice-Fragen aus der Klasse QuestionBank (Model) zu stellen.

  • home_screen.dart: Hier wird ein Bildschirm mit der Schaltfläche New Game (Neues Spiel) angezeigt.
  • main.dart: Konfiguriert die MaterialApp für die Verwendung von Material 3 und zeigt den Startbildschirm an.
  • model.dart: Definiert die Kernklassen, die in der gesamten App verwendet werden.
  • question_screen.dart: Zeigt die Benutzeroberfläche für das Quizspiel an.
  • view_model.dart: Hier werden der Status und die Logik für das Quizspiel gespeichert, die von QuestionScreen angezeigt werden.

fbb1e1f7b6c91e21.png

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

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 ein animiertes Scoreboard angezeigt wird. Häufige implizite Animationseffekte finden Sie in der API-Dokumentation zu ImplicitlyAnimatedWidget.

206dd8d9c1fae95.gif

Unanimiertes Scoreboard-Widget erstellen

Erstellen Sie eine neue Datei mit dem Namen lib/scoreboard.dart und 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 in die untergeordneten Elemente des StatusBar-Widgets ein und ersetzen Sie damit die Text-Widgets, in denen zuvor die Punktzahl und die Gesamtzahl der Fragen angezeigt wurden. Der Editor sollte automatisch das erforderliche import "scoreboard.dart" oben in der Datei hinzufügen.

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
            ),
          ],
        ),
      ),
    );
  }
}

In diesem Widget wird für jede Frage ein Sternsymbol angezeigt. Wenn eine Frage richtig beantwortet wird, leuchtet sofort ein weiterer Stern auf, ohne dass eine Animation erfolgt. In den folgenden Schritten informieren Sie den Nutzer darüber, dass sich sein Ergebnis geändert hat, indem Sie die Größe und Farbe animieren.

Impliziten Animationseffekt verwenden

Erstellen Sie ein neues Widget namens AnimatedStar, das ein AnimatedScale-Widget verwendet, um den scale-Betrag 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(isActive: score > i),                 // Edit this line.
        ],
      ),
    );
  }
}

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, wird die Größe des AnimatedStar-Widgets durch eine implizite Animation angepasst. Die Icon der color wird hier nicht animiert, nur die scale, was durch das AnimatedScale-Widget erfolgt.

84aec4776e70b870.gif

Tween verwenden, um zwischen zwei Werten zu interpolieren

Die Farbe des AnimatedStar-Widgets ändert sich sofort, nachdem sich das Feld isActive in „true“ ändert.

Wenn Sie einen animierten Farbeffekt erzielen möchten, können Sie das AnimatedContainer-Widget (eine weitere Unterklasse von ImplicitlyAnimatedWidget) verwenden, da alle seine Attribute, einschließlich der Farbe, automatisch animiert werden können. Leider muss in unserem Widget ein Symbol und kein Container angezeigt werden.

Sie können auch AnimatedIcon ausprobieren, bei 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 ein Tween als Parameter akzeptiert. Ein Tween ist eine Klasse, die zwei Werte (begin und end) verwendet und die Zwischenwerte berechnet, damit sie in einer Animation angezeigt werden können. In diesem Beispiel verwenden wir ein ColorTween, das die für die Erstellung unseres Animationseffekts erforderliche Tween-Schnittstelle erfüllt.

Wählen Sie das Icon-Widget aus und verwenden Sie die Schnellaktion „Wrap with Builder“ (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);     // And modify this line.
        },
      ),
    );
  }
}

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 unseres ColorTween basierend auf dem Wert des Parameters isActive ändert. Das liegt daran, dass TweenAnimationBuilder die Animation immer dann neu ausführt, wenn sich der Wert von Tween.end ändert. In diesem Fall wird die neue Animation vom aktuellen Animationswert zum neuen Endwert ausgeführt. So können Sie die Farbe jederzeit ändern, auch während die Animation läuft, und einen flüssigen Animationseffekt mit den richtigen Zwischenwerten erzielen.

Kurve anwenden

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

Mit einer Curve wird eine Easing-Funktion angewendet, die die Änderungsrate eines Parameters im Zeitverlauf definiert. Flutter enthält eine Sammlung vordefinierter Easing-Kurven in der Klasse Curves, z. B. easeIn oder easeOut.

5dabe68d1210b8a1.gif

3a9e7490c594279a.gif

Diese Diagramme (verfügbar auf der Seite mit der Curves-API-Dokumentation) geben einen Hinweis darauf, wie Kurven funktionieren. Mit 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. In diesen Diagrammen sehen Sie auch eine Vorschau, wie verschiedene Animationseffekte mit einer Easing-Kurve aussehen.

Erstellen Sie ein neues Feld in AnimatedStar mit dem Namen _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 Kurve elasticOut für einen übertriebenen Federeffekt, der mit einer Federbewegung beginnt und sich zum Ende hin 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 Entwicklertools aktivieren

Mit Flutter DevTools können Sie alle Animationen in Ihrer App verlangsamen, um Animationseffekte zu debuggen.

Damit Sie die DevTools öffnen können, muss die App im Debug-Modus ausgeführt werden. Öffnen Sie den Widget Inspector, indem Sie ihn in der Debug-Symbolleiste in VSCode auswählen oder in IntelliJ / Android Studio im Debug-Toolfenster auf die Schaltfläche Flutter DevTools öffnen klicken.

3ce33dc01d096b14.png

363ae0fbcd0c2395.png

Wenn der Widget-Inspector geöffnet ist, klicken Sie in der Symbolleiste auf die Schaltfläche Langsame Animationen.

adea0a16d01127ad.png

5. Explizite Animationseffekte verwenden

Wie implizite Animationen sind explizite Animationen vorgefertigte Animationseffekte. Sie verwenden jedoch kein Ziel, sondern ein Animation-Objekt als Parameter. Das macht sie nützlich in Situationen, in denen die Animation bereits durch einen Navigationsübergang, AnimatedSwitcher oder AnimationController definiert ist.

Expliziten Animationseffekt verwenden

Wenn Sie einen expliziten Animationseffekt verwenden möchten, umschließen Sie das Card-Widget mit einem AnimatedSwitcher.

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
    );
  }
}

AnimatedSwitcher verwendet standardmäßig einen Crossfade-Effekt. Sie können dies jedoch mit dem Parameter transitionBuilder überschreiben. Der Übergangs-Builder stellt das untergeordnete Widget bereit, das an AnimatedSwitcher übergeben wurde, sowie ein Animation-Objekt. Hier bietet sich eine explizite Animation an.

Die erste explizite Animation, die wir in diesem Codelab verwenden, ist SlideTransition. Sie verwendet ein Animation<Offset>, das den Start- und End-Offset definiert, zwischen denen sich die eingehenden und ausgehenden Widgets bewegen.

Für Tweens gibt es die Hilfsfunktion animate(), mit der ein beliebiges Animation in ein anderes Animation mit angewendetem Tween umgewandelt werden kann. Das bedeutet, dass ein Tween verwendet werden kann, um das von AnimatedSwitcher bereitgestellte Animation in ein Animation umzuwandeln, das dem SlideTransition-Widget zur Verfügung gestellt 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,
          ),
        ),
      ),
    );
  }
}

Dabei wird Tween.animate verwendet, um Curve auf Animation anzuwenden und dann von einem Tween, das zwischen 0,0 und 1,0 liegt, in ein Tween umzuwandeln, das auf der x-Achse von -0,1 bis 0,0 übergeht.

Alternativ hat die Klasse „Animation“ eine drive()-Funktion, die einen beliebigen Tween (oder Animatable) akzeptiert und in einen neuen Animation konvertiert. So können Tweens „verkettet“ werden, wodurch der resultierende Code kompakter wird:

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 der Verwendung expliziter Animationen ist, dass sie kombiniert werden können. Fügen Sie eine weitere explizite Animation, FadeTransition, hinzu, die dieselbe gekrümmte Animation verwendet, indem Sie das SlideTransition-Widget umschließen.

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 der AnimationSwitcher auf. Wenn QuestionCard zu einer neuen Frage wechselt, wird sie während der Animation in der Mitte des verfügbaren Bereichs angezeigt. Wenn die Animation beendet ist, wird das Widget oben auf dem Bildschirm angedockt. Dadurch kommt es zu einer ruckeligen Animation, da die endgültige Position der Fragekarte nicht mit der Position während der Animation übereinstimmt.

d77de181bdde58f7.gif

Um dieses Problem zu beheben, hat AnimatedSwitcher auch einen layoutBuilder-Parameter, mit dem das Layout definiert werden kann. Mit dieser Funktion können Sie den Layout-Generator so konfigurieren, dass die Karte am oberen Bildschirmrand 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 von defaultLayoutBuilder aus der Klasse AnimatedSwitcher, verwendet aber Alignment.topCenter anstelle von Alignment.center.

Zusammenfassung

  • Explizite Animationen sind Animationseffekte, die ein Animation-Objekt verwenden (im Gegensatz zu ImplicitlyAnimatedWidgets, die ein Ziel-value und duration verwenden).
  • Die Klasse Animation stellt eine laufende Animation dar, definiert aber keinen bestimmten Effekt.
  • Verwenden Sie Tween().animate oder Animation.drive(), um Tweens und Curves (mit CurveTween) auf eine Animation anzuwenden.
  • Mit dem Parameter layoutBuilder von AnimatedSwitcher können Sie anpassen, wie die untergeordneten Elemente angeordnet werden.

6. Status einer Animation steuern

Bisher wurde jede Animation automatisch vom Framework ausgeführt. Implizite Animationen werden automatisch ausgeführt. Für explizite Animationseffekte ist ein Animation erforderlich, damit sie richtig funktionieren. In diesem Abschnitt erfahren Sie, wie Sie mit einem AnimationController eigene Animation-Objekte erstellen und mit einem TweenSequence Tweens kombinieren.

Animation mit einem AnimationController ausführen

So erstellen Sie eine Animation mit einem AnimationController:

  1. StatefulWidget erstellen
  2. Verwenden Sie den SingleTickerProviderStateMixin-Mixin in Ihrer State-Klasse, um einen Ticker für Ihre AnimationController bereitzustellen.
  3. Initialisieren Sie AnimationController in der Lebenszyklusmethode initState und stellen Sie das aktuelle State-Objekt für den Parameter vsync (TickerProvider) bereit.
  4. Achten Sie darauf, dass Ihr Widget neu erstellt wird, wenn 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 kopieren Sie den folgenden Code hinein:

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,
    );
  }
}

In dieser Klasse wird ein AnimationController eingerichtet und die Animation wird immer dann neu ausgeführt, wenn das Framework didUpdateWidget aufruft, um es darüber zu informieren, dass sich die Widget-Konfiguration geändert hat und möglicherweise ein neues untergeordnetes Widget vorhanden ist.

Mit AnimatedBuilder wird dafür gesorgt, dass der Widget-Baum neu erstellt wird, wenn AnimationController seine Listener benachrichtigt. Das Transform-Widget wird verwendet, um einen 3D-Rotationseffekt anzuwenden, der das Umdrehen einer Karte simuliert.

Wenn Sie dieses Widget verwenden möchten, müssen Sie jede Antwortkarte mit einem CardFlipEffect-Widget umschließen. Achten Sie darauf, dass Sie dem Card-Widget ein key zur Verfügung stellen:

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, um zu sehen, wie die Antwortkarten mit dem CardFlipEffect-Widget umgedreht werden.

5455def725b866f6.gif

Diese Klasse ähnelt einem expliziten Animationseffekt. Oft ist es sogar sinnvoll, die AnimatedWidget-Klasse direkt zu erweitern, um eine eigene Version zu implementieren. Da in dieser Klasse das vorherige Widget in ihrem State gespeichert werden muss, muss sie leider ein StatefulWidget verwenden. Weitere Informationen zum Erstellen eigener expliziter Animationseffekte finden Sie in der API-Dokumentation zu AnimatedWidget.

Verzögerung mit TweenSequence hinzufügen

In diesem Abschnitt fügen Sie dem CardFlipEffect-Widget eine Verzögerung hinzu, damit die Karten nacheinander umgedreht werden. Fügen Sie zuerst ein neues Feld mit dem Namen 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 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, die die Verzögerung mithilfe eines TweenSequence anwendet. Beachten Sie, dass hier keine Dienstprogramme aus der dart:async-Bibliothek wie Future.delayed verwendet werden. Das liegt daran, dass die Verzögerung Teil der Animation ist und nicht explizit vom Widget gesteuert wird, wenn es AnimationController verwendet. So lässt sich der Animationseffekt leichter debuggen, wenn Sie langsame Animationen in den Entwicklertools aktivieren, da er dieselbe TickerProvider verwendet.

Wenn Sie ein TweenSequence verwenden möchten, erstellen Sie zwei TweenSequenceItems. Eine enthält ein ConstantTween, das die Animation für eine relative Dauer auf 0 hält, und eine reguläre Tween, die von 0.0 zu 1.0 wechselt.

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>([              // Add from here...
      if (widget.delayAmount > 0)
        TweenSequenceItem(
          tween: ConstantTween<double>(0.0),
          weight: widget.delayAmount,
        ),
      TweenSequenceItem(tween: Tween(begin: 0.0, end: 1.0), weight: 1.0),
    ]).animate(_animationController);                          // To here.
  }

Ersetzen Sie schließlich die Animation von AnimationController durch die neue verzögerte Animation in der Methode build.

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 Hot Reload für die App aus und sehen Sie zu, wie die Karten nacheinander umgedreht werden. Bei einer Challenge können Sie mit der Perspektive des 3D-Effekts experimentieren, die vom Transform-Widget bereitgestellt wird.

28b5291de9b3f55f.gif

7. Benutzerdefinierte Navigationsübergänge verwenden

Bisher haben wir uns angesehen, wie sich Effekte auf einem einzelnen Bildschirm anpassen lassen. Eine weitere Möglichkeit, Animationen zu verwenden, besteht darin, sie für den Übergang zwischen Bildschirmen einzusetzen. In diesem Abschnitt erfahren Sie, wie Sie mit integrierten Animationseffekten und den ausgefallenen vorgefertigten Animationseffekten, die vom offiziellen animations-Paket auf pub.dev bereitgestellt werden, Animationseffekte auf Bildschirmübergänge anwenden.

Navigationsübergang animieren

Die Klasse PageRouteBuilder ist ein Route, mit dem Sie die Übergangsanimation anpassen können. Damit können Sie den transitionBuilder-Callback überschreiben. Er stellt zwei Animation-Objekte bereit, die die eingehende und ausgehende Animation darstellen, die vom Navigator ausgeführt wird.

Wenn Sie die Übergangsanimation anpassen möchten, ersetzen Sie MaterialPageRoute durch PageRouteBuilder. Wenn Sie die Übergangsanimation anpassen möchten, wenn der Nutzer von HomeScreen zu QuestionScreen wechselt, Verwenden Sie ein FadeTransition (ein explizit animiertes Widget), um den neuen Bildschirm über dem vorherigen Bildschirm einblenden zu lassen.

lib/home_screen.dart

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

Das Animationspaket bietet ansprechende vorgefertigte Animationseffekte wie FadeThroughTransition. Importieren Sie das Animationspaket und ersetzen Sie FadeTransition durch das FadeThroughTransition-Widget:

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(                     // Add from here...
                animation: animation,
                secondaryAnimation: secondaryAnimation,
                child: child,
              );                                                // To here.
            },
      ),
    );
  },
  child: Text('New Game'),
),

Animation für die intelligente „Zurück“-Touchgeste anpassen

1c0558ffa3b76439.gif

Die Funktion „Vorhersagender Zurück-Button“ ist eine neue Android-Funktion, mit der Nutzer vor der Navigation sehen können, was sich hinter der aktuellen Route oder App befindet. Die Peek-Animation wird durch die Position des Fingers des Nutzers bestimmt, wenn er ihn über den Bildschirm zieht.

Flutter unterstützt die systembasierte Vorhersage für die Zurück-Geste, indem die Funktion auf Systemebene aktiviert wird, wenn in Flutter keine Routen im Navigationsstapel vorhanden sind, die entfernt werden können. Das heißt, wenn durch eine Zurück-Geste die App beendet würde. Diese Animation wird vom System und nicht von Flutter selbst verarbeitet.

Flutter unterstützt auch die intelligente „Zurück“-Geste beim Navigieren zwischen Routen in einer Flutter-App. Ein spezielles PageTransitionsBuilder namens PredictiveBackPageTransitionsBuilder überwacht die intelligenten „Zurück“-Gesten des Systems und steuert den Seitenübergang entsprechend dem Fortschritt der Geste.

Die Funktion „Vorhersagende Zurück-Geste“ wird nur in Android U und höher unterstützt. Flutter greift jedoch auf das ursprüngliche Verhalten der Zurück-Geste und ZoomPageTransitionBuilder zurück. Weitere Informationen, einschließlich eines Abschnitts zur Einrichtung in Ihrer eigenen App, finden Sie in unserem Blogpost.

Konfigurieren Sie in der ThemeData-Konfiguration für Ihre App PageTransitionsTheme so, dass PredictiveBack unter Android und der Fade-Through-Übergangseffekt aus dem Animationspaket auf anderen Plattformen 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),
        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(),
    );
  }
}

Sie können den Navigator.push()-Rückruf jetzt in einen MaterialPageRoute ändern.

lib/home_screen.dart

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

Mit FadeThroughTransition die aktuelle Frage ändern

Das AnimatedSwitcher-Widget stellt in seinem Builder-Callback nur ein Animation bereit. Zur Behebung dieses Problems bietet das Paket animations eine 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(                              // Add from here...
      layoutBuilder: (entries) {
        return Stack(alignment: Alignment.topCenter, children: entries);
      },
      transitionBuilder: (child, animation, secondaryAnimation) {
        return FadeThroughTransition(
          animation: animation,
          secondaryAnimation: secondaryAnimation,
          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,
          ),
        ),
      ),
    );
  }
}

OpenContainer verwenden

77358e5776eb104c.png

Das OpenContainer-Widget aus dem Paket animations bietet einen Container-Transformationsanimationseffekt, der erweitert wird, um eine visuelle Verbindung zwischen zwei Widgets herzustellen.

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

Um den openContainer-Callback mit dem View-Model zu verbinden, fügen Sie einen neuen Pass für das viewModel-Objekt in das QuestionCard-Widget ein und speichern Sie einen Callback, der zum Anzeigen des Bildschirms „Game Over“ verwendet 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
}

So fügen Sie ein neues Widget (GameOverScreen) hinzu:

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 QuestionCard-Widget das Card-Widget durch ein OpenContainer-Widget aus dem animations-Paket und fügen Sie zwei neue Felder für den viewModel- 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

Herzlichen Glückwunsch! Sie haben einer Flutter-App erfolgreich Animationseffekte hinzugefügt und die Kernkomponenten des Animationssystems von Flutter kennengelernt. Sie haben Folgendes gelernt:

  • So verwendest du ImplicitlyAnimatedWidget
  • So verwendest du ExplicitlyAnimatedWidget
  • Curves und Tweens auf eine Animation anwenden
  • Vordefinierte Übergangswidgets wie AnimatedSwitcher oder PageRouteBuilder verwenden
  • Verwendung von vorgefertigten Animationseffekten aus dem animations-Paket, z. B. FadeThroughTransition und OpenContainer
  • So passen Sie die Standardübergangsanimation an und fügen Unterstützung für die Funktion „Vorhersagende Zurück-Geste“ unter Android hinzu.

3026390ad413769c.gif

Nächste Schritte

Hier sind einige Codelabs:

Sie können auch die Beispiel-App für Animationen herunterladen, in der verschiedene Animationstechniken vorgestellt werden.

Weitere Informationen

Weitere Ressourcen zu Animationen finden Sie auf flutter.dev:

Oder sehen Sie sich diese Artikel auf Medium an:

Referenzdokumente