Master Flutter Animations: From Basics to Advanced Transitions
This article explains Flutter's animation system, covering core concepts like Animation, AnimationController, CurvedAnimation, Tween, and listeners, then demonstrates basic and combined animations, introduces system widgets such as AnimatedContainer and AnimatedCrossFade, and shows how to create custom page transitions with PageRouteBuilder and Hero.
Flutter Animation Basics
Flutter animations are built around the abstract Animation class, which stores the current value and state of an animation. An Animation<double> typically generates values over a range during a given duration, while UI widgets read these values and rebuild via setState to render the animation.
AnimationController
AnimationControllerextends Animation<double> and provides methods to start, pause, and stop an animation. It generates 60 values per second by default, each triggering any attached listeners. The required vsync parameter ties the animation ticker to a visible widget, pausing the timer when the widget is off‑screen and resuming when it returns.
Controlling Animations
forward()– play forward. reverse() – play backward. stop() – halt the animation.
Code Example
AnimationController controller = AnimationController(
duration: const Duration(seconds: 5),
vsync: this);CurvedAnimation
CurvedAnimationwraps an AnimationController with a non‑linear curve (e.g., Curves.linear, Curves.easeIn).
CurvedAnimation curvedAnimation = CurvedAnimation(
parent: controller,
curve: Curves.linear);Tween
Tween<T>maps a range of values to an animation. Example mapping 50→150:
Tween tweenAnim = Tween(begin: 50.0, end: 150.0)
.animate(curvedAnimation);Listeners and StatusListeners
addListener()– called whenever the animation value changes; typically triggers setState. addStatusListener() – notifies when the animation state changes (forward, reverse, completed, dismissed).
Basic Animation Application
The following widget animates width, opacity, and font size based on a Tween value:
class LookPage extends StatefulWidget { ... }
class _CJAnimationWidgetState extends State<LookPage> with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation _curvedAnimation;
Animation _tweenAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 1000),
vsync: this);
_curvedAnimation = CurvedAnimation(parent: _controller, curve: Curves.linear);
_tweenAnimation = Tween(begin: 1.0, end: 2.0).animate(_curvedAnimation);
_controller.addListener(() { setState(() {}); });
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) _controller.reverse();
else if (status == AnimationStatus.dismissed) _controller.forward();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("动画")),
body: Container(
child: Column(
children: [
SizedBox(height: 200),
Container(
color: Colors.blueAccent,
width: 100 * _tweenAnimation.value,
height: 60),
SizedBox(height: 20),
Opacity(
opacity: 2.0 - _tweenAnimation.value,
child: Container(color: Colors.blueAccent, width: 100, height: 100)),
SizedBox(height: 20),
Text("窗外风好大",
style: TextStyle(fontSize: 20 * _tweenAnimation.value)),
],
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.done_outline),
onPressed: () { _controller.forward(); },
),
);
}
@override
void dispose() { _controller.dispose(); super.dispose(); }
}Using setState for every value change leads to two drawbacks: duplicated code and unnecessary rebuilds that waste performance.
AnimatedWidget
Replacing manual listeners with AnimatedWidget separates animation logic from UI, removes the need for a dedicated State object, and automatically calls addListener and setState.
class CJAnimatedWidget extends AnimatedWidget {
final Animation tweenAnimation;
CJAnimatedWidget(this.tweenAnimation) : super(listenable: tweenAnimation);
@override
Widget build(BuildContext context) {
return Container(
child: Column(
children: [
// same UI as before, using tweenAnimation.value
],
),
);
}
}Flutter provides many ready‑made subclasses such as FadeTransition, ScaleTransition, RotationTransition, etc.
AnimatedBuilder
AnimatedBuilderfurther reduces boilerplate by letting a builder function rebuild only the parts that depend on the animation, keeping the rest of the widget tree untouched.
body: Container(
child: AnimatedBuilder(
animation: _tweenAnimation,
builder: (ctx, child) {
return Column(
children: [
// UI using _tweenAnimation.value
],
);
},
),
);Combined Animations
Complex scenes can be built by animating multiple properties simultaneously, e.g., moving and rotating sunglasses while a necklace changes position and opacity.
class LookPage extends StatefulWidget { ... }
class _CJAnimationWidgetState extends State<LookPage> with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation _curvedAnimation;
Animation _glassLocationAnim, _glassRotationAnim, _necklaceLocationAnim, _necklaceOpacityAnim;
@override
void initState() {
super.initState();
_controller = AnimationController(duration: Duration(milliseconds: 2000), vsync: this);
_curvedAnimation = CurvedAnimation(parent: _controller, curve: Curves.linear);
_glassLocationAnim = Tween(begin: 0.0, end: 252.0).animate(_curvedAnimation);
_glassRotationAnim = Tween(begin: 0.0, end: 2.1 * pi).animate(_curvedAnimation);
_necklaceLocationAnim = Tween(begin: 500.0, end: 370.0).animate(_curvedAnimation);
_necklaceOpacityAnim = Tween(begin: 0.0, end: 1.0).animate(_curvedAnimation);
}
// build method uses Stack with Positioned widgets that read the above animations
}System Animation Widgets
Flutter offers a suite of built‑in animated widgets that handle common transitions without manual controllers:
AnimatedContainer – animates size, color, border, etc., with just duration and curve.
AnimatedCrossFade – cross‑fades between two children.
AnimatedIcon – plays built‑in icon animations using an AnimationController.
AnimatedAlign, AnimatedOpacity, AnimatedPositioned, AnimatedSize, and many others.
AnimatedContainer Example
class LookPage extends StatefulWidget { ... }
class _CJAnimationWidgetState extends State<LookPage> {
bool _click = false;
@override
Widget build(BuildContext context) {
return Center(
child: GestureDetector(
onTap: () { setState(() { _click = !_click; }); },
child: AnimatedContainer(
height: _click ? 200 : 100,
width: _click ? 200 : 100,
duration: Duration(milliseconds: 2000),
curve: Curves.easeInOutCirc,
transform: Matrix4.rotationX(_click ? pi : 0),
decoration: BoxDecoration(
image: DecorationImage(image: AssetImage("assets/images/girl.jpg"), fit: BoxFit.cover),
borderRadius: BorderRadius.all(Radius.circular(_click ? 200 : 100)),
),
onEnd: () { setState(() { _click = !_click; }); },
),
),
);
}
}AnimatedCrossFade Example
AnimatedCrossFade(
duration: Duration(seconds: 2),
crossFadeState: _click ? CrossFadeState.showSecond : CrossFadeState.showFirst,
firstChild: Container(...),
secondChild: Container(...),
);AnimatedIcon Example
class LookPage extends StatefulWidget { ... }
class _CJAnimationWidgetState extends State<LookPage> with SingleTickerProviderStateMixin {
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(duration: Duration(milliseconds: 2000), vsync: this);
}
Widget createAnimatedIcon(AnimatedIconData data) {
return Container(
width: 138,
height: 138,
child: Center(
child: AnimatedIcon(icon: data, progress: _controller),
),
);
}
@override
Widget build(BuildContext context) {
return GridView(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3),
children: [
createAnimatedIcon(AnimatedIcons.add_event),
// ... other icons
],
);
}
}Page Transition Animations
PageRouteBuilder
Custom page transitions are built with PageRouteBuilder, providing a pageBuilder for the destination widget and a transitionsBuilder for the animation.
Navigator.of(context).push(PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => CJNextPage("assets/images/cj2.png"),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return CJRotationTransition(
turns: Tween(begin: 1.0, end: 0.0).animate(animation),
child: child,
);
},
));Custom CJRotationTransition extends AnimatedWidget to rotate around the Y‑axis.
class CJRotationTransition extends AnimatedWidget {
const CJRotationTransition({Key key, @required Animation turns, this.alignment = Alignment.center, this.child})
: assert(turns != null), super(key: key, listenable: turns);
Animation get turns => listenable;
final Alignment alignment;
final Widget child;
@override
Widget build(BuildContext context) {
final double turnsValue = turns.value;
final Matrix4 transform = Matrix4.rotationY(turnsValue * pi / 2.0);
return Transform(transform: transform, alignment: alignment, child: child);
}
}Hero
The Hero widget creates a shared‑element transition between two routes. Both source and destination must use the same tag.
GestureDetector(
onTap: () {
Navigator.of(context).push(PageRouteBuilder(
pageBuilder: (c, a, s) => CJHeroPage(imageURL, "tag-$index"),
transitionsBuilder: (c, a, s, child) => FadeTransition(opacity: a, child: child),
));
},
child: Hero(tag: "tag-$index", child: Image.asset(imageURL, width: 125, height: 125)),
);Conclusion
The article covered Flutter animation fundamentals, demonstrated how to replace manual listeners with higher‑level widgets, showed ways to combine multiple animations, introduced a variety of built‑in animated widgets, and explained custom page‑transition techniques such as PageRouteBuilder and Hero. With these tools, developers can create smooth, performant, and expressive UI animations in Flutter.
Beike Product & Technology
As Beike's official product and technology account, we are committed to building a platform for sharing Beike's product and technology insights, targeting internet/O2O developers and product professionals. We share high-quality original articles, tech salon events, and recruitment information weekly. Welcome to follow us.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
