Flutter: Maximale Wirkung mit impliziten Animationen
Zuletzt aktualisiert:
5 Min. Lesezeit

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:
Explizite Animationen
- Erfordern einen AnimationController
- Werden eingesetzt, wenn Sie volle Kontrolle wollen
- Mehr Code, mehr Flexibilität
- Beispiel: komplexe, verkettete Animationen
- Ausführlich: explizite Animationen mit dem AnimationController
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.easeOutBackKurven 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 animateHä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-WidgetDuration: Dauer der Animation (ideal: 200–400 ms)Curve: Beschleunigungs- und Abbremsverhalten (ideal: easeInOut)AnimatedOpacity: Für Ein- und AusblendeffekteAnimatedPositioned: Positionswechsel im StackAnimatedCrossFade: Übergang zwischen zwei WidgetsAnimatedSwitcher: 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.
Verwandte Artikel
Flutter: Hero-Animationen und Seitenübergänge
Flutter-Hero-Animationen: Tag-Zuordnung, Dauer und Kurve des Flugs, flightShuttleBuilder, typische Fehler und eigene Seitenübergänge mit PageRouteBuilder.
Flutter: Explizite Animationen mit AnimationController
Mit AnimationController, Tween und AnimatedBuilder professionelle Animationen in Flutter erstellen. Alle Details zu expliziten Animationen.
Flutter: Helles und dunkles Theme mit ThemeData
Ein Flutter-Theme mit ThemeData aufbauen: ColorScheme.fromSeed, helles und dunkles Theme, ThemeMode, Theme.of, TextTheme, Komponenten-Themes, ThemeExtension.