Animationen mit Flutter - das exensio Logo als Ladeanimation

von

Animiertes exensio-Logo mittels Flutter.

Bisher haben wir Flutter in unserem Blog nur in groben Zügen betrachtet und ausgewählte Vorzüge sowie Herausforderungen vorgestellt. In diesem Blogpost möchte ich tiefer die Möglichkeiten der Animation eintauchen, die Flutter bietet. Da das Framework nicht auf native Views zurückgreift, sondern jeden Pixel selbst zeichnet, kann quasi alles animiert werden – zum Teil ohne, dass man etwas dafür tun müsste.

Zunächst möchte ich kurz die verschiedenen Methoden vorstellen, die Flutter bietet, um Animationen umzusetzen. Der zweite Teil des Posts zeigt, wie wir das exensio Logo als Ladeanimation umgesetzt haben. Hier schon eine interaktive Vorschau auf das Ergebnis.

Sollte der Editor nicht richtig laden, kann das Ergebnis auch direkt auf Dartpad eingesehen werden.

Animation in Flutter

Bei der Verwendung einer MaterialApp bietet Flutter bereits ohne weiteres Zutun einige Animationen, die das native Look & Feel der Zielplattformen abbilden. Beim Navigieren zwischen Seiten blendet das Framework automatisch über und ein eingebundener Drawer gleitet vom Bildschirmrand über den aktuellen Screen. Doch welche Optionen bieten sich Entwicklern, um gezielt Animationen einzusetzen oder vorhandene zu überschreiben?

Flutter unterscheidet hier grob zwei Klassen: Implizite und Explizite Animationen. Für viele der am häufigsten genutzten Widgets bietet Flutter jeweils implizite und explizite Animationsvarianten.

Implizite Animationen

Widgets, die implizite Animationen ermöglichen, sind in der Regel durch das Präfix „Animated“ zu erkennen – z.B. AnimatedOpacity. Sie sind am einfachsten zu bedienen, bieten aber im Gegenzug den geringsten Grad an Kontrolle. Sobald sich das zu animierende Attribut ändert interpolieren diese Widgets automatisch von Start- zu Endwert und stoppen. Diese Widgets erlauben keine Unterbrechung/Wideraufnahme oder automatische Wiederholung der Animation, benötigen aber sonst kein weiteres Setup.

Der folgende Code ist ein Beispiel für AnimatedOpacity. Dem Widget muss lediglich der aktuelle Wert für die Deckkraft sowie eine Duration neben dem Kindelement übergeben werden. Sobald sich die Deckkraft ändert – z.B. durch setState() – animiert Flutter über die Dauer der gegebenen Duration hinweg zum neuen Wert.

AnimatedOpacity(
  opacity: _visible ? 1.0 : 0.0,
  duration: Duration(seconds: 1),
  child: FlutterLogo(),
)

Ein weiteres Beispiel hierfür ist das sogenannte Hero-Widget – welches bisher als einziges aus der Namenskonvention fällt. Mit ihm ist es möglich, ein Widget zwischen zwei Screens zu animieren. Ein klassisches Beispiel hierfür ist eine Liste mit Vorschaubildern, die sich beim Klick vergrößern und in die Vollbild-Variante übergehen. Dafür müssen lediglich das Vorschau- sowie Vollbild mit einem Hero-Widget umschlossen werden, in denen derselbe eindeutige Key definiert ist. Mehr ist nicht nötig.

Explizite Animationen

Mehr Kontrolle über die Animation bieten die expliziten Transition-Widgets. Statt Animated sind sie mit dem Postfix Transition gekennzeichnet – z.B. FadeTransition. Auf den ersten Blick verhalten sich diese sehr ähnlich zu ihren impliziten Gegenstücken, erwarten aber statt einer Duration mit Attributwert ein explizites Animations-Argument neben dem Kind. Eine solche Animation wird mittels eines AnimationControllers erzeugt, der manuell auch wieder aufgeräumt werden muss. Dafür bietet der Controller aber volle Kontrolle über den Ablauf: so kann die Animation vorwärts wie rückwärts abgespielt, unterbrochen oder wiederholt werden. Zudem kann eine sogenannte Easing-Kurve definiert werden, um den Ablauf der Animation weiter zu spezifizieren.

Das folgende Beispiel entspricht der AnimatedOpacity von oben, wiederholt sich aber dauerhaft.

class _AnimatedExampleState extends State<AnimatedExample> with SingleTickerProviderStateMixin {
  AnimationController _controller;
  Animation<double> _animation;
	
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(milliseconds: 1000),
      vsync: this,
      value: 0,
      lowerBound: 0,
      upperBound: 1
    );
    _animation = CurvedAnimation(parent: _controller, curve: Curves.easeInOut);
    _controller.repeat();
  }
	
  @override
  dispose() {
    _controller.dispose();
    super.dispose();
  }
    
  Widget build(BuildContext context) {
    return FadeTransition(
      opacity: _animation,
      child: FlutterLogo(),
    );
  }
}

Eine Übersicht der Animations-Widgest ist in der Dokumentation verfügbar.

exensio-Logo als Ladeanimation

Um nun das exensio-Logo als Ladeanimation nachzubauen müssen wir uns im ersten Schritt darüber Gedanken machen, welche Teile des Logos animiert werden sollen und welche Art von Animation benötigt wird. Die folgende Abbildung teilt das Logo in seine Einzelteile auf: der exensio-Schriftzug sowie der Subtitel können statisch als PNGs hinterlegt werden, lediglich die drei Punkte sind aktiv an der Animation beteiligt.

Aufteilung des exensio-Logos.

Da die Ladeanimation sich so lange wiederholen soll, bis ein Vorgang abgeschlossen ist, müssen wir, wie im letzten Abschnitt gelernt, auf explizite Animation zurückgreifen. Zudem sollen die drei Punkte nacheinander ihre Animation starten, auch hierfür benötigen wir Zugriff auf die Controller. Betrachten wir nun den Code für einen animierten Punkt genauer:

class AnimatedDot extends StatelessWidget {
  final Animation animation;
  final double dimension;

  const AnimatedDot({Key key, this.animation, this.dimension}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      width: dimension,
      height: dimension,
      child: FadeTransition(
        opacity: animation,
        child: ScaleTransition(
          scale: animation,
          child: Container(
            width: dimension,
            height: dimension,
            decoration: new BoxDecoration(
              color: Color(0xff75042f),
              shape: BoxShape.circle,
            ),
          ),
        ),
      ),
    );
  }
}

Gehen wir einmal von innen nach außen durch die build-Funktion. Der innerste Container beschreibt lediglich einen Kreis im exensio-Rot mit den übergebenen Dimensionen. Für die eigentliche Animation sind die nächsten zwei umschließenden Widgets verantwortlich. Zum einen animieren wir die Größe des Punktes mittels der ScaleTransition proportional zwischen seiner Originalgröße und 0. Wie diese Transition abläuft wird mit der übergebenen Animation beschrieben, auf die wir gleich einen Blick werfen. Gleichzeitig wird auch die Deckkraft mittels der FadeTransition anhand derselben Animation verändert. Der letzte, äußerste Container dient lediglich dazu, dass unser Widget immer seine maximale Dimension einnimmt, um keine ungewünschte Verschiebung im Layout zu erzeugen.

Um die beiden Transitions pro Punkt zu steuern benötigen wir nun drei AnimationController, welche für die verzögerten Übergänge zuständig sind. Dafür wird an jedem Controller ein StatusListener registriert, der nach Abschluss der Animation die nächste startet. Sobald der letzte Punkt erreicht wurde wird die Animation des ersten Punktes in entgegengesetzter Richtung abgespielt und löst im Ausgangsstadium wieder jeweils den nächsten Punkt aus. Dieser Ablauf wiederholt sich, bis die Animation gestoppt wird.

// initialization of _animationControllers
// each _animationController will have same animation duration
_animationControllers = List.generate(numberOfDots, (index) {
  return AnimationController(
    vsync: this, duration: Duration(milliseconds: animationDuration));
  },
).toList();

// define _curves for the animation
_curves = List.generate(numberOfDots, (index) => CurvedAnimation(
  parent: _animationControllers[index],
  curve: Curves.easeInOutCubic)
);

// define _animations with Tween
_animations = List.generate(numberOfDots, 
  (index) => Tween<double>(begin: 1, end: 0).animate(_curves[index]));

for (int i = 0; i < numberOfDots; i++) {
  _animationControllers[i].addStatusListener((status) {
    // If the animation is stopped at the end.
    if (status == AnimationStatus.completed) {
      // If it is not last dot then start the animation of next dot.
      if (i != numberOfDots - 1) {
        _animationControllers[i + 1].forward();
      }
    }
    // If the animation is stopped at the beginning.
    if (status == AnimationStatus.dismissed) {
      // If it is not last dot then reverse the animation of next dot.
      if (i != numberOfDots - 1) {
        _animationControllers[i + 1].reverse();
      }
    }
    // Repeat until stopped in initial position
    if (i == numberOfDots - 1 && status == AnimationStatus.completed) {
      _animationControllers[0].reverse();
    }
    if (widget.runAnimation && i == numberOfDots - 1 && status == AnimationStatus.dismissed) {
      _animationControllers[0].forward();
    }
  });
}

Damit ist der schwierigste Teil geschafft und es bleibt nur noch das Anordnen der einzelnen Teile entsprechend der oben dargestellten Grafik mittels Rows und Columns. Der gesamte Code hierfür kann am Anfang des Posts oder auf Dartpad eingesehen werden.

Fazit

Alles in Allem sind Animationen mittels Flutter relativ einfach umzusetzen, wenn man sie in ihre grundlegenden Teile herunterbricht. Das Framework bietet reichlich Unterstützung mit impliziten und expliziten Animations-Widgets, sodass für die meisten Anwendungszwecke bereits eine Lösung zur Verfügung steht oder – wie im gegebenen Beispiel veranschaulicht - durch Kombination dieser Bausteine gefunden werden kann. Ich hoffe ich konnte mit diesem Blogpost einen kleinen Einstieg in die Welt der Animationen bieten und wünsche viel Spaß beim Ausprobieren.

Links


Flutter Reihe:

Kategorien: DartexensioFlutter

Zurück