İçeriğe geç / Skip to content / Zum Inhalt

Flutter: Maximale Wirkung mit impliziten Animationen

Ahmet Balaman

Zuletzt aktualisiert:

5 Min. Lesezeit

FlutterAnimationAnimatedContainerImplicit AnimationUI/UX
Flutter: Maximale Wirkung mit impliziten Animationen

Denken Sie bei Animationen an komplexe Formeln, AnimationController und seitenweise Code? Was, wenn ich Ihnen sage, dass sich Ihre Widgets schon durch eine einzige Änderung geschmeidig animieren lassen?

Genau das leisten die impliziten Animationen von Flutter. Wenn Sie setState aufrufen, wechselt Ihr Widget automatisch vom alten zum neuen Wert. Magie? Nein, Flutter-Ingenieurskunst.

Explizit oder implizit: Wo liegt der Unterschied?

Es gibt zwei Arten von Animationen:

Eine implizite Animation braucht nur eine Wertänderung, eine explizite baut die Kette aus AnimationController, Tween und AnimatedBuilder

Explizite Animationen

Implizite Animationen

  • Kein Controller nötig
  • Für die allermeisten Fälle ausreichend
  • Weniger Code, tolle Ergebnisse
  • Beispiel: Buttonfarbe, Größenänderung, Positionswechsel

Goldene Regel: Wenn Sie die Animation nicht von Anfang bis Ende steuern müssen, nehmen Sie implizite Animationen.

Live-Demo

Sie können dieses Widget im interaktiven Beispiel unten ausprobieren:

💡 Falls das Beispiel oben nicht lädt, klicken Sie auf DartPad, um es in einem neuen Tab auszuführen.

AnimatedContainer: das Schweizer Taschenmesser

AnimatedContainer ist die animierte Variante des Container. Sobald Sie eine Eigenschaft ändern, animiert er automatisch. Falls Sie auffrischen möchten, was Padding, Alignment und Decoration im gewöhnlichen Container bewirken, hilft der Beitrag zu Widget-Baum und Layout.

Grundlegende Verwendung

class AnimatedBoxDemo extends StatefulWidget {
  @override
  _AnimatedBoxDemoState createState() => _AnimatedBoxDemoState();
}

class _AnimatedBoxDemoState extends State<AnimatedBoxDemo> {
  bool _isExpanded = false;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: GestureDetector(
        onTap: () {
          setState(() {
            _isExpanded = !_isExpanded;
          });
        },
        child: AnimatedContainer(
          duration: Duration(milliseconds: 300),
          width: _isExpanded ? 200 : 100,
          height: _isExpanded ? 200 : 100,
          decoration: BoxDecoration(
            color: _isExpanded ? Colors.blue : Colors.red,
            borderRadius: BorderRadius.circular(_isExpanded ? 50 : 10),
          ),
          child: Center(
            child: Text(
              'Tap Me',
              style: TextStyle(color: Colors.white),
            ),
          ),
        ),
      ),
    );
  }
}

Ein einziger setState-Aufruf animiert drei Eigenschaften gleichzeitig:

  • Breite und Höhe
  • Farbe
  • Eckenradius

Was kann AnimatedContainer?

AnimatedContainer kann alle Eigenschaften des Container animieren:

1. Größenanimation

AnimatedContainer(
  duration: Duration(seconds: 1),
  width: isLarge ? 300 : 100,
  height: isLarge ? 300 : 100,
  child: FlutterLogo(),
)

2. Farbübergang

AnimatedContainer(
  duration: Duration(milliseconds: 500),
  color: isActive ? Colors.green : Colors.grey,
  child: Text('Status'),
)

3. Padding-Animation

AnimatedContainer(
  duration: Duration(milliseconds: 300),
  padding: EdgeInsets.all(isExpanded ? 32 : 8),
  child: Icon(Icons.star),
)

4. Gradient-Animation

AnimatedContainer(
  duration: Duration(milliseconds: 800),
  decoration: BoxDecoration(
    gradient: LinearGradient(
      colors: isDarkMode 
          ? [Colors.black, Colors.grey[900]!] 
          : [Colors.blue, Colors.purple],
    ),
  ),
)

5. Transform (Drehen/Skalieren)

AnimatedContainer(
  duration: Duration(milliseconds: 400),
  transform: Matrix4.rotationZ(isRotated ? 3.14 : 0),
  child: Icon(Icons.refresh),
)

Weitere implizite Animations-Widgets

Flutter bietet für unterschiedliche Anforderungen spezialisierte implizite Animations-Widgets:

AnimatedOpacity – Übergänge der Sichtbarkeit

Perfekt für „Fade-in“-Effekte wie auf Websites:

class FadeInDemo extends StatefulWidget {
  @override
  _FadeInDemoState createState() => _FadeInDemoState();
}

class _FadeInDemoState extends State<FadeInDemo> {
  bool _isVisible = false;

  @override
  void initState() {
    super.initState();
    // Show after 1 second
    Future.delayed(Duration(seconds: 1), () {
      if (mounted) setState(() => _isVisible = true);
    });
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedOpacity(
      opacity: _isVisible ? 1.0 : 0.0,
      duration: Duration(milliseconds: 600),
      child: Text(
        'Hello World!',
        style: TextStyle(fontSize: 32),
      ),
    );
  }
}

Einsatzfälle:

  • Hinweismeldungen
  • Inhalte nach dem Laden einblenden
  • Hover-Effekte
  • Onboarding-Screens

AnimatedPositioned – Positionswechsel im Stack

class SlidingMenuDemo extends StatefulWidget {
  @override
  _SlidingMenuDemoState createState() => _SlidingMenuDemoState();
}

class _SlidingMenuDemoState extends State<SlidingMenuDemo> {
  bool _isMenuOpen = false;

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        // Main content
        Container(color: Colors.white),
        
        // Sliding menu
        AnimatedPositioned(
          duration: Duration(milliseconds: 300),
          curve: Curves.easeInOut,
          left: _isMenuOpen ? 0 : -250,
          top: 0,
          bottom: 0,
          width: 250,
          child: Container(
            color: Colors.blue,
            child: ListView(
              children: [
                ListTile(title: Text('Menu 1', style: TextStyle(color: Colors.white))),
                ListTile(title: Text('Menu 2', style: TextStyle(color: Colors.white))),
                ListTile(title: Text('Menu 3', style: TextStyle(color: Colors.white))),
              ],
            ),
          ),
        ),
        
        // Hamburger button
        Positioned(
          top: 50,
          left: 20,
          child: IconButton(
            icon: Icon(Icons.menu),
            onPressed: () {
              setState(() => _isMenuOpen = !_isMenuOpen);
            },
          ),
        ),
      ],
    );
  }
}

Einsatzfälle:

  • Drawer-Menüs
  • Position des Floating Action Button
  • Spielfiguren
  • Parallax-Effekte

AnimatedAlign – Übergänge der Ausrichtung

Widgets an eine andere Position innerhalb des Elternteils bewegen:

class BouncingBallDemo extends StatefulWidget {
  @override
  _BouncingBallDemoState createState() => _BouncingBallDemoState();
}

class _BouncingBallDemoState extends State<BouncingBallDemo> {
  Alignment _alignment = Alignment.topLeft;

  void _moveBall() {
    setState(() {
      _alignment = _alignment == Alignment.topLeft 
          ? Alignment.bottomRight 
          : Alignment.topLeft;
    });
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _moveBall,
      child: Container(
        width: 300,
        height: 300,
        color: Colors.grey[200],
        child: AnimatedAlign(
          alignment: _alignment,
          duration: Duration(seconds: 1),
          curve: Curves.bounceOut,
          child: Container(
            width: 50,
            height: 50,
            decoration: BoxDecoration(
              color: Colors.red,
              shape: BoxShape.circle,
            ),
          ),
        ),
      ),
    );
  }
}

AnimatedPadding – Animation der Abstände

AnimatedPadding(
  duration: Duration(milliseconds: 300),
  padding: EdgeInsets.all(isSelected ? 20 : 8),
  child: Card(
    child: Text('Selected Card'),
  ),
)

AnimatedDefaultTextStyle – Übergänge des Textstils

class TextStyleDemo extends StatefulWidget {
  @override
  _TextStyleDemoState createState() => _TextStyleDemoState();
}

class _TextStyleDemoState extends State<TextStyleDemo> {
  bool _isLarge = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => setState(() => _isLarge = !_isLarge),
      child: AnimatedDefaultTextStyle(
        duration: Duration(milliseconds: 300),
        style: TextStyle(
          fontSize: _isLarge ? 48 : 24,
          color: _isLarge ? Colors.red : Colors.blue,
          fontWeight: _isLarge ? FontWeight.bold : FontWeight.normal,
        ),
        child: Text('Touch Me'),
      ),
    );
  }
}

AnimatedCrossFade – Übergang zwischen zwei Widgets

Wechselt geschmeidig zwischen zwei Widgets:

class ToggleViewDemo extends StatefulWidget {
  @override
  _ToggleViewDemoState createState() => _ToggleViewDemoState();
}

class _ToggleViewDemoState extends State<ToggleViewDemo> {
  bool _showFirst = true;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        AnimatedCrossFade(
          firstChild: Container(
            width: 200,
            height: 200,
            color: Colors.blue,
            child: Center(child: Text('First Widget', style: TextStyle(color: Colors.white))),
          ),
          secondChild: Container(
            width: 200,
            height: 200,
            color: Colors.red,
            child: Center(child: Text('Second Widget', style: TextStyle(color: Colors.white))),
          ),
          crossFadeState: _showFirst 
              ? CrossFadeState.showFirst 
              : CrossFadeState.showSecond,
          duration: Duration(milliseconds: 500),
        ),
        SizedBox(height: 20),
        ElevatedButton(
          onPressed: () => setState(() => _showFirst = !_showFirst),
          child: Text('Switch'),
        ),
      ],
    );
  }
}

Einsatzfälle:

  • Zwischen Listen- und Rasteransicht umschalten
  • Leeren Zustand ein- und ausblenden
  • Wechsel zwischen Login- und Registrierungsformular
  • Spielmodus wechseln

AnimatedSwitcher – beliebiger Widget-Wechsel

Flexibler als AnimatedCrossFade:

class CounterWithAnimation extends StatefulWidget {
  @override
  _CounterWithAnimationState createState() => _CounterWithAnimationState();
}

class _CounterWithAnimationState extends State<CounterWithAnimation> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        AnimatedSwitcher(
          duration: Duration(milliseconds: 300),
          transitionBuilder: (child, animation) {
            return ScaleTransition(scale: animation, child: child);
          },
          child: Text(
            '$_count',
            key: ValueKey<int>(_count), // Important: Key required!
            style: TextStyle(fontSize: 72, fontWeight: FontWeight.bold),
          ),
        ),
        SizedBox(height: 20),
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            IconButton(
              icon: Icon(Icons.remove),
              onPressed: () => setState(() => _count--),
            ),
            SizedBox(width: 20),
            IconButton(
              icon: Icon(Icons.add),
              onPressed: () => setState(() => _count++),
            ),
          ],
        ),
      ],
    );
  }
}

Profi-Tipp: Der Parameter key ist bei AnimatedSwitcher erforderlich. Flutter erkennt den Widget-Wechsel anhand des Keys.

Duration und Curve: die Seele der Animation

Duration

Legt fest, wie lange die Animation dauert:

// Very fast - Change without drawing attention
Duration(milliseconds: 150)

// Standard - Ideal for most uses
Duration(milliseconds: 300)

// Slow - To draw attention
Duration(milliseconds: 600)

// Very slow - Dramatic effects
Duration(seconds: 1)

Tipp zur Nutzererfahrung:

  • Kleine Änderungen: 150–200 ms
  • Mittlere Änderungen: 300–400 ms
  • Große Änderungen: 500–800 ms
  • Länger als eine Sekunde: lässt Nutzer warten

Curve

Das Beschleunigungs- und Abbremsverhalten der Animation:

// Linear - Constant speed (generally not used)
curve: Curves.linear

// Slow start, speed up, slow end - Most natural
curve: Curves.easeInOut

// Slow start
curve: Curves.easeIn

// Slow end
curve: Curves.easeOut

// Elastic - Bounce back feel
curve: Curves.elasticOut

// Bounce - Bouncing effect
curve: Curves.bounceOut

// Fast start, slow end
curve: Curves.decelerate

// Overshoot and return
curve: Curves.easeOutBack

Kurven im Vergleich

class CurveComparison extends StatefulWidget {
  @override
  _CurveComparisonState createState() => _CurveComparisonState();
}

class _CurveComparisonState extends State<CurveComparison> {
  bool _expanded = false;

  Widget _buildAnimatedBox(String label, Curve curve) {
    return Column(
      children: [
        Text(label, style: TextStyle(fontSize: 12)),
        AnimatedContainer(
          duration: Duration(seconds: 1),
          curve: curve,
          width: _expanded ? 200 : 50,
          height: 50,
          color: Colors.blue,
        ),
      ],
    );
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        _buildAnimatedBox('linear', Curves.linear),
        SizedBox(height: 10),
        _buildAnimatedBox('easeInOut', Curves.easeInOut),
        SizedBox(height: 10),
        _buildAnimatedBox('bounceOut', Curves.bounceOut),
        SizedBox(height: 10),
        _buildAnimatedBox('elasticOut', Curves.elasticOut),
        SizedBox(height: 20),
        ElevatedButton(
          onPressed: () => setState(() => _expanded = !_expanded),
          child: Text('Start Animations'),
        ),
      ],
    );
  }
}

Einsatzfälle aus der Praxis

1. Ausklappbare Karte

class ExpandableCard extends StatefulWidget {
  final String title;
  final String content;

  ExpandableCard({required this.title, required this.content});

  @override
  _ExpandableCardState createState() => _ExpandableCardState();
}

class _ExpandableCardState extends State<ExpandableCard> {
  bool _isExpanded = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => setState(() => _isExpanded = !_isExpanded),
      child: AnimatedContainer(
        duration: Duration(milliseconds: 300),
        curve: Curves.easeInOut,
        padding: EdgeInsets.all(16),
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.circular(12),
          boxShadow: [
            BoxShadow(
              color: Colors.black12,
              blurRadius: _isExpanded ? 10 : 5,
              spreadRadius: _isExpanded ? 2 : 0,
            ),
          ],
        ),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Text(
                  widget.title,
                  style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                ),
                AnimatedRotation(
                  duration: Duration(milliseconds: 300),
                  turns: _isExpanded ? 0.5 : 0,
                  child: Icon(Icons.expand_more),
                ),
              ],
            ),
            AnimatedCrossFade(
              firstChild: SizedBox.shrink(),
              secondChild: Padding(
                padding: EdgeInsets.only(top: 12),
                child: Text(widget.content),
              ),
              crossFadeState: _isExpanded 
                  ? CrossFadeState.showSecond 
                  : CrossFadeState.showFirst,
              duration: Duration(milliseconds: 300),
            ),
          ],
        ),
      ),
    );
  }
}

2. Animation eines Like-Buttons

class LikeButton extends StatefulWidget {
  @override
  _LikeButtonState createState() => _LikeButtonState();
}

class _LikeButtonState extends State<LikeButton> {
  bool _isLiked = false;
  int _likeCount = 42;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        setState(() {
          _isLiked = !_isLiked;
          _likeCount += _isLiked ? 1 : -1;
        });
      },
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          AnimatedContainer(
            duration: Duration(milliseconds: 200),
            curve: Curves.easeInOut,
            padding: EdgeInsets.all(8),
            decoration: BoxDecoration(
              color: _isLiked ? Colors.red.withValues(alpha: 0.2) : Colors.transparent,
              shape: BoxShape.circle,
            ),
            child: AnimatedSwitcher(
              duration: Duration(milliseconds: 200),
              transitionBuilder: (child, animation) {
                return ScaleTransition(scale: animation, child: child);
              },
              child: Icon(
                _isLiked ? Icons.favorite : Icons.favorite_border,
                key: ValueKey<bool>(_isLiked),
                color: _isLiked ? Colors.red : Colors.grey,
                size: 28,
              ),
            ),
          ),
          SizedBox(width: 4),
          AnimatedDefaultTextStyle(
            duration: Duration(milliseconds: 200),
            style: TextStyle(
              color: _isLiked ? Colors.red : Colors.grey,
              fontWeight: _isLiked ? FontWeight.bold : FontWeight.normal,
              fontSize: 16,
            ),
            child: Text('$_likeCount'),
          ),
        ],
      ),
    );
  }
}

3. Platzhalter beim Laden (Skeleton Screen)

class ShimmerLoading extends StatefulWidget {
  @override
  _ShimmerLoadingState createState() => _ShimmerLoadingState();
}

class _ShimmerLoadingState extends State<ShimmerLoading> {
  bool _isLoading = true;

  @override
  void initState() {
    super.initState();
    // Show content after 3 seconds
    Future.delayed(Duration(seconds: 3), () {
      if (mounted) setState(() => _isLoading = false);
    });
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedCrossFade(
      firstChild: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Container(
            width: double.infinity,
            height: 200,
            decoration: BoxDecoration(
              color: Colors.grey[300],
              borderRadius: BorderRadius.circular(12),
            ),
          ),
          SizedBox(height: 12),
          Container(
            width: 200,
            height: 20,
            color: Colors.grey[300],
          ),
          SizedBox(height: 8),
          Container(
            width: 150,
            height: 20,
            color: Colors.grey[300],
          ),
        ],
      ),
      secondChild: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Image.network(
            'https://picsum.photos/400/200',
            height: 200,
            width: double.infinity,
            fit: BoxFit.cover,
          ),
          SizedBox(height: 12),
          Text('Title Goes Here', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
          SizedBox(height: 8),
          Text('Subtitle goes here', style: TextStyle(color: Colors.grey)),
        ],
      ),
      crossFadeState: _isLoading 
          ? CrossFadeState.showFirst 
          : CrossFadeState.showSecond,
      duration: Duration(milliseconds: 500),
    );
  }
}

4. Floating Action Button verwandeln

class MorphingFAB extends StatefulWidget {
  @override
  _MorphingFABState createState() => _MorphingFABState();
}

class _MorphingFABState extends State<MorphingFAB> {
  bool _isExpanded = false;

  @override
  Widget build(BuildContext context) {
    return AnimatedContainer(
      duration: Duration(milliseconds: 300),
      curve: Curves.easeInOut,
      width: _isExpanded ? 200 : 56,
      height: 56,
      decoration: BoxDecoration(
        color: Colors.blue,
        borderRadius: BorderRadius.circular(28),
      ),
      child: Material(
        color: Colors.transparent,
        child: InkWell(
          borderRadius: BorderRadius.circular(28),
          onTap: () => setState(() => _isExpanded = !_isExpanded),
          child: Padding(
            padding: EdgeInsets.symmetric(horizontal: 16),
            child: Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                Icon(Icons.add, color: Colors.white),
                if (_isExpanded) ...[
                  SizedBox(width: 12),
                  Text(
                    'Add New',
                    style: TextStyle(color: Colors.white, fontSize: 16),
                  ),
                ],
              ],
            ),
          ),
        ),
      ),
    );
  }
}

Tipps für die Performance

1. Verwenden Sie const-Konstruktoren

// ❌ Bad - New instance every rebuild
AnimatedContainer(
  duration: Duration(milliseconds: 300),
  child: Text('Static Text'),
)

// ✅ Good - Const child is cached
AnimatedContainer(
  duration: Duration(milliseconds: 300),
  child: const Text('Static Text'),
)

2. Unveränderte Eigenschaften sind kein Problem, wählen Sie das passende Widget

AnimatedContainer interpoliert nur die Eigenschaften, deren Wert sich ändert. Eine width und height, die gleich bleiben, kosten praktisch nichts; sie in einen separaten äußeren Container zu verschieben, bringt keine Performance. Der eigentliche Gewinn liegt darin, für eine einzelne Animation das spezialisierte Widget zu wählen: AnimatedOpacity für Deckkraft, AnimatedAlign für Ausrichtung, AnimatedPadding für Innenabstand. Der Code zeigt seine Absicht dann deutlicher.

// Nur die Farbe ändert sich: AnimatedContainer passt, feste Größen kosten nichts
AnimatedContainer(
  duration: const Duration(milliseconds: 300),
  width: 100,
  height: 100,
  color: selectedColor,
)

// Nur die Deckkraft ändert sich: das spezialisierte Widget ist klarer
AnimatedOpacity(
  opacity: isVisible ? 1.0 : 0.0,
  duration: const Duration(milliseconds: 300),
  child: const FlutterLogo(size: 100),
)

3. Optimieren Sie mit RepaintBoundary

RepaintBoundary(
  child: AnimatedOpacity(
    opacity: isVisible ? 1.0 : 0.0,
    duration: Duration(milliseconds: 300),
    child: HeavyWidget(),
  ),
)

4. Animieren Sie nicht zu vieles gleichzeitig

// ❌ Bad - 100 widgets animating at once
ListView.builder(
  itemCount: 100,
  itemBuilder: (context, index) {
    return AnimatedContainer(
      duration: Duration(seconds: 1),
      height: heights[index],
      child: ListTile(title: Text('Item $index')),
    );
  },
)

// ✅ Good - Only visible ones should animate

Häufige Fehler und ihre Lösungen

Fehler 1: Key beim AnimatedSwitcher vergessen

// ❌ Wrong - Animation won't work
AnimatedSwitcher(
  duration: Duration(milliseconds: 300),
  child: Text('$count'), // No key!
)

// ✅ Correct
AnimatedSwitcher(
  duration: Duration(milliseconds: 300),
  child: Text('$count', key: ValueKey(count)),
)

Fehler 2: zu lange Dauer

// ❌ User waits 3 seconds
AnimatedContainer(
  duration: Duration(seconds: 3),
  color: newColor,
)

// ✅ Fast and responsive
AnimatedContainer(
  duration: Duration(milliseconds: 300),
  color: newColor,
)

Fehler 3: falsche Wahl der Kurve

// ❌ Linear looks too mechanical
AnimatedContainer(
  duration: Duration(milliseconds: 300),
  curve: Curves.linear,
  width: newWidth,
)

// ✅ Natural looking
AnimatedContainer(
  duration: Duration(milliseconds: 300),
  curve: Curves.easeInOut,
  width: newWidth,
)

Alle impliziten Animations-Widgets

Flutter bietet über 15 implizite Animations-Widgets:

Widget Was es animiert
AnimatedContainer Alle Eigenschaften des Container
AnimatedOpacity Deckkraft (Transparenz)
AnimatedPadding Padding (Abstände)
AnimatedPositioned Position im Stack
AnimatedAlign Ausrichtung
AnimatedDefaultTextStyle Textstil
AnimatedPhysicalModel Schatten und Elevation
AnimatedCrossFade Übergang zwischen zwei Widgets
AnimatedSwitcher Beliebiger Widget-Wechsel
AnimatedSize Größe des Widgets
AnimatedRotation Drehwinkel
AnimatedScale Skalierung
AnimatedSlide Verschiebung (Offset)
AnimatedFractionallySizedBox Größe relativ zum Elternteil
AnimatedTheme Theme-Änderungen

Zusammenfassung

  • Implizite Animationen: Animation ohne Zusatzcode (setState genügt)
  • AnimatedContainer: Das vielseitigste implizite Animations-Widget
  • Duration: Dauer der Animation (ideal: 200–400 ms)
  • Curve: Beschleunigungs- und Abbremsverhalten (ideal: easeInOut)
  • AnimatedOpacity: Für Ein- und Ausblendeffekte
  • AnimatedPositioned: Positionswechsel im Stack
  • AnimatedCrossFade: Übergang zwischen zwei Widgets
  • AnimatedSwitcher: Beliebiger Widget-Wechsel (Key erforderlich!)
  • Performance: const, RepaintBoundary, keine unnötigen Animationen
  • UX: Kurze Dauer, natürliche Kurven, gezielter Einsatz

Implizite Animationen gehören zu den stärksten Funktionen von Flutter: maximale Wirkung bei minimalem Code, und meist genügt die Kombination aus setState und einem Animated*-Widget. Findet die Animation nicht innerhalb eines Bildschirms, sondern zwischen zwei Seiten statt, ist die Hero-Animation das passende Werkzeug: Sie trägt dasselbe Element von Seite zu Seite.

Häufig gestellte Fragen

Wann reicht eine implizite Animation nicht mehr?

Sobald Sie eine Animation anhalten, rückwärts laufen lassen, wiederholen oder mehrere Animationen verketten müssen, stoßen die impliziten Widgets an ihre Grenzen. Dann wechseln Sie zu einer expliziten Animation mit einem AnimationController.

Warum animiert mein AnimatedContainer überhaupt nicht?

Meist wird das Widget gar nicht neu aufgebaut: Haben Sie den Wert außerhalb von setState geändert, fehlen Flutter alter und neuer Wert für die Interpolation. Die zweithäufigste Ursache ist ein bei jedem Build neu vergebener key – Flutter hält das Widget dann für ein anderes und zeichnet es neu, statt überzublenden.

Welche Dauer ist die richtige?

Für kleine Rückmeldungen wie einen Farb- oder Skalierungswechsel sind 150–250 ms passend, für größere Bewegungen wie ein aufklappendes Panel 250–400 ms. Alles jenseits einer Sekunde lässt die App träge statt geschmeidig wirken.

Kommentare