chore: bring flutter client, docs, deploy and tools into monorepo
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
---
|
||||
name: flutter-animations
|
||||
description: >-
|
||||
Add, fix, refactor, debug, test, or explain Flutter animations and motion
|
||||
effects. Use when working with implicit animations such as AnimatedContainer,
|
||||
AnimatedOpacity, AnimatedSwitcher, and TweenAnimationBuilder; explicit
|
||||
animations using AnimationController, Tween, CurvedAnimation, AnimatedWidget,
|
||||
AnimatedBuilder, and built-in transitions; Hero/shared-element route
|
||||
transitions; staggered or sequenced animations; physics-based motion,
|
||||
gestures, springs, flings, scroll physics, curves, performance, accessibility,
|
||||
reduced motion, and animation lifecycle bugs.
|
||||
metadata:
|
||||
author: Stanislav [MADTeacher] Chernyshev
|
||||
version: "2.0"
|
||||
---
|
||||
|
||||
# Flutter Animations
|
||||
|
||||
You are a Flutter motion implementation specialist. Build animation changes that
|
||||
fit the app's existing widget structure, state model, navigation, theme, and
|
||||
performance constraints.
|
||||
|
||||
## Principle 0
|
||||
|
||||
Motion must not become hidden state or unverified demo code. Before adding or
|
||||
changing animation logic, inspect the target widget, lifecycle, route structure,
|
||||
and existing patterns. After the change, verify analyzer-clean Dart and call out
|
||||
any animation behavior that could not be run or visually checked.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Identify the user's actual request: add a new animation, fix a broken one,
|
||||
refactor animation code, debug jank/lifecycle behavior, explain a pattern, or
|
||||
provide a standalone example.
|
||||
2. Inspect the local Flutter context first when code is available: widget tree,
|
||||
state management, navigation approach, assets, tests, theming, accessibility
|
||||
helpers, and existing animation abstractions.
|
||||
3. Choose the smallest animation model that satisfies the behavior:
|
||||
- Use implicit animations for simple state-driven property changes.
|
||||
- Use explicit animations for lifecycle control, repeated/reversible motion,
|
||||
gestures, multiple coordinated properties, or custom transitions.
|
||||
- Use Hero for shared elements across route transitions.
|
||||
- Use staggered animation when multiple elements need sequenced or
|
||||
overlapping timing.
|
||||
- Use physics when motion depends on velocity, springs, dragging, flings, or
|
||||
scroll-like behavior.
|
||||
4. Implement in the app's style. Preserve existing public APIs unless the user
|
||||
asked for a refactor. Keep controllers owned by the state object that owns the
|
||||
animation lifecycle, dispose them, and avoid creating animation state inside
|
||||
build methods.
|
||||
5. Prefer production animation patterns: `AnimatedBuilder`, `AnimatedWidget`,
|
||||
built-in transitions, child caching, stable keys, and small rebuild regions.
|
||||
Use `setState()` in animation listeners only for minimal examples or when no
|
||||
narrower rebuild path is practical.
|
||||
6. Respect accessibility and user settings. Check `MediaQuery.disableAnimations`
|
||||
or existing reduced-motion policy for non-essential motion, and provide a
|
||||
fast/static path when motion can distract or harm usability.
|
||||
7. Validate with the strongest available local checks. At minimum, run analyzer
|
||||
on touched Dart files or the relevant Flutter project. Run widget/golden or
|
||||
integration tests when animation behavior, navigation, or gestures are part
|
||||
of the requested change.
|
||||
|
||||
## Decision Guide
|
||||
|
||||
| Need | Default approach |
|
||||
|---|---|
|
||||
| One property or a small set of state-driven visual changes | `AnimatedContainer`, `AnimatedOpacity`, `AnimatedAlign`, `AnimatedPadding`, `AnimatedSwitcher`, or `TweenAnimationBuilder` |
|
||||
| Full lifecycle control, repeat/reverse/status, or gesture-driven values | `AnimationController` with `Tween`, `CurvedAnimation`, `AnimatedBuilder`, or `AnimatedWidget` |
|
||||
| Shared visual element between routes | `Hero` with stable unique tags and compatible source/destination widget trees |
|
||||
| List/menu/card reveal with offset timings | One controller with `Interval`s, or per-item animation only when lifecycle truly differs |
|
||||
| Spring/fling/drag or platform scroll feel | `fling`, `animateWith`, `SpringSimulation`, gesture velocity, or platform-specific `ScrollPhysics` |
|
||||
| Motion feels wrong but code works | Tune duration, curve, interval, easing, or reduced-motion behavior before adding complexity |
|
||||
|
||||
## Resource Routing
|
||||
|
||||
Read only the resources needed for the current task:
|
||||
|
||||
| Task | Read/use | Purpose |
|
||||
|---|---|---|
|
||||
| Simple state-driven animation or AnimatedSwitcher/TweenAnimationBuilder choice | `references/implicit.md`; optionally `assets/templates/implicit_animation.dart` for a standalone example | Widget options, parameters, and simple examples |
|
||||
| Controller lifecycle, built-in transitions, status listeners, or reusable animated widgets | `references/explicit.md`; optionally `assets/templates/explicit_animation.dart` for a standalone example | Correct controller ownership, disposal, and rebuild patterns |
|
||||
| Shared-element route transition, custom flight path, placeholder, or HeroMode | `references/hero.md`; optionally `assets/templates/hero_transition.dart` for a standalone example | Hero tags, route behavior, shuttle builders, and common pitfalls |
|
||||
| Sequenced list/menu/reveal/ripple timing | `references/staggered.md`; optionally `assets/templates/staggered_animation.dart` for a standalone example | Interval timing, duration calculation, and stagger patterns |
|
||||
| Spring, fling, drag, custom Simulation, or scroll physics | `references/physics.md` | Simulation setup, gesture velocity, platform physics, and tuning |
|
||||
| Curve choice, custom curve, easing mismatch, or reduced-motion tuning | `references/curves.md` | Curve selection, custom curves, and accessibility notes |
|
||||
|
||||
Templates are complete demo files, not drop-in production modules. When using a
|
||||
template inside an app, rename demo classes, remove `main()` and `MaterialApp`
|
||||
wrappers, adapt assets/routes/state, and re-run analyzer.
|
||||
|
||||
## Constraints
|
||||
|
||||
- Do not invent missing routes, asset paths, theme tokens, gesture behavior, or
|
||||
state-management APIs. Inspect them or ask the user if they are absent.
|
||||
- Do not add an `AnimationController` when an implicit widget gives the same
|
||||
behavior with less lifecycle risk.
|
||||
- Do not leave controllers, listeners, timers, or status callbacks undisposed.
|
||||
- Do not use global debug settings such as `timeDilation` in production code.
|
||||
Mention them only as a local debugging aid.
|
||||
- Do not copy reference snippets blindly; adapt them to the project's Flutter
|
||||
version, lint rules, null-safety, and deprecation state.
|
||||
- Do not make accessibility optional for user-facing motion. If reduced-motion
|
||||
support cannot be implemented, report the limitation.
|
||||
|
||||
## Validation
|
||||
|
||||
- Run `dart format` or the project's formatter on edited Dart files when edits
|
||||
are made.
|
||||
- Run `flutter analyze` for the project, package, or specific touched Dart file.
|
||||
- Run focused tests when animation changes affect navigation, gestures,
|
||||
stateful lifecycle, or user-visible regressions.
|
||||
- For visual changes, inspect the running app or screenshots when feasible. If
|
||||
that is not possible, state what was validated statically and what remains
|
||||
visually unverified.
|
||||
- When updating templates, verify each template as `lib/main.dart` in a minimal
|
||||
Flutter project with `flutter analyze lib/main.dart`.
|
||||
+345
@@ -0,0 +1,345 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Explicit animation example using AnimationController
|
||||
///
|
||||
/// This example demonstrates:
|
||||
/// - AnimationController setup and lifecycle
|
||||
/// - Tween interpolation
|
||||
/// - AnimatedWidget pattern for reusable animations
|
||||
/// - AnimatedBuilder pattern for complex widgets
|
||||
/// - Animation status monitoring
|
||||
|
||||
void main() => runApp(const ExplicitAnimationApp());
|
||||
|
||||
class ExplicitAnimationApp extends StatelessWidget {
|
||||
const ExplicitAnimationApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Explicit Animation',
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
||||
),
|
||||
home: const LogoAnimationDemo(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Basic explicit animation with addListener and setState
|
||||
class BasicExplicitAnimation extends StatefulWidget {
|
||||
const BasicExplicitAnimation({super.key});
|
||||
|
||||
@override
|
||||
State<BasicExplicitAnimation> createState() => _BasicExplicitAnimationState();
|
||||
}
|
||||
|
||||
class _BasicExplicitAnimationState extends State<BasicExplicitAnimation>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(seconds: 2),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_animation = Tween<double>(begin: 0, end: 300).animate(_controller)
|
||||
..addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
|
||||
_controller.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Basic Explicit')),
|
||||
body: Center(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 10),
|
||||
height: _animation.value,
|
||||
width: _animation.value,
|
||||
child: const FlutterLogo(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// AnimatedWidget pattern - best for reusable animated widgets
|
||||
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(
|
||||
margin: const EdgeInsets.symmetric(vertical: 10),
|
||||
height: animation.value,
|
||||
width: animation.value,
|
||||
child: const FlutterLogo(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AnimatedWidgetDemo extends StatefulWidget {
|
||||
const AnimatedWidgetDemo({super.key});
|
||||
|
||||
@override
|
||||
State<AnimatedWidgetDemo> createState() => _AnimatedWidgetDemoState();
|
||||
}
|
||||
|
||||
class _AnimatedWidgetDemoState extends State<AnimatedWidgetDemo>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(seconds: 2),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_animation = Tween<double>(begin: 0, end: 300).animate(_controller);
|
||||
_controller.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Animated Widget')),
|
||||
body: AnimatedLogo(animation: _animation),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// AnimatedBuilder pattern - best for complex widgets
|
||||
class LogoWidget extends StatelessWidget {
|
||||
const LogoWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: const FlutterLogo(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AnimatedBuilderDemo extends StatefulWidget {
|
||||
const AnimatedBuilderDemo({super.key});
|
||||
|
||||
@override
|
||||
State<AnimatedBuilderDemo> createState() => _AnimatedBuilderDemoState();
|
||||
}
|
||||
|
||||
class _AnimatedBuilderDemoState extends State<AnimatedBuilderDemo>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(seconds: 2),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_animation = Tween<double>(begin: 0, end: 300).animate(_controller);
|
||||
_controller.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Animated Builder')),
|
||||
body: GrowTransition(animation: _animation, child: const LogoWidget()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Multiple simultaneous animations with status monitoring
|
||||
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(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MultiPropertyDemo extends StatefulWidget {
|
||||
const MultiPropertyDemo({super.key});
|
||||
|
||||
@override
|
||||
State<MultiPropertyDemo> createState() => _MultiPropertyDemoState();
|
||||
}
|
||||
|
||||
class _MultiPropertyDemoState extends State<MultiPropertyDemo>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(seconds: 2),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_animation = CurvedAnimation(parent: _controller, curve: Curves.easeIn)
|
||||
..addStatusListener((status) {
|
||||
if (status == AnimationStatus.completed) {
|
||||
_controller.reverse();
|
||||
} else if (status == AnimationStatus.dismissed) {
|
||||
_controller.forward();
|
||||
}
|
||||
});
|
||||
|
||||
_controller.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Multi Property')),
|
||||
body: MultiPropertyAnimation(animation: _animation),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Main demo widget showing all examples
|
||||
class LogoAnimationDemo extends StatelessWidget {
|
||||
const LogoAnimationDemo({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Explicit Animations')),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) => const BasicExplicitAnimation(),
|
||||
),
|
||||
),
|
||||
child: const Text('Basic Explicit'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) => const AnimatedWidgetDemo(),
|
||||
),
|
||||
),
|
||||
child: const Text('Animated Widget'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) => const AnimatedBuilderDemo(),
|
||||
),
|
||||
),
|
||||
child: const Text('Animated Builder'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) => const MultiPropertyDemo(),
|
||||
),
|
||||
),
|
||||
child: const Text('Multi Property'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Hero animation example for shared element transitions
|
||||
///
|
||||
/// This example demonstrates:
|
||||
/// - Basic hero animation with matching tags
|
||||
/// - Custom PhotoHero widget
|
||||
/// - Navigation between screens with hero transition
|
||||
/// - MaterialRectCenterArcTween for aspect ratio preservation
|
||||
|
||||
void main() => runApp(const HeroAnimationApp());
|
||||
|
||||
class HeroAnimationApp extends StatelessWidget {
|
||||
const HeroAnimationApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Hero Animation',
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
||||
),
|
||||
home: const GalleryScreen(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom hero widget for images
|
||||
class PhotoHero extends StatelessWidget {
|
||||
const PhotoHero({
|
||||
super.key,
|
||||
required this.photo,
|
||||
this.onTap,
|
||||
required this.width,
|
||||
});
|
||||
|
||||
final String photo;
|
||||
final VoidCallback? onTap;
|
||||
final double width;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: width,
|
||||
child: Hero(
|
||||
tag: photo,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Image.asset(
|
||||
photo,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Container(
|
||||
width: width,
|
||||
height: width,
|
||||
color: Colors.grey,
|
||||
child: const Icon(Icons.broken_image),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Gallery screen showing multiple hero images
|
||||
class GalleryScreen extends StatelessWidget {
|
||||
const GalleryScreen({super.key});
|
||||
|
||||
static const List<String> _photos = [
|
||||
'images/photo1.png',
|
||||
'images/photo2.png',
|
||||
'images/photo3.png',
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Hero Gallery')),
|
||||
body: GridView.builder(
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
),
|
||||
itemCount: _photos.length,
|
||||
itemBuilder: (context, index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: PhotoHero(
|
||||
photo: _photos[index],
|
||||
width: 100,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) => DetailScreen(photo: _photos[index]),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Detail screen showing hero image in fullscreen
|
||||
class DetailScreen extends StatelessWidget {
|
||||
const DetailScreen({super.key, required this.photo});
|
||||
|
||||
final String photo;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Detail')),
|
||||
body: Center(
|
||||
child: PhotoHero(
|
||||
photo: photo,
|
||||
width: MediaQuery.of(context).size.width - 32,
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Example with circular hero (using placeholder instead of asset)
|
||||
class CircularHeroExample extends StatefulWidget {
|
||||
const CircularHeroExample({super.key});
|
||||
|
||||
@override
|
||||
State<CircularHeroExample> createState() => _CircularHeroExampleState();
|
||||
}
|
||||
|
||||
class _CircularHeroExampleState extends State<CircularHeroExample> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Circular Hero')),
|
||||
body: ListView(
|
||||
children: List.generate(3, (index) {
|
||||
final tag = 'circle-$index';
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Hero(
|
||||
tag: tag,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) =>
|
||||
CircularDetailScreen(tag: tag, index: index),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.blue,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CircularDetailScreen extends StatelessWidget {
|
||||
const CircularDetailScreen({
|
||||
super.key,
|
||||
required this.tag,
|
||||
required this.index,
|
||||
});
|
||||
|
||||
final String tag;
|
||||
final int index;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Circular Detail')),
|
||||
body: Center(
|
||||
child: Hero(
|
||||
tag: tag,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
child: Container(
|
||||
width: 300,
|
||||
height: 300,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.withValues(alpha: 0.8),
|
||||
borderRadius: BorderRadius.circular(150),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Image $index',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 24),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Implicit animation example showing basic fade animation
|
||||
///
|
||||
/// This example demonstrates:
|
||||
/// - AnimatedOpacity for fade in/out
|
||||
/// - State-driven animation triggers
|
||||
/// - Simple, declarative animation code
|
||||
|
||||
void main() => runApp(const ImplicitAnimationApp());
|
||||
|
||||
class ImplicitAnimationApp extends StatelessWidget {
|
||||
const ImplicitAnimationApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Implicit Animation',
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
||||
),
|
||||
home: const FadeExample(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FadeExample extends StatefulWidget {
|
||||
const FadeExample({super.key});
|
||||
|
||||
@override
|
||||
State<FadeExample> createState() => _FadeExampleState();
|
||||
}
|
||||
|
||||
class _FadeExampleState extends State<FadeExample> {
|
||||
bool _visible = true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Implicit Fade')),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
AnimatedOpacity(
|
||||
opacity: _visible ? 1.0 : 0.0,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.easeInOut,
|
||||
child: const FlutterLogo(size: 100),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton(
|
||||
onPressed: () => setState(() => _visible = !_visible),
|
||||
child: Text(_visible ? 'Fade Out' : 'Fade In'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// AnimatedContainer example - animating multiple properties
|
||||
class AnimatedContainerExample extends StatefulWidget {
|
||||
const AnimatedContainerExample({super.key});
|
||||
|
||||
@override
|
||||
State<AnimatedContainerExample> createState() =>
|
||||
_AnimatedContainerExampleState();
|
||||
}
|
||||
|
||||
class _AnimatedContainerExampleState extends State<AnimatedContainerExample> {
|
||||
bool _expanded = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Animated Container')),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
width: _expanded ? 200 : 100,
|
||||
height: _expanded ? 200 : 100,
|
||||
decoration: BoxDecoration(
|
||||
color: _expanded ? Colors.blue : Colors.red,
|
||||
borderRadius: BorderRadius.circular(_expanded ? 20 : 8),
|
||||
),
|
||||
child: const FlutterLogo(),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton(
|
||||
onPressed: () => setState(() => _expanded = !_expanded),
|
||||
child: Text(_expanded ? 'Shrink' : 'Expand'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// TweenAnimationBuilder example - custom tween without boilerplate
|
||||
class TweenAnimationBuilderExample extends StatefulWidget {
|
||||
const TweenAnimationBuilderExample({super.key});
|
||||
|
||||
@override
|
||||
State<TweenAnimationBuilderExample> createState() =>
|
||||
_TweenAnimationBuilderExampleState();
|
||||
}
|
||||
|
||||
class _TweenAnimationBuilderExampleState
|
||||
extends State<TweenAnimationBuilderExample> {
|
||||
bool _animated = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Tween Animation Builder')),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween<double>(begin: 0, end: 1),
|
||||
duration: const Duration(seconds: 1),
|
||||
curve: Curves.easeInOut,
|
||||
builder: (context, value, child) {
|
||||
return Opacity(
|
||||
opacity: value,
|
||||
child: Transform.scale(scale: value, child: child),
|
||||
);
|
||||
},
|
||||
child: const FlutterLogo(size: 100),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton(
|
||||
onPressed: () => setState(() => _animated = !_animated),
|
||||
child: const Text('Animate'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Staggered animation example with multiple property animations
|
||||
///
|
||||
/// This example demonstrates:
|
||||
/// - Single AnimationController driving multiple animations
|
||||
/// - Interval-based timing for sequential/overlapping animations
|
||||
/// - Multiple tweens for different properties
|
||||
/// - Status monitoring for animation loops
|
||||
|
||||
void main() => runApp(const StaggeredAnimationApp());
|
||||
|
||||
class StaggeredAnimationApp extends StatelessWidget {
|
||||
const StaggeredAnimationApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Staggered Animation',
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
||||
),
|
||||
home: const StaggeredDemo(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Staggered animation with fade, scale, and color changes
|
||||
class StaggeredAnimation extends StatelessWidget {
|
||||
StaggeredAnimation({super.key, required this.animation});
|
||||
|
||||
late final Animation<double> opacity = Tween<double>(begin: 0.0, end: 1.0)
|
||||
.animate(
|
||||
CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: const Interval(0.0, 0.1, curve: Curves.ease),
|
||||
),
|
||||
);
|
||||
|
||||
late final Animation<double> width = Tween<double>(begin: 50.0, end: 150.0)
|
||||
.animate(
|
||||
CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: const Interval(0.125, 0.25, curve: Curves.ease),
|
||||
),
|
||||
);
|
||||
|
||||
late final Animation<double> height = Tween<double>(begin: 50.0, end: 150.0)
|
||||
.animate(
|
||||
CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: const Interval(0.25, 0.375, curve: Curves.ease),
|
||||
),
|
||||
);
|
||||
|
||||
late final Animation<BorderRadius?> borderRadius =
|
||||
BorderRadiusTween(
|
||||
begin: BorderRadius.circular(4),
|
||||
end: BorderRadius.circular(75),
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: const Interval(0.375, 0.5, curve: Curves.ease),
|
||||
),
|
||||
);
|
||||
|
||||
late final Animation<Color?> color =
|
||||
ColorTween(begin: Colors.red, end: Colors.orange).animate(
|
||||
CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: const Interval(0.5, 0.625, curve: Curves.ease),
|
||||
),
|
||||
);
|
||||
|
||||
final Animation<double> animation;
|
||||
|
||||
Widget _buildAnimation(BuildContext context, Widget? child) {
|
||||
return Center(
|
||||
child: Opacity(
|
||||
opacity: opacity.value,
|
||||
child: Container(
|
||||
width: width.value,
|
||||
height: height.value,
|
||||
decoration: BoxDecoration(
|
||||
color: color.value,
|
||||
border: Border.all(color: Colors.indigo[300]!, width: 3),
|
||||
borderRadius: borderRadius.value,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(animation: animation, builder: _buildAnimation);
|
||||
}
|
||||
}
|
||||
|
||||
class StaggeredDemo extends StatefulWidget {
|
||||
const StaggeredDemo({super.key});
|
||||
|
||||
@override
|
||||
State<StaggeredDemo> createState() => _StaggeredDemoState();
|
||||
}
|
||||
|
||||
class _StaggeredDemoState extends State<StaggeredDemo>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 2000),
|
||||
vsync: this,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _playAnimation() async {
|
||||
try {
|
||||
await _controller.forward().orCancel;
|
||||
await _controller.reverse().orCancel;
|
||||
} on TickerCanceled {
|
||||
// Animation was canceled
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Staggered Animation')),
|
||||
body: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: _playAnimation,
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 300,
|
||||
height: 300,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
border: Border.all(color: Colors.black.withValues(alpha: 0.5)),
|
||||
),
|
||||
child: StaggeredAnimation(animation: _controller.view),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Staggered menu animation example
|
||||
class StaggeredMenuExample extends StatefulWidget {
|
||||
const StaggeredMenuExample({super.key});
|
||||
|
||||
@override
|
||||
State<StaggeredMenuExample> createState() => _StaggeredMenuExampleState();
|
||||
}
|
||||
|
||||
class _StaggeredMenuExampleState extends State<StaggeredMenuExample>
|
||||
with SingleTickerProviderStateMixin {
|
||||
static const _menuTitles = [
|
||||
'Declarative Style',
|
||||
'Premade Widgets',
|
||||
'Stateful Hot Reload',
|
||||
'Native Performance',
|
||||
'Great Community',
|
||||
];
|
||||
|
||||
static const _initialDelayTime = Duration(milliseconds: 50);
|
||||
static const _itemSlideTime = Duration(milliseconds: 250);
|
||||
static const _staggerTime = Duration(milliseconds: 50);
|
||||
|
||||
late AnimationController _controller;
|
||||
|
||||
final _animationDuration =
|
||||
_initialDelayTime + (_staggerTime * _menuTitles.length) + _itemSlideTime;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: _animationDuration,
|
||||
vsync: this,
|
||||
);
|
||||
_controller.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Staggered Menu')),
|
||||
body: ListView.builder(
|
||||
itemCount: _menuTitles.length,
|
||||
itemBuilder: (context, index) {
|
||||
final start =
|
||||
_initialDelayTime.inMilliseconds.toDouble() +
|
||||
(_staggerTime.inMilliseconds.toDouble() * index);
|
||||
final end = start + _itemSlideTime.inMilliseconds.toDouble();
|
||||
final intervalStart =
|
||||
start / _animationDuration.inMilliseconds.toDouble();
|
||||
final intervalEnd =
|
||||
end / _animationDuration.inMilliseconds.toDouble();
|
||||
|
||||
final animation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Interval(intervalStart, intervalEnd, curve: Curves.easeOut),
|
||||
);
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
return SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(1, 0),
|
||||
end: Offset.zero,
|
||||
).animate(animation),
|
||||
child: FadeTransition(opacity: animation, child: child),
|
||||
);
|
||||
},
|
||||
child: ListTile(title: Text(_menuTitles[index])),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,566 @@
|
||||
# Curves Reference
|
||||
|
||||
Curves define the easing function for animations, controlling how values interpolate from start to end.
|
||||
|
||||
## Core Concept
|
||||
|
||||
A curve takes an input `t` (0.0 to 1.0, representing time) and outputs a value (typically 0.0 to 1.0, representing progress).
|
||||
|
||||
## Built-in Curves
|
||||
|
||||
### Linear
|
||||
|
||||
```dart
|
||||
Curves.linear
|
||||
```
|
||||
|
||||
No easing - straight line from 0 to 1. Use when you want constant speed.
|
||||
|
||||
### Ease (Sigmoid)
|
||||
|
||||
```dart
|
||||
Curves.ease
|
||||
Curves.easeIn
|
||||
Curves.easeOut
|
||||
Curves.easeInOut
|
||||
```
|
||||
|
||||
**Curves.ease** - Slow start and end, fast middle (most common)
|
||||
|
||||
**Curves.easeIn** - Slow start, fast end (accelerating)
|
||||
|
||||
**Curves.easeOut** - Fast start, slow end (decelerating)
|
||||
|
||||
**Curves.easeInOut** - Slow start and end, fast middle (combination)
|
||||
|
||||
### Cubic (Stronger Ease)
|
||||
|
||||
```dart
|
||||
Curves.easeInCubic
|
||||
Curves.easeOutCubic
|
||||
Curves.easeInOutCubic
|
||||
```
|
||||
|
||||
Stronger easing than basic ease. More dramatic slow/fast zones.
|
||||
|
||||
### Elastic (Bouncy)
|
||||
|
||||
```dart
|
||||
Curves.elasticIn
|
||||
Curves.elasticOut
|
||||
Curves.elasticInOut
|
||||
```
|
||||
|
||||
**Curves.elasticIn** - Bounces in (starts behind, shoots forward)
|
||||
|
||||
**Curves.elasticOut** - Bounces out (goes past, comes back)
|
||||
|
||||
**Curves.elasticInOut** - Bounces in and out
|
||||
|
||||
Example: Good for playful UI, notifications
|
||||
|
||||
### Bounce
|
||||
|
||||
```dart
|
||||
Curves.bounceIn
|
||||
Curves.bounceOut
|
||||
Curves.bounceInOut
|
||||
```
|
||||
|
||||
**Curves.bounceIn** - Bounces in (less bouncy than elastic)
|
||||
|
||||
**Curves.bounceOut** - Bounces out (less bouncy than elastic)
|
||||
|
||||
**Curves.bounceInOut** - Bounces in and out
|
||||
|
||||
Example: Good for feedback animations
|
||||
|
||||
### Back (Overshoot)
|
||||
|
||||
```dart
|
||||
Curves.backIn
|
||||
Curves.backOut
|
||||
Curves.backInOut
|
||||
```
|
||||
|
||||
**Curves.backIn** - Backs up before going in
|
||||
|
||||
**Curves.backOut** - Backs up before coming out
|
||||
|
||||
**Curves.backInOut** - Backs up on both ends
|
||||
|
||||
Example: Good for revealing content
|
||||
|
||||
### Decelerate (Fast then Slow)
|
||||
|
||||
```dart
|
||||
Curves.decelerate
|
||||
```
|
||||
|
||||
Fast start, very slow end. Similar to easeOut but more dramatic.
|
||||
|
||||
### Fast Out Slow In
|
||||
|
||||
```dart
|
||||
Curves.fastOutSlowIn
|
||||
```
|
||||
|
||||
Very fast start, very slow middle, very fast end. Dramatic.
|
||||
|
||||
### Custom Curves in Flutter API
|
||||
|
||||
Flutter includes specialized curves:
|
||||
|
||||
```dart
|
||||
// Material design curves
|
||||
Curves.fastLinearToSlowEaseIn
|
||||
Curves.slowMiddle
|
||||
|
||||
// Specific curves for certain transitions
|
||||
Curves.ease
|
||||
```
|
||||
|
||||
## Using Curves
|
||||
|
||||
### With Implicit Animation
|
||||
|
||||
```dart
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
width: _expanded ? 200 : 100,
|
||||
child: const FlutterLogo(),
|
||||
)
|
||||
```
|
||||
|
||||
### With Explicit Animation (CurvedAnimation)
|
||||
|
||||
```dart
|
||||
animation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
```
|
||||
|
||||
### With Interval
|
||||
|
||||
```dart
|
||||
animation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: const Interval(
|
||||
0.0,
|
||||
0.5,
|
||||
curve: Curves.easeIn,
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
### With Curves.elasticOut
|
||||
|
||||
```dart
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 1000),
|
||||
curve: Curves.elasticOut,
|
||||
transform: Matrix4.rotationZ(_rotated ? 0.5 : 0),
|
||||
child: const FlutterLogo(),
|
||||
)
|
||||
```
|
||||
|
||||
## Custom Curves
|
||||
|
||||
### Creating a Custom Curve
|
||||
|
||||
```dart
|
||||
import 'dart:math' as math;
|
||||
|
||||
class CustomCurve extends Curve {
|
||||
@override
|
||||
double transform(double t) {
|
||||
// t is 0.0 to 1.0
|
||||
// Return 0.0 to 1.0
|
||||
return math.pow(t, 2); // Quadratic ease in
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```dart
|
||||
AnimatedContainer(
|
||||
curve: CustomCurve(),
|
||||
duration: const Duration(milliseconds: 300),
|
||||
width: _expanded ? 200 : 100,
|
||||
child: const FlutterLogo(),
|
||||
)
|
||||
```
|
||||
|
||||
### Shake Curve
|
||||
|
||||
```dart
|
||||
class ShakeCurve extends Curve {
|
||||
@override
|
||||
double transform(double t) {
|
||||
return sin(t * pi * 2);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Smooth Step Curve
|
||||
|
||||
```dart
|
||||
class SmoothStep extends Curve {
|
||||
final double steps;
|
||||
|
||||
const SmoothStep({this.steps = 5});
|
||||
|
||||
@override
|
||||
double transform(double t) {
|
||||
return (t * steps).floor() / steps;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Exponential Curve
|
||||
|
||||
```dart
|
||||
class ExponentialCurve extends Curve {
|
||||
final double exponent;
|
||||
|
||||
const ExponentialCurve({this.exponent = 2});
|
||||
|
||||
@override
|
||||
double transform(double t) {
|
||||
return math.pow(t, exponent);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Curve Composition
|
||||
|
||||
### Cubic Bezier
|
||||
|
||||
Flutter doesn't have built-in cubic bezier, but you can approximate:
|
||||
|
||||
```dart
|
||||
class CubicBezier extends Curve {
|
||||
final double p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y;
|
||||
|
||||
const CubicBezier({
|
||||
required this.p0x,
|
||||
required this.p0y,
|
||||
required this.p1x,
|
||||
required this.p1y,
|
||||
required this.p2x,
|
||||
required this.p2y,
|
||||
required this.p3x,
|
||||
required this.p3y,
|
||||
});
|
||||
|
||||
@override
|
||||
double transform(double t) {
|
||||
// Simplified cubic bezier formula
|
||||
final u = 1 - t;
|
||||
return (u * u * u * p0y +
|
||||
3 * u * u * t * p1y +
|
||||
3 * u * t * t * p2y +
|
||||
t * t * t * p3y);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Combining Curves
|
||||
|
||||
```dart
|
||||
class CombinedCurve extends Curve {
|
||||
final Curve first;
|
||||
final Curve second;
|
||||
final double threshold;
|
||||
|
||||
const CombinedCurve({
|
||||
required this.first,
|
||||
required this.second,
|
||||
required this.threshold,
|
||||
});
|
||||
|
||||
@override
|
||||
double transform(double t) {
|
||||
if (t < threshold) {
|
||||
return first.transform(t / threshold) * threshold;
|
||||
}
|
||||
return threshold +
|
||||
second.transform((t - threshold) / (1 - threshold)) * (1 - threshold);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```dart
|
||||
AnimatedContainer(
|
||||
curve: CombinedCurve(
|
||||
first: Curves.easeIn,
|
||||
second: Curves.easeOut,
|
||||
threshold: 0.5,
|
||||
),
|
||||
duration: const Duration(milliseconds: 500),
|
||||
width: _expanded ? 200 : 100,
|
||||
child: const FlutterLogo(),
|
||||
)
|
||||
```
|
||||
|
||||
## Choosing the Right Curve
|
||||
|
||||
### Motion Design Guidelines
|
||||
|
||||
| Animation Type | Recommended Curve | Why |
|
||||
|---------------|-------------------|-----|
|
||||
| Fade in/out | easeInOut | Natural opacity change |
|
||||
| Size change | easeOut | Decelerate feels natural for growth |
|
||||
| Position slide | easeInOut | Smooth start and stop |
|
||||
| Rotation | easeInOut | Natural angular acceleration |
|
||||
| Color change | linear | Uniform color transition |
|
||||
| Scale | elasticOut | Playful, bouncy feel |
|
||||
| Reveal | backOut | Dramatic reveal effect |
|
||||
| Loading | linear or ease | Consistent speed |
|
||||
| Success | elasticOut | Celebratory feel |
|
||||
|
||||
### Platform Conventions
|
||||
|
||||
**iOS:**
|
||||
- Prefers subtle curves (ease, easeInOut)
|
||||
- Limited use of elastic/bounce
|
||||
- Consistent with system animations
|
||||
|
||||
**Android:**
|
||||
- Wider variety of curves
|
||||
- More use of elastic/bounce for feedback
|
||||
- Material design curves (easeIn, fastOutSlowIn)
|
||||
|
||||
**Web:**
|
||||
- CSS-like curves (ease, ease-in, ease-out, ease-in-out)
|
||||
- Limited use of complex curves
|
||||
|
||||
## Curve Combinations
|
||||
|
||||
### Multi-Stage Animation
|
||||
|
||||
```dart
|
||||
// Stage 1: Ease in (0.0 - 0.5)
|
||||
// Stage 2: Ease out (0.5 - 1.0)
|
||||
|
||||
animation1 = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: const Interval(0.0, 0.5, curve: Curves.easeIn),
|
||||
);
|
||||
|
||||
animation2 = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: const Interval(0.5, 1.0, curve: Curves.easeOut),
|
||||
);
|
||||
```
|
||||
|
||||
### Staggered with Different Curves
|
||||
|
||||
```dart
|
||||
// Item 1: Bouncy
|
||||
item1Animation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: const Interval(0.0, 0.3, curve: Curves.elasticOut),
|
||||
);
|
||||
|
||||
// Item 2: Smooth
|
||||
item2Animation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: const Interval(0.1, 0.4, curve: Curves.easeInOut),
|
||||
);
|
||||
|
||||
// Item 3: Linear
|
||||
item3Animation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: const Interval(0.2, 0.5, curve: Curves.linear),
|
||||
);
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Curve Complexity
|
||||
|
||||
Simple curves (linear, ease) are faster:
|
||||
- Fewer calculations
|
||||
- Less GPU work
|
||||
- Consistent timing
|
||||
|
||||
Complex curves (elastic, bounce) are slower:
|
||||
- More calculations
|
||||
- Potential for frame drops
|
||||
- Variable timing
|
||||
|
||||
### Optimization Tips
|
||||
|
||||
- Use simpler curves on low-end devices
|
||||
- Cache curve calculations if needed
|
||||
- Test performance on target devices
|
||||
- Profile with Flutter DevTools
|
||||
|
||||
## Debugging Curves
|
||||
|
||||
### Visualize Curve
|
||||
|
||||
```dart
|
||||
class CurveVisualizer extends StatelessWidget {
|
||||
final Curve curve;
|
||||
|
||||
const CurveVisualizer({super.key, required this.curve});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomPaint(
|
||||
painter: _CurvePainter(curve: curve),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CurvePainter extends CustomPainter {
|
||||
final Curve curve;
|
||||
|
||||
const CurvePainter({required this.curve});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint()
|
||||
..color = Colors.blue
|
||||
..strokeWidth = 2
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
final path = Path();
|
||||
for (double t = 0; t <= 1; t += 0.01) {
|
||||
final x = t * size.width;
|
||||
final y = size.height - (curve.transform(t) * size.height);
|
||||
if (t == 0) {
|
||||
path.moveTo(x, y);
|
||||
} else {
|
||||
path.lineTo(x, y);
|
||||
}
|
||||
}
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(_CurvePainter oldDelegate) => false;
|
||||
}
|
||||
```
|
||||
|
||||
### Print Curve Values
|
||||
|
||||
```dart
|
||||
void printCurveValues(Curve curve) {
|
||||
print('Curve: $curve');
|
||||
for (double t = 0; t <= 1; t += 0.1) {
|
||||
print('t=$t, output=${curve.transform(t)}');
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
printCurveValues(Curves.easeInOut);
|
||||
```
|
||||
|
||||
## Accessibility
|
||||
|
||||
### Respecting User Preferences
|
||||
|
||||
```dart
|
||||
Curve getAdaptiveCurve(BuildContext context) {
|
||||
if (MediaQuery.of(context).disableAnimations) {
|
||||
return Curves.linear; // No easing when animations disabled
|
||||
}
|
||||
return Curves.easeInOut;
|
||||
}
|
||||
```
|
||||
|
||||
### Reduced Motion
|
||||
|
||||
```dart
|
||||
AnimatedContainer(
|
||||
duration: MediaQuery.of(context).disableAnimations
|
||||
? Duration.zero
|
||||
: const Duration(milliseconds: 300),
|
||||
curve: getAdaptiveCurve(context),
|
||||
width: _expanded ? 200 : 100,
|
||||
child: const FlutterLogo(),
|
||||
)
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Loading Spinner
|
||||
|
||||
```dart
|
||||
_animation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.linear, // Constant speed
|
||||
);
|
||||
```
|
||||
|
||||
### Success Checkmark
|
||||
|
||||
```dart
|
||||
_animation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.elasticOut, // Bouncy celebration
|
||||
);
|
||||
```
|
||||
|
||||
### Page Transition
|
||||
|
||||
```dart
|
||||
_enterAnimation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeIn, // Accelerate in
|
||||
);
|
||||
|
||||
_exitAnimation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeOut, // Decelerate out
|
||||
);
|
||||
```
|
||||
|
||||
### Modal Popup
|
||||
|
||||
```dart
|
||||
_animation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeOutBack, // Dramatic reveal
|
||||
);
|
||||
```
|
||||
|
||||
## Curve Comparison
|
||||
|
||||
| Curve | Start | Middle | End | Feel |
|
||||
|-------|-------|--------|-----|------|
|
||||
| linear | Fast | Fast | Fast | Mechanical |
|
||||
| ease | Slow | Fast | Slow | Natural |
|
||||
| easeIn | Slow | Fast | Fast | Accelerating |
|
||||
| easeOut | Fast | Slow | Slow | Decelerating |
|
||||
| easeInOut | Slow | Fast | Slow | Smooth |
|
||||
| elasticIn | Back | Fast | Normal | Bouncy start |
|
||||
| elasticOut | Normal | Fast | Back | Bouncy end |
|
||||
| bounceIn | Bounce | Fast | Normal | Bouncy start |
|
||||
| bounceOut | Normal | Fast | Bounce | Bouncy end |
|
||||
| backIn | Back | Fast | Normal | Overshoot start |
|
||||
| backOut | Normal | Fast | Back | Overshoot end |
|
||||
|
||||
## Best Practices
|
||||
|
||||
### DO
|
||||
|
||||
- Use appropriate curves for animation type
|
||||
- Test on real devices
|
||||
- Consider platform conventions
|
||||
- Respect accessibility settings
|
||||
- Profile performance with DevTools
|
||||
|
||||
### DON'T
|
||||
|
||||
- Use elastic/bounce for everything (distracting)
|
||||
- Over-combine curves (confusing motion)
|
||||
- Ignore device performance
|
||||
- Use same curve for all animations (boring)
|
||||
- Forget to handle edge cases (t < 0 or t > 1)
|
||||
@@ -0,0 +1,553 @@
|
||||
# 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
|
||||
@@ -0,0 +1,724 @@
|
||||
# Hero Animations Reference
|
||||
|
||||
Hero animations create shared element transitions between screens, making elements appear to "fly" from one route to another.
|
||||
|
||||
## Core Concept
|
||||
|
||||
Use two `Hero` widgets with matching `tag` in different routes. Flutter automatically animates the transition between them.
|
||||
|
||||
## Basic Hero Animation
|
||||
|
||||
### Simple Image Transition
|
||||
|
||||
**Source route (list screen):**
|
||||
```dart
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) => const DetailScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Hero(
|
||||
tag: 'hero-image',
|
||||
child: Image.asset('images/thumbnail.png'),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
**Destination route (detail screen):**
|
||||
```dart
|
||||
Scaffold(
|
||||
appBar: AppBar(title: const Text('Detail')),
|
||||
body: GestureDetector(
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
child: Hero(
|
||||
tag: 'hero-image', // Same tag!
|
||||
child: Image.asset('images/thumbnail.png'),
|
||||
),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
### Custom PhotoHero Widget
|
||||
|
||||
Reusable hero widget for images:
|
||||
|
||||
```dart
|
||||
class PhotoHero extends StatelessWidget {
|
||||
const PhotoHero({
|
||||
super.key,
|
||||
required this.photo,
|
||||
this.onTap,
|
||||
required this.width,
|
||||
});
|
||||
|
||||
final String photo;
|
||||
final VoidCallback? onTap;
|
||||
final double width;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: width,
|
||||
child: Hero(
|
||||
tag: photo,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Image.asset(
|
||||
photo,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Usage in source route:**
|
||||
```dart
|
||||
class SourceScreen extends StatelessWidget {
|
||||
const SourceScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Gallery')),
|
||||
body: GridView.builder(
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
),
|
||||
itemCount: 10,
|
||||
itemBuilder: (context, index) {
|
||||
return PhotoHero(
|
||||
photo: 'images/photo_$index.png',
|
||||
width: 100,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) => DetailScreen(photo: 'images/photo_$index.png'),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Usage in destination route:**
|
||||
```dart
|
||||
class DetailScreen extends StatelessWidget {
|
||||
const DetailScreen({super.key, required this.photo});
|
||||
|
||||
final String photo;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Detail')),
|
||||
body: Center(
|
||||
child: PhotoHero(
|
||||
photo: photo,
|
||||
width: 300,
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Hero Tag Best Practices
|
||||
|
||||
### Using Object as Tag
|
||||
|
||||
For unique, consistent tags:
|
||||
|
||||
```dart
|
||||
// Data model
|
||||
class Photo {
|
||||
final String url;
|
||||
final String id;
|
||||
|
||||
Photo({required this.url, required this.id});
|
||||
}
|
||||
|
||||
// Source
|
||||
Hero(
|
||||
tag: photo.id, // Use unique identifier
|
||||
child: Image.network(photo.url),
|
||||
)
|
||||
|
||||
// Destination
|
||||
Hero(
|
||||
tag: photo.id, // Same unique identifier
|
||||
child: Image.network(photo.url),
|
||||
)
|
||||
```
|
||||
|
||||
### Using Data Object as Tag
|
||||
|
||||
When data object is consistent:
|
||||
|
||||
```dart
|
||||
// Source
|
||||
Hero(
|
||||
tag: photo, // Photo object must be same instance or implement ==
|
||||
child: Image.network(photo.url),
|
||||
)
|
||||
|
||||
// Destination
|
||||
Hero(
|
||||
tag: photo, // Same Photo object
|
||||
child: Image.network(photo.url),
|
||||
)
|
||||
```
|
||||
|
||||
**Important:** If using object as tag, ensure proper `==` and `hashCode` implementation.
|
||||
|
||||
## Custom Hero Flight Path
|
||||
|
||||
### MaterialRectArcTween (Default)
|
||||
|
||||
```dart
|
||||
Hero(
|
||||
tag: 'hero-image',
|
||||
flightShuttleBuilder: (flightContext, animation, direction, fromContext, toContext) {
|
||||
return AnimatedBuilder(
|
||||
animation: animation,
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: animation.value,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
createRectTween: (begin, end) {
|
||||
return MaterialRectArcTween(begin: begin, end: end);
|
||||
},
|
||||
child: Image.asset('image.png'),
|
||||
)
|
||||
```
|
||||
|
||||
### MaterialRectCenterArcTween
|
||||
|
||||
Center-based interpolation (good for maintaining aspect ratio):
|
||||
|
||||
```dart
|
||||
static RectTween _createRectTween(Rect? begin, Rect? end) {
|
||||
return MaterialRectCenterArcTween(begin: begin, end: end);
|
||||
}
|
||||
|
||||
Hero(
|
||||
tag: 'hero-image',
|
||||
createRectTween: _createRectTween,
|
||||
child: Image.asset('image.png'),
|
||||
)
|
||||
```
|
||||
|
||||
### Custom RectTween
|
||||
|
||||
For complete control:
|
||||
|
||||
```dart
|
||||
class LinearRectTween extends Tween<Rect> {
|
||||
LinearRectTween({required Rect begin, required Rect end})
|
||||
: super(begin: begin, end: end);
|
||||
|
||||
@override
|
||||
Rect lerp(double t) => Rect.lerp(begin!, end!, t);
|
||||
}
|
||||
|
||||
Hero(
|
||||
tag: 'hero-image',
|
||||
createRectTween: (begin, end) => LinearRectTween(begin: begin, end: end),
|
||||
child: Image.asset('image.png'),
|
||||
)
|
||||
```
|
||||
|
||||
## Radial Hero Animation
|
||||
|
||||
Transform from circle to rectangle during transition.
|
||||
|
||||
### RadialExpansion Widget
|
||||
|
||||
```dart
|
||||
import 'dart:math' as math;
|
||||
|
||||
class RadialExpansion extends StatelessWidget {
|
||||
const RadialExpansion({
|
||||
super.key,
|
||||
required this.maxRadius,
|
||||
this.child,
|
||||
}) : clipRectSize = 2.0 * (maxRadius / math.sqrt2);
|
||||
|
||||
final double maxRadius;
|
||||
final double clipRectSize;
|
||||
final Widget? child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ClipOval(
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: clipRectSize,
|
||||
height: clipRectSize,
|
||||
child: ClipRect(child: child),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Radial Photo Widget
|
||||
|
||||
```dart
|
||||
class RadialPhoto extends StatelessWidget {
|
||||
const RadialPhoto({
|
||||
super.key,
|
||||
required this.photo,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
final String photo;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Theme.of(context).primaryColor.withValues(alpha: 0.25),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Image.asset(
|
||||
photo,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Complete Radial Hero Example
|
||||
|
||||
```dart
|
||||
class RadialHeroAnimation extends StatelessWidget {
|
||||
const RadialHeroAnimation({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Radial Hero')),
|
||||
body: ListView(
|
||||
children: List.generate(6, (index) {
|
||||
final photo = 'images/photo_$index.png';
|
||||
return Hero(
|
||||
tag: photo,
|
||||
createRectTween: _createRectTween,
|
||||
flightShuttleBuilder: (flightContext, animation, direction, fromContext, toContext) {
|
||||
return AnimatedBuilder(
|
||||
animation: animation,
|
||||
builder: (context, child) {
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: RadialExpansion(
|
||||
maxRadius: 120,
|
||||
child: RadialPhoto(
|
||||
photo: photo,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) => DetailScreen(photo: photo),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static RectTween _createRectTween(Rect? begin, Rect? end) {
|
||||
return MaterialRectCenterArcTween(begin: begin, end: end);
|
||||
}
|
||||
}
|
||||
|
||||
class DetailScreen extends StatelessWidget {
|
||||
const DetailScreen({super.key, required this.photo});
|
||||
|
||||
final String photo;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Detail')),
|
||||
body: Center(
|
||||
child: Hero(
|
||||
tag: photo,
|
||||
createRectTween: RadialHeroAnimation._createRectTween,
|
||||
child: SizedBox(
|
||||
width: 300,
|
||||
height: 300,
|
||||
child: RadialPhoto(
|
||||
photo: photo,
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Placeholder During Flight
|
||||
|
||||
### flightShuttleBuilder
|
||||
|
||||
Customize hero appearance during flight:
|
||||
|
||||
```dart
|
||||
Hero(
|
||||
tag: 'hero-image',
|
||||
flightShuttleBuilder: (flightContext, animation, direction, fromContext, toContext) {
|
||||
// fromContext - source hero's context
|
||||
// toContext - destination hero's context
|
||||
// direction - HeroFlightDirection.push or pop
|
||||
// animation - Animation<double> for the flight
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: animation,
|
||||
builder: (context, child) {
|
||||
return Transform.rotate(
|
||||
angle: animation.value * math.pi,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: Image.asset('image.png'),
|
||||
)
|
||||
```
|
||||
|
||||
### Replace Entire Hero During Flight
|
||||
|
||||
```dart
|
||||
Hero(
|
||||
tag: 'hero-image',
|
||||
flightShuttleBuilder: (flightContext, animation, direction, fromContext, toContext) {
|
||||
// Show different widget during flight
|
||||
return Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
color: Colors.blue,
|
||||
);
|
||||
},
|
||||
child: Image.asset('image.png'),
|
||||
)
|
||||
```
|
||||
|
||||
## Transition Settings
|
||||
|
||||
### Animation Duration
|
||||
|
||||
```dart
|
||||
MaterialApp(
|
||||
// Set global hero animation duration
|
||||
theme: ThemeData(
|
||||
pageTransitionsTheme: PageTransitionsTheme(
|
||||
builders: {
|
||||
TargetPlatform.android: ZoomPageTransitionsBuilder(),
|
||||
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
### Custom PageTransitionBuilder
|
||||
|
||||
```dart
|
||||
class CustomHeroTransitionBuilder extends PageTransitionsBuilder {
|
||||
@override
|
||||
Widget buildTransitions<T>(
|
||||
PageRoute<T> route,
|
||||
BuildContext? secondaryContext,
|
||||
Widget child,
|
||||
) {
|
||||
return FadeUpwardsPageTransitionsBuilder().buildTransitions(
|
||||
route,
|
||||
secondaryContext,
|
||||
child,
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Hero Mode
|
||||
|
||||
### Disable Hero Animations
|
||||
|
||||
```dart
|
||||
HeroMode(
|
||||
enabled: false, // Disable all child hero animations
|
||||
child: ListView(
|
||||
children: [
|
||||
Hero(tag: 'image1', child: Image.asset('1.png')),
|
||||
Hero(tag: 'image2', child: Image.asset('2.png')),
|
||||
],
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
### Conditional Hero Mode
|
||||
|
||||
```dart
|
||||
HeroMode(
|
||||
enabled: !_disableAnimations,
|
||||
child: Hero(tag: 'image', child: Image.asset('image.png')),
|
||||
)
|
||||
```
|
||||
|
||||
## Debugging Hero Animations
|
||||
|
||||
### Slow Animation
|
||||
|
||||
```dart
|
||||
void main() {
|
||||
timeDilation = 10.0; // 10x slower
|
||||
runApp(MyApp());
|
||||
}
|
||||
```
|
||||
|
||||
### Visualize Hero Bounds
|
||||
|
||||
```dart
|
||||
void main() {
|
||||
debugPaintSizeEnabled = true;
|
||||
runApp(MyApp());
|
||||
}
|
||||
```
|
||||
|
||||
### Print Hero Tags
|
||||
|
||||
```dart
|
||||
class DebugHero extends StatelessWidget {
|
||||
const DebugHero({
|
||||
super.key,
|
||||
required this.tag,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
final Object tag;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
print('Building Hero with tag: $tag');
|
||||
return Hero(tag: tag, child: child);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### DO
|
||||
|
||||
- Use unique, consistent tags (often the data object itself)
|
||||
- Keep hero widget trees similar between routes
|
||||
- Wrap images in `Material` with transparent color for "pop" effect
|
||||
- Use `timeDilation` to debug transitions
|
||||
- Consider `createRectTween` for custom flight paths
|
||||
- Use `flightShuttleBuilder` for custom flight appearance
|
||||
|
||||
### DON'T
|
||||
|
||||
- Use duplicate tags (conflicts!)
|
||||
- Change hero structure significantly between routes (jarring transition)
|
||||
- Forget `Material` wrapper (no splash effect)
|
||||
- Use very large images in heroes (performance)
|
||||
- Ignore aspect ratio during transition (distortion)
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Grid to Fullscreen Image
|
||||
|
||||
**Grid item:**
|
||||
```dart
|
||||
PhotoHero(
|
||||
photo: photo.url,
|
||||
width: 150, // Smaller in grid
|
||||
onTap: () => Navigator.push(..., detailScreen),
|
||||
)
|
||||
```
|
||||
|
||||
**Fullscreen:**
|
||||
```dart
|
||||
PhotoHero(
|
||||
photo: photo.url,
|
||||
width: MediaQuery.of(context).size.width, // Full width
|
||||
onTap: () => Navigator.pop(),
|
||||
)
|
||||
```
|
||||
|
||||
### List Header to Page Header
|
||||
|
||||
**List:**
|
||||
```dart
|
||||
Hero(
|
||||
tag: 'header',
|
||||
child: SizedBox(
|
||||
height: 200,
|
||||
child: Image.asset('header.jpg'),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
**Page:**
|
||||
```dart
|
||||
Hero(
|
||||
tag: 'header',
|
||||
child: SizedBox(
|
||||
height: 300, // Larger on detail page
|
||||
child: Image.asset('header.jpg'),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
### Shared Element with Content Update
|
||||
|
||||
```dart
|
||||
// In both routes
|
||||
Hero(
|
||||
tag: 'card',
|
||||
child: Card(
|
||||
child: Column(
|
||||
children: [
|
||||
Image.asset('image.png'),
|
||||
Text(showDetails ? 'Full description...' : 'Brief description'),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
- Optimize hero images (compress, lazy load)
|
||||
- Use `RepaintBoundary` around hero children if needed
|
||||
- Test on low-end devices
|
||||
- Profile with Flutter DevTools Performance overlay
|
||||
- Avoid complex widget trees inside hero
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Respect `MediaQuery.disableAnimations` setting
|
||||
- Consider alternative navigation for users who prefer no animations
|
||||
- Ensure hero content remains accessible during transition
|
||||
- Test with screen readers
|
||||
|
||||
## Advanced Techniques
|
||||
|
||||
### Multiple Heroes on Same Route
|
||||
|
||||
```dart
|
||||
class PhotoDetail extends StatelessWidget {
|
||||
const PhotoDetail({super.key, required this.photos});
|
||||
|
||||
final List<String> photos;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
// Main photo hero
|
||||
Positioned.fill(
|
||||
child: Hero(
|
||||
tag: photos[0],
|
||||
child: Image.asset(photos[0]),
|
||||
),
|
||||
),
|
||||
// Overlay hero (e.g., like button)
|
||||
Positioned(
|
||||
top: 16,
|
||||
right: 16,
|
||||
child: Hero(
|
||||
tag: 'like-button',
|
||||
child: Icon(Icons.favorite),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Nested Heroes
|
||||
|
||||
```dart
|
||||
Hero(
|
||||
tag: 'parent',
|
||||
child: Card(
|
||||
child: Column(
|
||||
children: [
|
||||
Hero(
|
||||
tag: 'child-image',
|
||||
child: Image.asset('image.png'),
|
||||
),
|
||||
Hero(
|
||||
tag: 'child-title',
|
||||
child: Text('Title'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
### Hero with Scroll Views
|
||||
|
||||
```dart
|
||||
class ScrollableHero extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
expandedHeight: 300,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: Hero(
|
||||
tag: 'header-image',
|
||||
child: Image.asset('header.jpg', fit: BoxFit.cover),
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate([
|
||||
// Content
|
||||
]),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,368 @@
|
||||
# Implicit Animations Reference
|
||||
|
||||
Implicit animations automatically handle animations when widget properties change. No AnimationController or explicit state management needed.
|
||||
|
||||
## Core Concept
|
||||
|
||||
Implicitly animated widgets extend `ImplicitlyAnimatedWidget`. When you change a property (color, size, etc.), the widget automatically animates to the new value using a specified duration and curve.
|
||||
|
||||
## Available Widgets
|
||||
|
||||
### AnimatedContainer
|
||||
|
||||
Animates multiple properties simultaneously.
|
||||
|
||||
```dart
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
width: _expanded ? 200 : 100,
|
||||
height: _expanded ? 200 : 100,
|
||||
decoration: BoxDecoration(
|
||||
color: _expanded ? Colors.blue : Colors.red,
|
||||
borderRadius: BorderRadius.circular(_expanded ? 20 : 8),
|
||||
),
|
||||
child: const FlutterLogo(),
|
||||
)
|
||||
```
|
||||
|
||||
**Animatable properties:**
|
||||
- `width`, `height` - Size
|
||||
- `color` - Background color
|
||||
- `decoration` - BoxDecoration (includes border, shadow, image, etc.)
|
||||
- `padding` - Padding
|
||||
- `margin` - Margin (via decoration)
|
||||
- `transform` - Matrix4 transformation
|
||||
|
||||
### AnimatedOpacity
|
||||
|
||||
Fades a widget in and out.
|
||||
|
||||
```dart
|
||||
AnimatedOpacity(
|
||||
opacity: _visible ? 1.0 : 0.0,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.easeInOut,
|
||||
onEnd: () => print('Animation complete'),
|
||||
child: const Text('Hello'),
|
||||
)
|
||||
```
|
||||
|
||||
### AnimatedPadding
|
||||
|
||||
Animates padding changes.
|
||||
|
||||
```dart
|
||||
AnimatedPadding(
|
||||
padding: EdgeInsets.all(_padded ? 16.0 : 8.0),
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.ease,
|
||||
child: const Card(child: Text('Padded content')),
|
||||
)
|
||||
```
|
||||
|
||||
### AnimatedPositioned
|
||||
|
||||
Animates position within a Stack.
|
||||
|
||||
```dart
|
||||
Stack(
|
||||
children: [
|
||||
AnimatedPositioned(
|
||||
top: _top ? 0 : 100,
|
||||
left: _left ? 0 : 100,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.easeInOut,
|
||||
child: const FlutterLogo(),
|
||||
),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
### AnimatedAlign
|
||||
|
||||
Animates alignment changes.
|
||||
|
||||
```dart
|
||||
AnimatedAlign(
|
||||
alignment: _aligned ? Alignment.topLeft : Alignment.bottomRight,
|
||||
duration: const Duration(milliseconds: 400),
|
||||
curve: Curves.easeInOut,
|
||||
child: const Text('Align me'),
|
||||
)
|
||||
```
|
||||
|
||||
### AnimatedContainer (Multiple Properties)
|
||||
|
||||
```dart
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
transform: Matrix4.rotationZ(_rotated ? 0.5 : 0),
|
||||
child: const FlutterLogo(),
|
||||
)
|
||||
```
|
||||
|
||||
### TweenAnimationBuilder
|
||||
|
||||
Custom tween animation without creating a custom widget.
|
||||
|
||||
```dart
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween<double>(begin: 0, end: 1),
|
||||
duration: const Duration(seconds: 1),
|
||||
curve: Curves.easeInOut,
|
||||
builder: (context, value, child) {
|
||||
return Opacity(
|
||||
opacity: value,
|
||||
child: Transform.scale(
|
||||
scale: value,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const FlutterLogo(),
|
||||
)
|
||||
```
|
||||
|
||||
### AnimatedSwitcher
|
||||
|
||||
Cross-fades between two widgets.
|
||||
|
||||
```dart
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
transitionBuilder: (Widget child, Animation<double> animation) {
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: _showFirst
|
||||
? const Text('First')
|
||||
: const Text('Second'),
|
||||
)
|
||||
```
|
||||
|
||||
With custom size transition:
|
||||
```dart
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
transitionBuilder: (Widget child, Animation<double> animation) {
|
||||
return SizeTransition(
|
||||
sizeFactor: animation,
|
||||
axis: Axis.vertical,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: _expanded
|
||||
? const Text('Expanded content')
|
||||
: const Text('Collapsed content'),
|
||||
)
|
||||
```
|
||||
|
||||
### AnimatedDefaultTextStyle
|
||||
|
||||
Animates text style changes.
|
||||
|
||||
```dart
|
||||
AnimatedDefaultTextStyle(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
style: TextStyle(
|
||||
fontSize: _large ? 32 : 16,
|
||||
color: _colored ? Colors.blue : Colors.black,
|
||||
fontWeight: _bold ? FontWeight.bold : FontWeight.normal,
|
||||
),
|
||||
child: const Text('Animated text'),
|
||||
)
|
||||
```
|
||||
|
||||
### AnimatedPhysicalModel
|
||||
|
||||
Animates elevation, color, and shape with physical shadow.
|
||||
|
||||
```dart
|
||||
AnimatedPhysicalModel(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
shape: BoxShape.rectangle,
|
||||
elevation: _elevated ? 12 : 0,
|
||||
color: _elevated ? Colors.blue : Colors.grey[300]!,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Text('Physical model'),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
### AnimatedTheme
|
||||
|
||||
Animates theme changes.
|
||||
|
||||
```dart
|
||||
AnimatedTheme(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
data: ThemeData(
|
||||
brightness: _darkMode ? Brightness.dark : Brightness.light,
|
||||
primaryColor: _darkMode ? Colors.blue[700] : Colors.blue,
|
||||
),
|
||||
child: const MyChildWidget(),
|
||||
)
|
||||
```
|
||||
|
||||
## Common Parameters
|
||||
|
||||
All implicit animations support these parameters:
|
||||
|
||||
```dart
|
||||
AnimatedContainer(
|
||||
duration: Duration(milliseconds: 300), // Required: Animation duration
|
||||
curve: Curves.easeInOut, // Optional: Animation curve
|
||||
onEnd: () {}, // Optional: Completion callback
|
||||
child: Widget(), // The child widget to animate
|
||||
)
|
||||
```
|
||||
|
||||
### Duration
|
||||
|
||||
```dart
|
||||
const Duration(milliseconds: 300) // 0.3 seconds
|
||||
const Duration(seconds: 1) // 1 second
|
||||
const Duration(days: 1) // 1 day
|
||||
```
|
||||
|
||||
### Curves
|
||||
|
||||
Common curves from `Curves` class:
|
||||
|
||||
```dart
|
||||
Curves.linear // Linear (no easing)
|
||||
Curves.ease // Simple ease in and out
|
||||
Curves.easeIn // Slow start, fast end
|
||||
Curves.easeOut // Fast start, slow end
|
||||
Curves.easeInOut // Slow start and end, fast middle
|
||||
Curves.easeInCubic // Cubic ease in
|
||||
Curves.easeOutCubic // Cubic ease out
|
||||
Curves.easeInOutCubic // Cubic ease in and out
|
||||
Curves.elasticIn // Bounces in
|
||||
Curves.elasticOut // Bounces out
|
||||
Curves.elasticInOut // Bounces in and out
|
||||
Curves.bounceIn // Bounces in (more subtle)
|
||||
Curves.bounceOut // Bounces out (more subtle)
|
||||
```
|
||||
|
||||
Custom curve:
|
||||
```dart
|
||||
class ShakeCurve extends Curve {
|
||||
@override
|
||||
double transform(double t) => sin(t * pi * 2);
|
||||
}
|
||||
|
||||
// Usage
|
||||
AnimatedContainer(
|
||||
curve: ShakeCurve(),
|
||||
// ...
|
||||
)
|
||||
```
|
||||
|
||||
### onEnd Callback
|
||||
|
||||
Trigger action when animation completes:
|
||||
|
||||
```dart
|
||||
AnimatedOpacity(
|
||||
opacity: _visible ? 1.0 : 0.0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
onEnd: () {
|
||||
if (!_visible) {
|
||||
// Animation finished fading out
|
||||
// Remove widget, navigate, etc.
|
||||
}
|
||||
},
|
||||
child: const Text('Hello'),
|
||||
)
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### DO
|
||||
|
||||
- Use implicit animations for simple, one-off animations
|
||||
- Set appropriate durations for natural feel (200-500ms typical)
|
||||
- Use curves to make animations feel natural
|
||||
- Use `onEnd` callback for post-animation actions
|
||||
- Test animations on various devices
|
||||
- Consider `MediaQuery.disableAnimations` for accessibility
|
||||
|
||||
### DON'T
|
||||
|
||||
- Use implicit animations for complex, multi-property sequences (use staggered instead)
|
||||
- Forget to set duration (defaults to instant change)
|
||||
- Use extremely long durations (> 1s) without good reason
|
||||
- Animate too many properties simultaneously (performance impact)
|
||||
- Nest implicit animations deeply (can cause jank)
|
||||
|
||||
## Performance Tips
|
||||
|
||||
- Implicit animations are optimized but still rebuild during animation
|
||||
- Avoid animating complex widget trees
|
||||
- Use `RepaintBoundary` around animated children to isolate repaints
|
||||
- Test performance with Flutter DevTools Performance overlay
|
||||
- Consider using `AnimatedBuilder` for complex widgets to minimize rebuilds
|
||||
|
||||
## Debugging
|
||||
|
||||
### Slow Animations
|
||||
|
||||
```dart
|
||||
// Slow all animations by 10x during development
|
||||
timeDilation = 10.0;
|
||||
```
|
||||
|
||||
### Show Repaint Rainbows
|
||||
|
||||
```dart
|
||||
void main() {
|
||||
debugPaintSizeEnabled = true;
|
||||
runApp(MyApp());
|
||||
}
|
||||
```
|
||||
|
||||
### Performance Overlay
|
||||
|
||||
```dart
|
||||
void main() {
|
||||
runApp(MyApp());
|
||||
}
|
||||
|
||||
// In debug mode, press 'p' to toggle performance overlay
|
||||
```
|
||||
|
||||
## Comparison: Implicit vs Explicit
|
||||
|
||||
| Feature | Implicit | Explicit |
|
||||
|---------|----------|-----------|
|
||||
| Controller needed | No | Yes (AnimationController) |
|
||||
| Setup complexity | Low | Medium/High |
|
||||
| Reusability | Widget-based | Controller-based |
|
||||
| Multiple properties | AnimatedContainer only | Unlimited |
|
||||
| Lifecycle control | Limited | Full |
|
||||
| State monitoring | Limited (onEnd) | Full (addStatusListener) |
|
||||
| Performance | Good | Excellent (with proper patterns) |
|
||||
|
||||
## When to Use Implicit Animations
|
||||
|
||||
Use implicit animations when:
|
||||
- Animating a single property
|
||||
- Animation is triggered by state change
|
||||
- No need for fine-grained control
|
||||
- Want simple, declarative code
|
||||
- Animation is one-time or simple toggle
|
||||
|
||||
Use explicit animations when:
|
||||
- Need full control over animation lifecycle
|
||||
- Animating multiple properties
|
||||
- Need to react to animation state
|
||||
- Creating reusable animation components
|
||||
- Need complex timing or sequencing
|
||||
- Performance is critical
|
||||
@@ -0,0 +1,700 @@
|
||||
# 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: [...],
|
||||
)
|
||||
```
|
||||
@@ -0,0 +1,726 @@
|
||||
# Staggered Animations Reference
|
||||
|
||||
Staggered animations run multiple animations with different timing offsets, creating sequential or overlapping visual effects.
|
||||
|
||||
## Core Concept
|
||||
|
||||
All animations share one `AnimationController`. Each animation has an `Interval` defining when it starts and ends within the controller's timeline.
|
||||
|
||||
## Basic Staggered Animation
|
||||
|
||||
### Two-Property Stagger
|
||||
|
||||
```dart
|
||||
class StaggeredFadeSlide extends StatelessWidget {
|
||||
const StaggeredFadeSlide({super.key, required this.controller});
|
||||
|
||||
final AnimationController controller;
|
||||
|
||||
// Fade in first (0.0 - 0.5 of controller)
|
||||
late final Animation<double> opacity = Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: const Interval(0.0, 0.5, curve: Curves.easeIn),
|
||||
),
|
||||
);
|
||||
|
||||
// Slide in later (0.25 - 1.0 of controller)
|
||||
late final Animation<Offset> slide = Tween<Offset>(
|
||||
begin: Offset(0, 0.5),
|
||||
end: Offset.zero,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: const Interval(0.25, 1.0, curve: Curves.easeOut),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: controller,
|
||||
builder: (context, child) {
|
||||
return FadeTransition(
|
||||
opacity: opacity,
|
||||
child: SlideTransition(
|
||||
position: slide,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('Staggered animation'),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Multiple Intervals Example
|
||||
|
||||
```dart
|
||||
class MultiPropertyStagger extends StatelessWidget {
|
||||
const MultiPropertyStagger({super.key, required this.controller});
|
||||
|
||||
final AnimationController controller;
|
||||
|
||||
// Opacity: 0.0 - 0.1 (10%)
|
||||
late final Animation<double> opacity = Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: const Interval(0.0, 0.1, curve: Curves.ease),
|
||||
),
|
||||
);
|
||||
|
||||
// Width: 0.125 - 0.25 (12.5% - 25%)
|
||||
late final Animation<double> width = Tween<double>(begin: 50, end: 150).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: const Interval(0.125, 0.25, curve: Curves.ease),
|
||||
),
|
||||
);
|
||||
|
||||
// Height: 0.25 - 0.375 (25% - 37.5%)
|
||||
late final Animation<double> height = Tween<double>(begin: 50, end: 150).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: const Interval(0.25, 0.375, curve: Curves.ease),
|
||||
),
|
||||
);
|
||||
|
||||
// Border radius: 0.375 - 0.5 (37.5% - 50%)
|
||||
late final Animation<BorderRadius?> borderRadius = BorderRadiusTween(
|
||||
begin: BorderRadius.circular(4),
|
||||
end: BorderRadius.circular(75),
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: const Interval(0.375, 0.5, curve: Curves.ease),
|
||||
),
|
||||
);
|
||||
|
||||
// Color: 0.5 - 0.625 (50% - 62.5%)
|
||||
late final Animation<Color?> color = ColorTween(
|
||||
begin: Colors.red,
|
||||
end: Colors.orange,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: const Interval(0.5, 0.625, curve: Curves.ease),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: controller,
|
||||
builder: (context, child) {
|
||||
return Opacity(
|
||||
opacity: opacity.value,
|
||||
child: Container(
|
||||
width: width.value,
|
||||
height: height.value,
|
||||
decoration: BoxDecoration(
|
||||
color: color.value,
|
||||
borderRadius: borderRadius.value,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const FlutterLogo(),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Controller Setup
|
||||
|
||||
### Single Controller
|
||||
|
||||
```dart
|
||||
class _StaggerState extends State<StaggerWidget>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 2000),
|
||||
vsync: this,
|
||||
);
|
||||
_controller.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StaggerAnimation(controller: _controller.view);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Multiple Controllers
|
||||
|
||||
For independent animation sequences:
|
||||
|
||||
```dart
|
||||
class _ComplexStaggerState extends State<ComplexStaggerWidget>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _fadeInController;
|
||||
late AnimationController _slideController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_fadeInController = AnimationController(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_slideController = AnimationController(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
// Start fade, then slide
|
||||
_fadeInController.forward().then((_) {
|
||||
_slideController.forward();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_fadeInController.dispose();
|
||||
_slideController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Interval Timing
|
||||
|
||||
### Understanding Interval
|
||||
|
||||
```dart
|
||||
Interval(
|
||||
0.25, // Start at 25% of controller duration
|
||||
0.75, // End at 75% of controller duration
|
||||
curve: Curves.easeInOut,
|
||||
)
|
||||
```
|
||||
|
||||
**Example:** If controller duration is 2000ms:
|
||||
- Animation starts at 500ms (0.25 * 2000)
|
||||
- Animation ends at 1500ms (0.75 * 2000)
|
||||
- Animation takes 1000ms (1500 - 500)
|
||||
|
||||
### Overlapping Intervals
|
||||
|
||||
```dart
|
||||
// Animation 1: 0.0 - 0.5 (first half)
|
||||
anim1 = Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: const Interval(0.0, 0.5),
|
||||
),
|
||||
);
|
||||
|
||||
// Animation 2: 0.3 - 0.8 (starts before anim1 ends)
|
||||
anim2 = Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: const Interval(0.3, 0.8),
|
||||
),
|
||||
);
|
||||
|
||||
// Animation 3: 0.6 - 1.0 (starts after anim1 ends)
|
||||
anim3 = Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: const Interval(0.6, 1.0),
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
### Gaps Between Animations
|
||||
|
||||
```dart
|
||||
// Animation 1: 0.0 - 0.4
|
||||
// Gap: 0.4 - 0.5
|
||||
// Animation 2: 0.5 - 0.9
|
||||
|
||||
anim1 = Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: const Interval(0.0, 0.4),
|
||||
),
|
||||
);
|
||||
|
||||
anim2 = Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: const Interval(0.5, 0.9),
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
## Staggered List Animation
|
||||
|
||||
### Menu Items Animation
|
||||
|
||||
```dart
|
||||
class StaggeredMenu extends StatefulWidget {
|
||||
const StaggeredMenu({super.key});
|
||||
|
||||
static const _menuItems = [
|
||||
'Home',
|
||||
'Profile',
|
||||
'Settings',
|
||||
'About',
|
||||
];
|
||||
|
||||
@override
|
||||
State<StaggeredMenu> createState() => _StaggeredMenuState();
|
||||
}
|
||||
|
||||
class _StaggeredMenuState extends State<StaggeredMenu>
|
||||
with SingleTickerProviderStateMixin {
|
||||
static const _itemDelayTime = Duration(milliseconds: 50);
|
||||
static const _itemAnimationTime = Duration(milliseconds: 250);
|
||||
|
||||
late AnimationController _controller;
|
||||
late List<Animation<double>> _itemAnimations;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: _itemAnimationTime +
|
||||
(_itemDelayTime * (StaggeredMenu._menuItems.length - 1)),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_itemAnimations = StaggeredMenu._menuItems.map((item) {
|
||||
final index = StaggeredMenu._menuItems.indexOf(item);
|
||||
return Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Interval(
|
||||
index * 0.1, // Start later for each item
|
||||
(index * 0.1) + 0.4, // Each takes 40% of controller
|
||||
curve: Curves.easeOut,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
|
||||
_controller.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView.builder(
|
||||
itemCount: StaggeredMenu._menuItems.length,
|
||||
itemBuilder: (context, index) {
|
||||
return AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
return SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: Offset(1.0, 0),
|
||||
end: Offset.zero,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Interval(
|
||||
index * 0.1,
|
||||
(index * 0.1) + 0.4,
|
||||
curve: Curves.easeOut,
|
||||
),
|
||||
)),
|
||||
child: FadeTransition(
|
||||
opacity: _itemAnimations[index],
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: ListTile(
|
||||
title: Text(StaggeredMenu._menuItems[index]),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Grid Animation
|
||||
|
||||
```dart
|
||||
class StaggeredGrid extends StatefulWidget {
|
||||
const StaggeredGrid({super.key, required this.itemCount});
|
||||
|
||||
final int itemCount;
|
||||
|
||||
@override
|
||||
State<StaggeredGrid> createState() => _StaggeredGridState();
|
||||
}
|
||||
|
||||
class _StaggeredGridState extends State<StaggeredGrid>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late List<Animation<double>> _animations;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 1500),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_animations = List.generate(widget.itemCount, (index) {
|
||||
return Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Interval(
|
||||
index / widget.itemCount,
|
||||
(index / widget.itemCount) + 0.3,
|
||||
curve: Curves.easeOut,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
_controller.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GridView.builder(
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
),
|
||||
itemCount: widget.itemCount,
|
||||
itemBuilder: (context, index) {
|
||||
return AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
return FadeTransition(
|
||||
opacity: _animations[index],
|
||||
child: ScaleTransition(
|
||||
scale: _animations[index],
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Card(
|
||||
child: Center(child: Text('Item $index')),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Complex Staggered Patterns
|
||||
|
||||
### Sequential Completion
|
||||
|
||||
```dart
|
||||
animation1 = Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: const Interval(0.0, 0.3),
|
||||
),
|
||||
);
|
||||
|
||||
animation2 = Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: const Interval(0.3, 0.6),
|
||||
),
|
||||
);
|
||||
|
||||
animation3 = Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: const Interval(0.6, 1.0),
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
### Ripple Effect
|
||||
|
||||
```dart
|
||||
// Center expands first, then outward
|
||||
for (int i = 0; i < itemCount; i++) {
|
||||
final distance = (i - centerIndex).abs();
|
||||
animations[i] = Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: Interval(
|
||||
distance * 0.1,
|
||||
(distance * 0.1) + 0.3,
|
||||
curve: Curves.easeOut,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Staggered Reveal
|
||||
|
||||
```dart
|
||||
// Reveal items one by one from top
|
||||
for (int i = 0; i < itemCount; i++) {
|
||||
animations[i] = CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: Interval(
|
||||
i * 0.1,
|
||||
(i * 0.1) + 0.15,
|
||||
curve: Curves.easeOut,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Use for opacity or transform
|
||||
Opacity(opacity: animations[i].value)
|
||||
```
|
||||
|
||||
## Duration Calculation
|
||||
|
||||
### Calculate Total Duration
|
||||
|
||||
```dart
|
||||
const itemDelay = Duration(milliseconds: 50);
|
||||
const itemAnimationTime = Duration(milliseconds: 250);
|
||||
const itemCount = 10;
|
||||
|
||||
final totalDuration = itemAnimationTime +
|
||||
(itemDelay * (itemCount - 1));
|
||||
|
||||
// 250ms + (50ms * 9) = 700ms
|
||||
```
|
||||
|
||||
### Using in Controller
|
||||
|
||||
```dart
|
||||
_controller = AnimationController(
|
||||
duration: totalDuration,
|
||||
vsync: this,
|
||||
);
|
||||
```
|
||||
|
||||
### Dynamic Duration
|
||||
|
||||
```dart
|
||||
late AnimationController _controller;
|
||||
late Duration _totalDuration;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
final itemCount = _items.length;
|
||||
const itemDelay = Duration(milliseconds: 50);
|
||||
const itemAnimationTime = Duration(milliseconds: 250);
|
||||
|
||||
_totalDuration = itemAnimationTime + (itemDelay * (itemCount - 1));
|
||||
|
||||
_controller = AnimationController(
|
||||
duration: _totalDuration,
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
// Create animations...
|
||||
_controller.forward();
|
||||
}
|
||||
```
|
||||
|
||||
## Repeating Staggered Animations
|
||||
|
||||
### Loop on Completion
|
||||
|
||||
```dart
|
||||
_controller.addStatusListener((status) {
|
||||
if (status == AnimationStatus.completed) {
|
||||
_controller.reset();
|
||||
_controller.forward();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Ping-Pong (Forward and Reverse)
|
||||
|
||||
```dart
|
||||
_controller.addStatusListener((status) {
|
||||
if (status == AnimationStatus.completed) {
|
||||
_controller.reverse();
|
||||
} else if (status == AnimationStatus.dismissed) {
|
||||
_controller.forward();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Debugging Staggered Animations
|
||||
|
||||
### Slow Animation
|
||||
|
||||
```dart
|
||||
void main() {
|
||||
timeDilation = 10.0; // 10x slower
|
||||
runApp(MyApp());
|
||||
}
|
||||
```
|
||||
|
||||
### Print Interval Ranges
|
||||
|
||||
```dart
|
||||
for (int i = 0; i < _animations.length; i++) {
|
||||
final anim = _animations[i] as CurvedAnimation;
|
||||
print('Animation $i: ${anim.curve}');
|
||||
}
|
||||
|
||||
// Output:
|
||||
// Animation 0: Interval(0.0, 0.4, Curves.easeOut)
|
||||
// Animation 1: Interval(0.1, 0.5, Curves.easeOut)
|
||||
// ...
|
||||
```
|
||||
|
||||
### Visualize Animation State
|
||||
|
||||
```dart
|
||||
class DebugStaggeredWidget extends StatelessWidget {
|
||||
const DebugStaggeredWidget({super.key, required this.controller, required this.animations});
|
||||
|
||||
final AnimationController controller;
|
||||
final List<Animation<double>> animations;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
for (int i = 0; i < animations.length; i++)
|
||||
Text('Animation $i: ${animations[i].value.toStringAsFixed(2)}'),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Best Practices
|
||||
|
||||
### DO
|
||||
|
||||
- Use one controller when animations are related
|
||||
- Calculate total duration correctly
|
||||
- Use `AnimatedBuilder` for optimal rebuilds
|
||||
- Profile with Flutter DevTools
|
||||
- Test on various devices
|
||||
- Consider reducing animation complexity on low-end devices
|
||||
|
||||
### DON'T
|
||||
|
||||
- Create too many controllers unnecessarily
|
||||
- Forget to dispose controllers
|
||||
- Use complex widget trees inside staggered animations
|
||||
- Animate too many items simultaneously (jank)
|
||||
- Use very long durations without good reason
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Loading Animation
|
||||
|
||||
```dart
|
||||
// Dot 1: 0.0 - 0.3
|
||||
// Dot 2: 0.1 - 0.4
|
||||
// Dot 3: 0.2 - 0.5
|
||||
// Repeat forever
|
||||
```
|
||||
|
||||
### Success Animation Sequence
|
||||
|
||||
```dart
|
||||
// Checkmark appears: 0.0 - 0.3
|
||||
// "Success!" text fades in: 0.2 - 0.5
|
||||
// Confetti falls: 0.3 - 1.0
|
||||
```
|
||||
|
||||
### Onboarding Steps
|
||||
|
||||
```dart
|
||||
// Step 1: 0.0 - 0.25
|
||||
// Step 2: 0.25 - 0.5
|
||||
// Step 3: 0.5 - 0.75
|
||||
// Step 4: 0.75 - 1.0
|
||||
```
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Respect `MediaQuery.disableAnimations` setting
|
||||
- Provide alternative to complex staggered animations
|
||||
- Ensure content remains accessible during animation
|
||||
- Test with screen readers
|
||||
|
||||
## Advanced Techniques
|
||||
|
||||
### Conditional Staggering
|
||||
|
||||
```dart
|
||||
// Show animations based on device performance
|
||||
final isLowEnd = Platform.isAndroid && deviceInfo.version.sdkInt < 21;
|
||||
|
||||
final staggerDelay = isLowEnd
|
||||
? Duration(milliseconds: 100) // Slower on low-end
|
||||
: Duration(milliseconds: 50);
|
||||
```
|
||||
|
||||
### Adaptive Staggering
|
||||
|
||||
```dart
|
||||
// Adjust based on screen size
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final itemsPerRow = screenWidth ~/ 150;
|
||||
|
||||
// Calculate stagger based on grid position
|
||||
final row = index ~/ itemsPerRow;
|
||||
final col = index % itemsPerRow;
|
||||
final startDelay = (row * 0.2) + (col * 0.05);
|
||||
```
|
||||
|
||||
### Physics-Influenced Staggering
|
||||
|
||||
```dart
|
||||
// Use spring physics for staggered elements
|
||||
for (int i = 0; i < itemCount; i++) {
|
||||
animations[i] = Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: Interval(
|
||||
i * 0.05,
|
||||
(i * 0.05) + 0.5,
|
||||
curve: Curves.elasticOut,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user