Verstehe die innere Mechanik impliziter Animationen und steuere deinen eigenen AnimationController präzise.
Diese Lektion in Kodokon öffnenAnimatedContainer 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.
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,
),
),
),
);
}
}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.
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,
),
);
}
}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.
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,
);
},
);
}
}