554 lines
11 KiB
Markdown
554 lines
11 KiB
Markdown
# Explicit Animations Reference
|
|
|
|
Explicit animations provide full control using AnimationController, Tween, and animation status listeners.
|
|
|
|
## Core Components
|
|
|
|
### AnimationController
|
|
|
|
The heart of explicit animations. Manages animation lifecycle, value, and timing.
|
|
|
|
```dart
|
|
late AnimationController _controller;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller = AnimationController(
|
|
duration: const Duration(seconds: 2),
|
|
vsync: this, // Required TickerProvider
|
|
lowerBound: 0.0, // Optional: default 0.0
|
|
upperBound: 1.0, // Optional: default 1.0
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose(); // ALWAYS dispose!
|
|
super.dispose();
|
|
}
|
|
```
|
|
|
|
**Lifecycle methods:**
|
|
|
|
```dart
|
|
// Start animation forward
|
|
_controller.forward();
|
|
|
|
// Start animation backward
|
|
_controller.reverse();
|
|
|
|
// Stop animation
|
|
_controller.stop();
|
|
|
|
// Repeat animation
|
|
_controller.repeat();
|
|
|
|
// Animate to specific value
|
|
await _controller.animateTo(0.5);
|
|
|
|
// Reset to beginning
|
|
_controller.reset();
|
|
|
|
// Animate with physics simulation
|
|
_controller.fling(velocity: 2.0);
|
|
|
|
// Animate with custom simulation
|
|
_controller.animateWith(SpringSimulation(...));
|
|
```
|
|
|
|
**Listening to changes:**
|
|
|
|
```dart
|
|
_controller.addListener(() {
|
|
// Value changed - rebuild needed
|
|
setState(() {});
|
|
});
|
|
|
|
_controller.addStatusListener((status) {
|
|
// Status changed
|
|
switch (status) {
|
|
case AnimationStatus.dismissed:
|
|
print('Animation dismissed (at 0)');
|
|
break;
|
|
case AnimationStatus.forward:
|
|
print('Animation moving forward');
|
|
break;
|
|
case AnimationStatus.reverse:
|
|
print('Animation moving backward');
|
|
break;
|
|
case AnimationStatus.completed:
|
|
print('Animation completed (at 1)');
|
|
break;
|
|
}
|
|
});
|
|
```
|
|
|
|
### Tween
|
|
|
|
Interpolates between begin and end values.
|
|
|
|
```dart
|
|
// Simple tween
|
|
animation = Tween<double>(begin: 0, end: 300).animate(_controller);
|
|
|
|
// Color tween
|
|
animation = ColorTween(
|
|
begin: Colors.red,
|
|
end: Colors.blue,
|
|
).animate(_controller);
|
|
|
|
// Border radius tween
|
|
animation = BorderRadiusTween(
|
|
begin: BorderRadius.circular(4),
|
|
end: BorderRadius.circular(75),
|
|
).animate(_controller);
|
|
|
|
// Rect tween
|
|
animation = RectTween(
|
|
begin: Rect.fromLTWH(0, 0, 100, 100),
|
|
end: Rect.fromLTWH(100, 100, 200, 200),
|
|
).animate(_controller);
|
|
```
|
|
|
|
**Common Tweens:**
|
|
- `Tween<T>` - Generic tween (use for most types)
|
|
- `ColorTween` - Color interpolation
|
|
- `RectTween` - Rectangle interpolation
|
|
- `IntTween` - Integer interpolation
|
|
- `SizeTween` - Size interpolation
|
|
- `OffsetTween` - Offset interpolation
|
|
- `BorderRadiusTween` - Border radius interpolation
|
|
|
|
### CurvedAnimation
|
|
|
|
Applies a curve to transform animation values.
|
|
|
|
```dart
|
|
animation = CurvedAnimation(
|
|
parent: _controller,
|
|
curve: Curves.easeInOut,
|
|
reverseCurve: Curves.easeIn, // Optional: different curve for reverse
|
|
);
|
|
```
|
|
|
|
**With Interval:**
|
|
|
|
```dart
|
|
animation = CurvedAnimation(
|
|
parent: _controller,
|
|
curve: const Interval(
|
|
0.25, // Start at 25% of controller
|
|
0.75, // End at 75% of controller
|
|
curve: Curves.easeInOut,
|
|
),
|
|
);
|
|
```
|
|
|
|
### ReverseAnimation
|
|
|
|
Reverses the parent animation.
|
|
|
|
```dart
|
|
final forwardAnimation = Tween<double>(begin: 0, end: 1).animate(_controller);
|
|
final reverseAnimation = ReverseAnimation(forwardAnimation);
|
|
```
|
|
|
|
## Patterns
|
|
|
|
### AnimatedWidget Pattern
|
|
|
|
Best for reusable animated widgets. Automatically handles rebuilds.
|
|
|
|
```dart
|
|
class AnimatedLogo extends AnimatedWidget {
|
|
const AnimatedLogo({super.key, required Animation<double> animation})
|
|
: super(listenable: animation);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final animation = listenable as Animation<double>;
|
|
return Center(
|
|
child: Container(
|
|
height: animation.value,
|
|
width: animation.value,
|
|
child: const FlutterLogo(),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// Usage
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return AnimatedLogo(animation: _animation);
|
|
}
|
|
```
|
|
|
|
### AnimatedBuilder Pattern
|
|
|
|
Best for complex widgets. Separates animation logic from widget logic.
|
|
|
|
```dart
|
|
class GrowTransition extends StatelessWidget {
|
|
const GrowTransition({
|
|
required this.child,
|
|
required this.animation,
|
|
super.key,
|
|
});
|
|
|
|
final Widget child;
|
|
final Animation<double> animation;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Center(
|
|
child: AnimatedBuilder(
|
|
animation: animation,
|
|
builder: (context, child) {
|
|
return SizedBox(
|
|
height: animation.value,
|
|
width: animation.value,
|
|
child: child,
|
|
);
|
|
},
|
|
child: child,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// Usage
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return GrowTransition(
|
|
animation: _animation,
|
|
child: const LogoWidget(),
|
|
);
|
|
}
|
|
```
|
|
|
|
**Why pass child twice?**
|
|
The `child` parameter to `AnimatedBuilder` is passed to the `builder` function, allowing the child to be built once and reused, rather than rebuilt every animation frame.
|
|
|
|
### Multiple Animations Pattern
|
|
|
|
Animate multiple properties from single controller.
|
|
|
|
```dart
|
|
class MultiPropertyAnimation extends AnimatedWidget {
|
|
const MultiPropertyAnimation({super.key, required Animation<double> animation})
|
|
: super(listenable: animation);
|
|
|
|
static final _opacityTween = Tween<double>(begin: 0.1, end: 1);
|
|
static final _sizeTween = Tween<double>(begin: 0, end: 300);
|
|
static final _colorTween = ColorTween(
|
|
begin: Colors.red,
|
|
end: Colors.blue,
|
|
);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final animation = listenable as Animation<double>;
|
|
return Center(
|
|
child: Opacity(
|
|
opacity: _opacityTween.evaluate(animation),
|
|
child: Container(
|
|
height: _sizeTween.evaluate(animation),
|
|
width: _sizeTween.evaluate(animation),
|
|
decoration: BoxDecoration(
|
|
color: _colorTween.evaluate(animation),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: const FlutterLogo(),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Built-in Transitions
|
|
|
|
Flutter provides pre-built transitions for common use cases.
|
|
|
|
### FadeTransition
|
|
|
|
```dart
|
|
FadeTransition(
|
|
opacity: _animation,
|
|
child: const FlutterLogo(),
|
|
)
|
|
```
|
|
|
|
### ScaleTransition
|
|
|
|
```dart
|
|
ScaleTransition(
|
|
scale: _animation,
|
|
child: const FlutterLogo(),
|
|
)
|
|
```
|
|
|
|
### SlideTransition
|
|
|
|
```dart
|
|
SlideTransition(
|
|
position: Tween<Offset>(
|
|
begin: Offset(0, 1),
|
|
end: Offset.zero,
|
|
).animate(_animation),
|
|
child: const FlutterLogo(),
|
|
)
|
|
```
|
|
|
|
### SizeTransition
|
|
|
|
```dart
|
|
SizeTransition(
|
|
sizeFactor: _animation,
|
|
axis: Axis.vertical,
|
|
child: const FlutterLogo(),
|
|
)
|
|
```
|
|
|
|
### RotationTransition
|
|
|
|
```dart
|
|
RotationTransition(
|
|
turns: _animation,
|
|
child: const FlutterLogo(),
|
|
)
|
|
```
|
|
|
|
### DecoratedBoxTransition
|
|
|
|
```dart
|
|
DecoratedBoxTransition(
|
|
decoration: DecorationTween(
|
|
begin: BoxDecoration(color: Colors.red),
|
|
end: BoxDecoration(color: Colors.blue),
|
|
).animate(_animation),
|
|
child: const FlutterLogo(),
|
|
)
|
|
```
|
|
|
|
### PositionedTransition
|
|
|
|
```dart
|
|
Stack(
|
|
children: [
|
|
PositionedTransition(
|
|
rect: RelativeRectTween(
|
|
begin: RelativeRect.fill(calculateRect(context, startPosition)),
|
|
end: RelativeRect.fill(calculateRect(context, endPosition)),
|
|
).animate(_animation),
|
|
child: const FlutterLogo(),
|
|
),
|
|
],
|
|
)
|
|
```
|
|
|
|
## Animation Status Handling
|
|
|
|
### Basic Status Loop
|
|
|
|
```dart
|
|
_animation.addStatusListener((status) {
|
|
if (status == AnimationStatus.completed) {
|
|
_controller.reverse();
|
|
} else if (status == AnimationStatus.dismissed) {
|
|
_controller.forward();
|
|
}
|
|
});
|
|
```
|
|
|
|
### One-way Animation
|
|
|
|
```dart
|
|
_animation.addStatusListener((status) {
|
|
if (status == AnimationStatus.completed) {
|
|
_controller.stop(); // Or navigate, show dialog, etc.
|
|
}
|
|
});
|
|
```
|
|
|
|
### Reset and Repeat
|
|
|
|
```dart
|
|
_animation.addStatusListener((status) {
|
|
if (status == AnimationStatus.completed) {
|
|
_controller.reset();
|
|
_controller.forward();
|
|
}
|
|
});
|
|
```
|
|
|
|
## Performance Best Practices
|
|
|
|
### DO
|
|
|
|
- Always dispose AnimationController
|
|
- Use `AnimatedWidget` or `AnimatedBuilder` instead of `setState()` in listeners
|
|
- Minimize widget rebuilds during animation
|
|
- Use static Tweens when possible (avoid recreating)
|
|
- Profile with Flutter DevTools
|
|
- Use `timeDilation` for debugging
|
|
|
|
### DON'T
|
|
|
|
- Forget to dispose controllers (memory leak)
|
|
- Call `setState()` in animation listeners unnecessarily
|
|
- Create complex widget trees inside animation builds
|
|
- Animate too many widgets simultaneously
|
|
- Use animation values directly in expensive operations
|
|
|
|
### Performance Pattern: RepaintBoundary
|
|
|
|
```dart
|
|
RepaintBoundary(
|
|
child: AnimatedBuilder(
|
|
animation: _controller,
|
|
builder: (context, child) {
|
|
return ExpensiveWidget();
|
|
},
|
|
),
|
|
)
|
|
```
|
|
|
|
## Debugging
|
|
|
|
### Slow Animations
|
|
|
|
```dart
|
|
void main() {
|
|
timeDilation = 10.0; // 10x slower
|
|
runApp(MyApp());
|
|
}
|
|
```
|
|
|
|
### Print Animation Values
|
|
|
|
```dart
|
|
_controller.addListener(() {
|
|
print('Animation value: ${_controller.value}');
|
|
});
|
|
|
|
_animation.addStatusListener((status) {
|
|
print('Animation status: $status');
|
|
});
|
|
```
|
|
|
|
### Flutter DevTools Performance Overlay
|
|
|
|
1. Run app in debug mode
|
|
2. Press 'p' to toggle performance overlay
|
|
3. Look for "GPU UI" and "GPU Raster" graphs
|
|
4. Aim for 60fps (16.67ms per frame)
|
|
|
|
## Common Patterns
|
|
|
|
### Fade In Then Slide Up
|
|
|
|
```dart
|
|
_animation = Tween<double>(begin: 0, end: 1).animate(
|
|
CurvedAnimation(
|
|
parent: _controller,
|
|
curve: const Interval(0.0, 0.5, curve: Curves.easeIn),
|
|
),
|
|
);
|
|
|
|
_slideAnimation = Tween<Offset>(
|
|
begin: Offset(0, 0.5),
|
|
end: Offset.zero,
|
|
).animate(
|
|
CurvedAnimation(
|
|
parent: _controller,
|
|
curve: const Interval(0.3, 1.0, curve: Curves.easeOut),
|
|
),
|
|
);
|
|
```
|
|
|
|
### Bounce Effect
|
|
|
|
```dart
|
|
_animation = CurvedAnimation(
|
|
parent: _controller,
|
|
curve: Curves.elasticOut,
|
|
);
|
|
```
|
|
|
|
### Shake Effect
|
|
|
|
```dart
|
|
class ShakeCurve extends Curve {
|
|
@override
|
|
double transform(double t) {
|
|
return sin(t * pi * 2);
|
|
}
|
|
}
|
|
|
|
_animation = CurvedAnimation(
|
|
parent: _controller,
|
|
curve: ShakeCurve(),
|
|
);
|
|
```
|
|
|
|
### Parallax Scrolling
|
|
|
|
```dart
|
|
class ParallaxWidget extends StatelessWidget {
|
|
const ParallaxWidget({
|
|
required this.animation,
|
|
required this.child,
|
|
super.key,
|
|
});
|
|
|
|
final Animation<double> animation;
|
|
final Widget child;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return AnimatedBuilder(
|
|
animation: animation,
|
|
builder: (context, child) {
|
|
return Transform.translate(
|
|
offset: Offset(0, animation.value * 50),
|
|
child: child,
|
|
);
|
|
},
|
|
child: child,
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Comparison: Implicit vs Explicit
|
|
|
|
| Feature | Implicit | Explicit |
|
|
|---------|----------|-----------|
|
|
| Controller | None needed | AnimationController required |
|
|
| Setup | Simple | More complex |
|
|
| Control | Limited (duration, curve) | Full (lifecycle, status, physics) |
|
|
| Multiple Properties | AnimatedContainer only | Unlimited |
|
|
| Performance | Good | Excellent (with patterns) |
|
|
| Reusability | Widget-based | Component-based |
|
|
| State Monitoring | Limited (onEnd) | Full (status listeners) |
|
|
|
|
## When to Use Explicit Animations
|
|
|
|
Use explicit animations when:
|
|
- Need full control over animation lifecycle
|
|
- Animating multiple properties
|
|
- Need to react to animation state changes
|
|
- Creating reusable animation components
|
|
- Need complex timing or sequencing
|
|
- Performance is critical
|
|
- Want physics-based or custom animations
|
|
|
|
Use implicit animations when:
|
|
- Simple, one-off animations
|
|
- Animation triggered by state change
|
|
- No need for fine-grained control
|
|
- Want simple, declarative code
|