Production-grade Flutter animations mastery - Implicit and explicit animations, AnimationController, Hero transitions, physics-based motion, Lottie/Rive integration, 60fps optimization with comprehensive code examples
Provides production-ready Flutter animation patterns including implicit/explicit animations, Hero transitions, physics-based motion, and Lottie/Rive integration with 60fps optimization techniques. Use when building animated UI components or optimizing Flutter app performance.
/plugin marketplace add pluginagentmarketplace/custom-plugin-flutter/plugin install custom-plugin-flutter@pluginagentmarketplace-flutterThis skill inherits all available tools. When active, it can use any tool Claude has access to.
assets/config.yamlassets/schema.jsonreferences/GUIDE.mdreferences/PATTERNS.mdscripts/validate.pyclass AnimatedProductCard extends StatefulWidget {
final Product product;
final bool isSelected;
const AnimatedProductCard({
required this.product,
required this.isSelected,
});
@override
State<AnimatedProductCard> createState() => _AnimatedProductCardState();
}
class _AnimatedProductCardState extends State<AnimatedProductCard>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<double> _scaleAnimation;
late final Animation<double> _opacityAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 200),
vsync: this,
);
_scaleAnimation = Tween<double>(begin: 1.0, end: 0.95).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
_opacityAnimation = Tween<double>(begin: 1.0, end: 0.8).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _onTapDown(TapDownDetails details) => _controller.forward();
void _onTapUp(TapUpDetails details) => _controller.reverse();
void _onTapCancel() => _controller.reverse();
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: _onTapDown,
onTapUp: _onTapUp,
onTapCancel: _onTapCancel,
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: Opacity(
opacity: _opacityAnimation.value,
child: child,
),
);
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
decoration: BoxDecoration(
border: Border.all(
color: widget.isSelected ? Colors.blue : Colors.grey,
width: widget.isSelected ? 2 : 1,
),
borderRadius: BorderRadius.circular(12),
),
child: ProductContent(product: widget.product),
),
),
);
}
}
// AnimatedContainer - Animate multiple properties
AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: isExpanded ? 300 : 100,
height: isExpanded ? 200 : 100,
decoration: BoxDecoration(
color: isActive ? Colors.blue : Colors.grey,
borderRadius: BorderRadius.circular(isExpanded ? 16 : 8),
boxShadow: isActive ? [BoxShadow(blurRadius: 10)] : [],
),
child: content,
)
// AnimatedOpacity
AnimatedOpacity(
opacity: isVisible ? 1.0 : 0.0,
duration: Duration(milliseconds: 200),
child: content,
)
// AnimatedPositioned (inside Stack)
AnimatedPositioned(
duration: Duration(milliseconds: 300),
left: isLeft ? 0 : 100,
top: isTop ? 0 : 100,
child: widget,
)
// AnimatedSwitcher - Cross-fade between widgets
AnimatedSwitcher(
duration: Duration(milliseconds: 300),
transitionBuilder: (child, animation) {
return FadeTransition(opacity: animation, child: child);
},
child: Text(
currentText,
key: ValueKey(currentText), // Important!
),
)
// TweenAnimationBuilder - Custom implicit animation
TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: progress),
duration: Duration(milliseconds: 500),
builder: (context, value, child) {
return CircularProgressIndicator(value: value);
},
)
class ExplicitAnimationWidget extends StatefulWidget {
@override
State<ExplicitAnimationWidget> createState() => _ExplicitAnimationWidgetState();
}
class _ExplicitAnimationWidgetState extends State<ExplicitAnimationWidget>
with TickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<Offset> _slideAnimation;
late final Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 500),
vsync: this,
);
_slideAnimation = Tween<Offset>(
begin: Offset(0, 1),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _controller,
curve: Interval(0.0, 0.6, curve: Curves.easeOut),
));
_fadeAnimation = Tween<double>(
begin: 0,
end: 1,
).animate(CurvedAnimation(
parent: _controller,
curve: Interval(0.3, 1.0, curve: Curves.easeIn),
));
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SlideTransition(
position: _slideAnimation,
child: FadeTransition(
opacity: _fadeAnimation,
child: Content(),
),
);
}
}
// Built-in transitions
FadeTransition(opacity: animation, child: widget)
SlideTransition(position: offsetAnimation, child: widget)
ScaleTransition(scale: animation, child: widget)
RotationTransition(turns: animation, child: widget)
SizeTransition(sizeFactor: animation, child: widget)
// AnimatedBuilder - Custom rendering
AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform(
transform: Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateY(_controller.value * pi),
alignment: Alignment.center,
child: child,
);
},
child: Card(),
)
// Source screen
Hero(
tag: 'product-${product.id}',
child: Image.network(product.imageUrl),
)
// Destination screen
Hero(
tag: 'product-${product.id}',
child: Image.network(product.imageUrl),
)
// Custom Hero flight
Hero(
tag: 'avatar',
flightShuttleBuilder: (
flightContext,
animation,
flightDirection,
fromHeroContext,
toHeroContext,
) {
return AnimatedBuilder(
animation: animation,
builder: (context, _) {
return CircleAvatar(
radius: lerpDouble(40, 60, animation.value),
);
},
);
},
child: CircleAvatar(),
)
class StaggeredList extends StatefulWidget {
@override
State<StaggeredList> createState() => _StaggeredListState();
}
class _StaggeredListState extends State<StaggeredList>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
final List<Animation<Offset>> _animations = [];
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 1000),
vsync: this,
);
// Create staggered animations
for (int i = 0; i < 5; i++) {
final start = i * 0.1;
final end = start + 0.4;
_animations.add(
Tween<Offset>(begin: Offset(1, 0), end: Offset.zero).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(start, end, curve: Curves.easeOut),
),
),
);
}
_controller.forward();
}
@override
Widget build(BuildContext context) {
return Column(
children: List.generate(5, (index) {
return SlideTransition(
position: _animations[index],
child: ListTile(title: Text('Item $index')),
);
}),
);
}
}
// Spring simulation
class SpringAnimation extends StatefulWidget {
@override
State<SpringAnimation> createState() => _SpringAnimationState();
}
class _SpringAnimationState extends State<SpringAnimation>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late Animation<double> _animation;
final SpringDescription spring = SpringDescription(
mass: 1,
stiffness: 100,
damping: 10,
);
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this);
final simulation = SpringSimulation(spring, 0, 1, 0);
_controller.animateWith(simulation);
_animation = _controller.drive(Tween(begin: 0.0, end: 100.0));
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Transform.translate(
offset: Offset(0, _animation.value),
child: child,
);
},
child: Ball(),
);
}
}
// Lottie animation
import 'package:lottie/lottie.dart';
Lottie.asset(
'assets/loading.json',
width: 200,
height: 200,
fit: BoxFit.contain,
repeat: true,
animate: true,
onLoaded: (composition) {
_controller.duration = composition.duration;
},
)
// Rive animation
import 'package:rive/rive.dart';
RiveAnimation.asset(
'assets/animation.riv',
fit: BoxFit.cover,
stateMachines: ['StateMachine1'],
onInit: (artboard) {
final controller = StateMachineController.fromArtboard(
artboard,
'StateMachine1',
);
artboard.addController(controller!);
_trigger = controller.findInput<bool>('trigger') as SMITrigger;
},
)
// Custom page route
class FadePageRoute<T> extends PageRouteBuilder<T> {
final Widget page;
FadePageRoute({required this.page})
: super(
pageBuilder: (context, animation, secondaryAnimation) => page,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(opacity: animation, child: child);
},
transitionDuration: Duration(milliseconds: 300),
);
}
// Slide + fade combined
transitionsBuilder: (context, animation, secondaryAnimation, child) {
final offsetAnimation = Tween<Offset>(
begin: Offset(1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
));
return SlideTransition(
position: offsetAnimation,
child: FadeTransition(
opacity: animation,
child: child,
),
);
}
// Use const where possible
const AnimatedContainer(...)
// RepaintBoundary for expensive animations
RepaintBoundary(
child: AnimatedWidget(),
)
// AnimatedBuilder isolates rebuilds
AnimatedBuilder(
animation: _controller,
child: ExpensiveWidget(), // Not rebuilt
builder: (context, child) {
return Transform.scale(
scale: _controller.value,
child: child, // Reused
);
},
)
// Avoid layout-triggering animations
// Good: Transform, Opacity
// Avoid: width/height in tight loops
Issue: Animation jank (dropped frames)
1. Use RepaintBoundary to isolate
2. Profile with DevTools Performance
3. Avoid layout changes during animation
4. Use AnimatedBuilder, not setState
5. Check for heavy build methods
Issue: Animation doesn't start
1. Verify AnimationController.forward() called
2. Check vsync: this (TickerProviderStateMixin)
3. Verify widget is mounted before animating
4. Check duration is set
Issue: Animation leaks memory
1. Dispose AnimationController in dispose()
2. Cancel any listeners
3. Use mounted check before setState
| Need | Solution |
|---|---|
| Simple property change | AnimatedContainer |
| Cross-fade widgets | AnimatedSwitcher |
| Custom timing control | AnimationController |
| Shared element | Hero |
| List items appearing | Staggered animations |
| Natural motion | Physics simulation |
| Complex vector | Lottie/Rive |
Create fluid, 60fps animations in Flutter.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.