Implicit Animation Widgets: AnimatedOpacity, AnimatedAlign & AnimatedSwitcher
Implicit Animation Widgets: AnimatedOpacity, AnimatedAlign & AnimatedSwitcher
Flutter ships a family of implicit animation widgets that handle all animation logic internally. You simply set a new value, specify a duration and optional curve, and Flutter interpolates smoothly between the old and new values. In this lesson we focus on three powerful members of that family: AnimatedOpacity, AnimatedAlign, and AnimatedSwitcher.
AnimatedOpacity
AnimatedOpacity wraps any widget and animates its transparency between 0.0 (fully invisible) and 1.0 (fully opaque). It is the idiomatic way to fade widgets in and out without writing a single line of animation controller code.
- opacity — the target opacity value; triggers the animation whenever it changes.
- duration — how long the transition takes (e.g.
Duration(milliseconds: 500)). - curve — easing curve; defaults to
Curves.linear. - onEnd — optional callback fired when the animation completes.
opacity: 0.0) still occupies space in the layout. Use Visibility or conditional rendering if you also need to remove the widget from the layout.AnimatedOpacity — Fade In/Out on Button Tap
class FadeDemo extends StatefulWidget {
const FadeDemo({super.key});
@override
State<FadeDemo> createState() => _FadeDemoState();
}
class _FadeDemoState extends State<FadeDemo> {
double _opacity = 1.0;
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedOpacity(
opacity: _opacity,
duration: const Duration(milliseconds: 600),
curve: Curves.easeInOut,
child: Container(
width: 200,
height: 200,
color: Colors.deepPurple,
child: const Center(
child: Text(
'Flutter',
style: TextStyle(color: Colors.white, fontSize: 28),
),
),
),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () {
setState(() {
_opacity = _opacity == 1.0 ? 0.0 : 1.0;
});
},
child: const Text('Toggle Visibility'),
),
],
);
}
}
AnimatedAlign
AnimatedAlign animates its child’s position within the parent by interpolating an Alignment value. Common use-cases include sliding a panel in from an edge, highlighting an active tab indicator, or animating a floating action button to a new corner.
- alignment — target
Alignment; changing it starts the animation. - duration & curve — same semantics as all implicit widgets.
- widthFactor / heightFactor — optional factors to size the widget relative to its child.
AnimatedOpacity and AnimatedAlign around the same child to create a “slide-and-fade” entrance effect with zero boilerplate.AnimatedAlign — Slide Between Corners
class AlignDemo extends StatefulWidget {
const AlignDemo({super.key});
@override
State<AlignDemo> createState() => _AlignDemoState();
}
class _AlignDemoState extends State<AlignDemo> {
Alignment _alignment = Alignment.topLeft;
static const List<Alignment> _positions = [
Alignment.topLeft,
Alignment.topRight,
Alignment.bottomRight,
Alignment.bottomLeft,
];
int _index = 0;
void _next() {
setState(() {
_index = (_index + 1) % _positions.length;
_alignment = _positions[_index];
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
SizedBox(
width: 300,
height: 300,
child: AnimatedAlign(
alignment: _alignment,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOutCubic,
child: Container(
width: 60,
height: 60,
decoration: const BoxDecoration(
color: Colors.teal,
shape: BoxShape.circle,
),
),
),
),
ElevatedButton(
onPressed: _next,
child: const Text('Move'),
),
],
);
}
}
AnimatedSwitcher
AnimatedSwitcher cross-fades (or applies any custom transition) between two different child widgets. Whenever the child property is set to a widget with a different key, the old child plays an exit animation while the new child plays an entrance animation simultaneously.
- duration — transition duration for the incoming child.
- reverseDuration — optional separate duration for the outgoing child.
- transitionBuilder — factory that wraps the child in a custom animated widget (default:
FadeTransition). - layoutBuilder — controls how old and new children are stacked during the transition.
Key, Flutter considers them the same widget and does not trigger the transition. Always assign a ValueKey that changes with the content.AnimatedSwitcher — Cross-Fade Between a Counter and an Icon
class SwitcherDemo extends StatefulWidget {
const SwitcherDemo({super.key});
@override
State<SwitcherDemo> createState() => _SwitcherDemoState();
}
class _SwitcherDemoState extends State<SwitcherDemo> {
int _count = 0;
bool _showIcon = false;
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 400),
transitionBuilder: (child, animation) {
return FadeTransition(
opacity: animation,
child: ScaleTransition(scale: animation, child: child),
);
},
child: _showIcon
? const Icon(
Icons.check_circle,
key: ValueKey('icon'),
size: 80,
color: Colors.green,
)
: Text(
'$_count',
key: ValueKey(_count),
style: const TextStyle(fontSize: 72, fontWeight: FontWeight.bold),
),
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () => setState(() => _count++),
child: const Text('Increment'),
),
const SizedBox(width: 12),
ElevatedButton(
onPressed: () => setState(() => _showIcon = !_showIcon),
child: const Text('Toggle View'),
),
],
),
],
);
}
}
Choosing the Right Widget
- Use
AnimatedOpacitywhen you want to fade a widget without moving it. - Use
AnimatedAlignwhen you want to slide a widget to a new position within its parent. - Use
AnimatedSwitcherwhen you want to swap one widget for another with a transition effect.
Summary
All three widgets follow the same implicit animation contract: set a new property value inside setState(), provide a duration, and Flutter does the rest. AnimatedOpacity controls transparency, AnimatedAlign controls alignment-based position, and AnimatedSwitcher transitions between entirely different child widgets. Mastering these three covers the vast majority of everyday UI motion needs without reaching for low-level AnimationController APIs.