skills/flutter-animations/references/physics.md

701 lines
14 KiB
Markdown

# Physics-Based Animations Reference
Physics-based animations create natural-feeling motion using simulations like springs, gravity, and momentum.
## Core Concept
Instead of linear interpolation, use physics simulations for realistic motion that responds naturally to forces like velocity and damping.
## Fling Animation
### Basic Fling
```dart
class FlingWidget extends StatefulWidget {
@override
State<FlingWidget> createState() => _FlingWidgetState();
}
class _FlingWidgetState extends State<FlingWidget>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 1),
vsync: this,
);
_controller.fling(velocity: 2.0);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.translate(
offset: Offset(_controller.value * 100, 0),
child: child,
);
},
child: const FlutterLogo(),
);
}
}
```
### Fling with Gesture
```dart
class DraggableFling extends StatefulWidget {
@override
State<DraggableFling> createState() => _DraggableFlingState();
}
class _DraggableFlingState extends State<DraggableFling>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
double _dragX = 0;
Offset? _startPosition;
@override
void initState() {
super.initState();
_controller = AnimationController.unbounded(vsync: this);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _handlePanStart(DragStartDetails details) {
_controller.stop();
_startPosition = details.globalPosition;
}
void _handlePanUpdate(DragUpdateDetails details) {
if (_startPosition != null) {
setState(() {
_dragX += details.delta.dx;
});
}
}
void _handlePanEnd(DragEndDetails details) {
_controller.fling(velocity: details.velocity.pixelsPerSecond.dx / 1000);
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onPanStart: _handlePanStart,
onPanUpdate: _handlePanUpdate,
onPanEnd: _handlePanEnd,
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.translate(
offset: Offset(_dragX + _controller.value, 0),
child: child,
);
},
child: const FlutterLogo(),
),
);
}
}
```
## Spring Simulation
### Basic Spring
```dart
class SpringAnimation extends StatefulWidget {
@override
State<SpringAnimation> createState() => _SpringAnimationState();
}
class _SpringAnimationState extends State<SpringAnimation>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController.unbounded(vsync: this);
_controller.animateWith(
SpringSimulation(
spring: const SpringDescription(
mass: 1.0,
stiffness: 200.0,
damping: 10.0,
),
start: 0.0,
end: 1.0,
velocity: 0.0,
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.scale(
scale: _controller.value,
child: child,
);
},
child: const FlutterLogo(),
);
}
}
```
### Spring Description Parameters
```dart
SpringDescription(
mass: 1.0, // Mass of the object (higher = more inertia)
stiffness: 200.0, // Spring stiffness (higher = faster, stiffer)
damping: 10.0, // Damping ratio (higher = less oscillation)
)
)
```
**Mass:**
- How "heavy" the object feels
- Typical values: 0.5 - 5.0
- Higher mass = more momentum, slower response
**Stiffness:**
- How stiff the spring is
- Typical values: 100 - 500
- Higher stiffness = faster oscillation
- Lower stiffness = slower, more "bouncy"
**Damping:**
- How quickly oscillation stops
- Typical values: 5 - 20
- Higher damping = less bounce
- Lower damping = more bounce
- Critical damping: ~15-18
### Spring Presets
**Bouncy:**
```dart
SpringDescription(
mass: 0.5,
stiffness: 300,
damping: 8,
)
```
**Snappy:**
```dart
SpringDescription(
mass: 1.0,
stiffness: 400,
damping: 18,
)
```
**Gentle:**
```dart
SpringDescription(
mass: 2.0,
stiffness: 150,
damping: 20,
)
```
## Gravity Simulation
### Falling Animation
```dart
class GravityAnimation extends StatefulWidget {
@override
State<GravityAnimation> createState() => _GravityAnimationState();
}
class _GravityAnimationState extends State<GravityAnimation>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController.unbounded(vsync: this);
_controller.animateWith(
GravitySimulation(
acceleration: 980, // pixels/s²
distance: 500, // pixels to fall
startDistance: 0,
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.translate(
offset: Offset(0, _controller.value),
child: child,
);
},
child: const FlutterLogo(),
);
}
}
```
## Scroll Physics
### Bouncing Scroll
```dart
CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: [
SliverList(
delegate: SliverChildListDelegate([
// Items
]),
),
],
)
```
### Clamping Scroll
```dart
CustomScrollView(
physics: const ClampingScrollPhysics(),
slivers: [
SliverList(
delegate: SliverChildListDelegate([
// Items
]),
),
],
)
```
### Fixed Scroll
```dart
CustomScrollView(
physics: const NeverScrollableScrollPhysics(),
slivers: [
SliverList(
delegate: SliverChildListDelegate([
// Items
]),
),
],
)
```
### Platform-Adaptive Scroll
```dart
CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverList(
delegate: SliverChildListDelegate([
// Items
]),
),
],
)
```
## Custom Simulation
### Creating Custom Simulation
```dart
class CustomSimulation extends Simulation {
final double target;
CustomSimulation({required this.target});
@override
double x(double time) {
// Calculate position at given time
return target * (1 - math.exp(-time / 0.5));
}
@override
double dx(double time) {
// Calculate velocity (derivative) at given time
return target * math.exp(-time / 0.5) / 0.5;
}
@override
bool isDone(double time) {
return dx(time).abs() < 0.01;
}
}
```
**Usage:**
```dart
_controller.animateWith(
CustomSimulation(target: 1.0),
);
```
### Decay Simulation
```dart
class DecaySimulation extends Simulation {
final double velocity;
DecaySimulation({required this.velocity});
@override
double x(double time) {
return velocity * (1 - math.exp(-time));
}
@override
double dx(double time) {
return velocity * math.exp(-time);
}
@override
bool isDone(double time) {
return dx(time).abs() < 0.1;
}
}
```
## Combining Physics and Animation
### Spring with Fallback
```dart
void animateWithSpring({
required AnimationController controller,
required double start,
required double end,
}) {
try {
controller.animateWith(
SpringSimulation(
spring: const SpringDescription(
mass: 1.0,
stiffness: 200.0,
damping: 15.0,
),
start: start,
end: end,
velocity: 0.0,
),
);
} catch (e) {
// Fallback to linear animation
controller.animateTo(end);
}
}
```
### Spring with Completion Callback
```dart
_controller.animateWith(
SpringSimulation(
spring: const SpringDescription(
mass: 1.0,
stiffness: 200.0,
damping: 15.0,
),
start: 0.0,
end: 1.0,
velocity: 0.0,
),
);
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
// Animation complete
}
});
```
## Physics for Gestures
### Swipe to Dismiss with Physics
```dart
class DismissibleWithPhysics extends StatefulWidget {
final Widget child;
final VoidCallback onDismiss;
const DismissibleWithPhysics({
super.key,
required this.child,
required this.onDismiss,
});
@override
State<DismissibleWithPhysics> createState() => _DismissibleWithPhysicsState();
}
class _DismissibleWithPhysicsState extends State<DismissibleWithPhysics>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
double _dragX = 0;
bool _isDismissing = false;
@override
void initState() {
super.initState();
_controller = AnimationController.unbounded(vsync: this);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _handlePanEnd(DragEndDetails details) {
final velocity = details.velocity.pixelsPerSecond.dx;
final screenWidth = MediaQuery.of(context).size.width;
if (velocity.abs() > 500 || _dragX.abs() > screenWidth / 3) {
// Dismiss
_controller.fling(velocity: velocity / screenWidth);
_isDismissing = true;
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed && _isDismissing) {
widget.onDismiss();
}
});
} else {
// Spring back
_controller.animateWith(
SpringSimulation(
spring: const SpringDescription(
mass: 1.0,
stiffness: 300.0,
damping: 15.0,
),
start: _dragX,
end: 0,
velocity: velocity,
),
);
}
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onPanUpdate: (details) {
if (!_controller.isAnimating) {
setState(() {
_dragX += details.delta.dx;
});
}
},
onPanEnd: _handlePanEnd,
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.translate(
offset: Offset(_dragX + _controller.value, 0),
child: child,
);
},
child: widget.child,
),
);
}
}
```
## Performance Considerations
### Simulation Complexity
Physics simulations can be computationally expensive. Optimize by:
- Using simpler simulations when possible (e.g., linear interpolation)
- Reducing simulation resolution
- Caching simulation results
- Using built-in physics widgets
### Spring Tuning
Finding the right spring parameters can be trial and error:
1. Start with moderate values (mass: 1.0, stiffness: 200, damping: 15)
2. Adjust for feel:
- Too slow? Increase stiffness or decrease mass
- Too bouncy? Increase damping
- Not bouncy enough? Decrease damping or stiffness
3. Test on real devices
## Debugging
### Visualize Physics
```dart
void main() {
timeDilation = 5.0; // Slow down physics
runApp(MyApp());
}
```
### Print Simulation Values
```dart
_controller.addListener(() {
print('Position: ${_controller.value}');
});
_controller.addStatusListener((status) {
print('Status: $status');
});
```
### Physics Inspector
Use Flutter DevTools to profile physics animations:
1. Open Performance overlay
2. Look for frame drops during physics simulations
3. Identify expensive calculations
## Best Practices
### DO
- Use physics simulations for natural-feeling motion
- Tune spring parameters for desired feel
- Test on various devices and screen sizes
- Profile performance with DevTools
- Provide fallback animations for physics failures
### DON'T
- Use complex physics when simple curves suffice
- Forget to handle simulation completion
- Use overly bouncy animations (distracting)
- Ignore accessibility (respect disable animations preference)
- Assume physics simulation completes instantly
## Common Physics Patterns
### Bouncy Button Press
```dart
_controller.animateWith(
SpringSimulation(
spring: const SpringDescription(
mass: 0.5,
stiffness: 500,
damping: 10,
),
start: 1.0,
end: 0.9,
velocity: 0,
),
);
```
### Swipeable Card
```dart
_controller.fling(
velocity: details.velocity.pixelsPerSecond.dx / 1000,
);
```
### Pull to Refresh
```dart
RefreshIndicator(
onRefresh: () async {
// Refresh data
},
child: ListView(...),
)
```
## Accessibility
- Respect `MediaQuery.disableAnimations` setting
- Provide non-physics alternatives
- Ensure physics animations don't cause motion sickness
- Test with reduced motion settings
## Platform-Specific Physics
### Adaptive Scroll Physics
```dart
class AdaptiveScrollPhysics extends ScrollPhysics {
const AdaptiveScrollPhysics({super.parent});
@override
AdaptiveScrollPhysics applyTo(ScrollPhysics? ancestor) {
return AdaptiveScrollPhysics(parent: buildParent(ancestor));
}
@override
SpringDescription get spring => Platform.isIOS
? const SpringDescription(
mass: 0.5,
stiffness: 100,
damping: 10,
)
: const SpringDescription(
mass: 0.8,
stiffness: 150,
damping: 15,
);
}
```
**Usage:**
```dart
ListView(
physics: const AdaptiveScrollPhysics(),
children: [...],
)
```