Kodokon kodokon.com

Animationen: implizit und explizit unter der Haube

Verstehe die innere Mechanik impliziter Animationen und steuere deinen eigenen AnimationController präzise.

10 Min. · 3 Fragen

Diese Lektion in Kodokon öffnen

AnimatedContainer gehört zur Familie der impliziten Animationen: Du änderst einen Zielwert und das Framework animiert den Übergang. Unter der Haube hält jedes ImplicitlyAnimatedWidget einen AnimationController, der in seinem State versteckt ist. Wenn eine neue Konfiguration eintrifft, läuft didUpdateWidget über forEachTween durch die Eigenschaften und richtet jeden Tween neu aus: Die Grenze begin wird zum aktuellen interpolierten Wert, die Grenze end zum neuen Ziel, dann startet der Controller wieder bei null. Eine wertvolle Folge davon: Wenn du das Ziel mitten in der Animation änderst, gibt es keinen sichtbaren Sprung - die Animation läuft genau dort weiter, wo sie war.

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

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

  @override
  State<SelectableCard> createState() =>
      _SelectableCardState();
}

class _SelectableCardState extends State<SelectableCard> {
  bool _selected = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () =>
          setState(() => _selected = !_selected),
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 300),
        curve: Curves.easeOutCubic,
        width: _selected ? 220 : 160,
        height: 96,
        decoration: BoxDecoration(
          color: _selected
              ? Colors.indigo
              : Colors.indigo.shade200,
          borderRadius: BorderRadius.circular(
            _selected ? 28 : 12,
          ),
        ),
      ),
    );
  }
}
Größe, Farbe und Radius, animiert durch ein einziges setState.

Die impliziten Animationen decken die Eigenschaften ab, die das Framework vorsieht, und TweenAnimationBuilder verallgemeinert das Verfahren auf jeden interpolierbaren Wert. Sobald du die Animation steuern musst - anhalten, umkehren, wiederholen, auf den Status reagieren -, wechselst du zum expliziten AnimationController. Der Parameter vsync erwartet einen TickerProvider: Das Mixin erzeugt einen Ticker, einen Callback, den das SchedulerBinding bei jedem Frame aufruft. Eine wenig bekannte Feinheit: TickerMode legt die Ticker still, die zu einem verborgenen Teilbaum gehören (zum Beispiel unter einer überdeckten Route). Die Zeit läuft weiter, aber es wird kein Callback mehr ausgeführt - null CPU-Verbrauch für unsichtbare Animationen.

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

class RevealPanel extends StatefulWidget {
  const RevealPanel({super.key, required this.child});

  final Widget child;

  @override
  State<RevealPanel> createState() => _RevealPanelState();
}

class _RevealPanelState extends State<RevealPanel>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller =
      AnimationController(
    vsync: this,
    duration: const Duration(milliseconds: 400),
    reverseDuration: const Duration(milliseconds: 200),
  );

  late final Animation<double> _opacity = _controller
      .drive(CurveTween(curve: Curves.easeOutCubic));

  void toggle() {
    switch (_controller.status) {
      case AnimationStatus.forward:
      case AnimationStatus.completed:
        _controller.reverse();
      case AnimationStatus.reverse:
      case AnimationStatus.dismissed:
        _controller.forward();
    }
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: toggle,
      child: FadeTransition(
        opacity: _opacity,
        child: widget.child,
      ),
    );
  }
}
reverseDuration: Hinweg und Rückweg bekommen jeweils ihr eigenes Tempo.

Ein Controller ist nichts anderes als eine Animation<double>, die von 0 bis 1 läuft: Setze sie zusammen, statt sie roh zu verbrauchen. controller.drive(CurveTween(...)) erzeugt eine abgeleitete Animation ohne zusätzlichen Zustand. Um den Wert auf den Baum anzuwenden, bevorzuge die Transition-Widgets (FadeTransition, RotationTransition): Sie hören die Animation direkt auf Ebene des Render Objects ab, ohne pro Frame ein einziges Widget neu zu bauen. Wenn die Transformation beliebig ist, erledigt AnimatedBuilder die Aufgabe - vorausgesetzt, du übergibst den statischen Teilbaum über child: Er wird nur einmal gebaut und dann bei jedem Frame wieder in builder eingespeist. Das ist die Optimierung mit dem größten Ertrag bei expliziten Animationen.

DART
import 'dart:math' as math;

import 'package:flutter/material.dart';

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

  @override
  State<Orbit> createState() => _OrbitState();
}

class _OrbitState extends State<Orbit>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller =
      AnimationController(
    vsync: this,
    duration: const Duration(seconds: 4),
  )..repeat();

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      child: const Icon(Icons.satellite_alt, size: 32),
      builder: (context, child) {
        final angle = _controller.value * 2 * math.pi;
        return Transform.translate(
          offset: Offset(
            math.cos(angle) * 80,
            math.sin(angle) * 80,
          ),
          child: child,
        );
      },
    );
  }
}
Das als child übergebene Icon wird nur einmal gebaut.

Wissenscheck

Stelle sicher, dass du die wichtigsten Punkte dieser Lektion behalten hast.

  1. Ein AnimatedContainer bekommt eine neue Zielfarbe, während die vorherige Animation erst zur Hälfte gelaufen ist. Was passiert?
    • Der Tween wird neu ausgerichtet: Er startet vom aktuellen Wert aus zum neuen Ziel, ohne sichtbaren Sprung
    • Die laufende Animation wird zuerst zu Ende geführt, dann startet die neue
    • Das Widget springt sofort auf die neue Farbe
  2. Wofür ist der Parameter vsync eines AnimationControllers genau da?
    • Er liefert einen mit den Frames synchronisierten Ticker, den TickerMode stilllegen kann, wenn der Teilbaum verborgen ist
    • Er synchronisiert den Controller mit den anderen Animationen der App
    • Er begrenzt die Animation auf 30 Bilder pro Sekunde
    • Er aktiviert die Hardwarebeschleunigung der GPU
  3. Warum übergibst du den statischen Teilbaum über den Parameter child von AnimatedBuilder?
    • Er wird einmal gebaut und dann in builder wieder eingespeist, statt bei jedem Frame neu gebaut zu werden
    • Es ist Pflicht: builder kann keine neuen Widgets zurückgeben
    • Es gibt den Controller am Ende automatisch frei