We've explored the building blocks of animations in Flutter: tweens, curves, and controllers. Now, let's synthesize these concepts into a more realistic scenario. Imagine an app that displays a list of items, and when a user taps an item, it expands to reveal more details, fading in and scaling up simultaneously. This involves coordinating multiple animation properties and managing their lifecycle.
To achieve this, we'll need a StatefulWidget. This allows us to manage the animation state, including the AnimationController and the AnimatedWidget (or AnimatedBuilder) that will use the animation values.
class ExpandableListItem extends StatefulWidget {
const ExpandableListItem({Key? key}) : super(key: key);
@override
_ExpandableListItemState createState() => _ExpandableListItemState();
}
class _ExpandableListItemState extends State<ExpandableListItem> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
late Animation<double> _opacityAnimation;
bool _isExpanded = false;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 300));
_scaleAnimation = Tween<double>(begin: 0.8, end: 1.0).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut)
);
_opacityAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeIn)
);
_controller.addListener(() {
setState(() {});
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _toggleExpand() {
setState(() {
_isExpanded = !_isExpanded;
if (_isExpanded) {
_controller.forward();
} else {
_controller.reverse();
}
});
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _toggleExpand,
child: ScaleTransition(
scale: _scaleAnimation,
child: Opacity(
opacity: _opacityAnimation.value,
child: Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Item Title', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
if (_isExpanded) ...[
SizedBox(height: 10),
Text('Detailed information about this item goes here.'),
]
],In this example, we're using SingleTickerProviderStateMixin for our AnimationController. We define two animations: one for scaling and another for opacity. When the user taps the GestureDetector, we toggle the _isExpanded state and then either forward or reverse the _controller. The _controller.addListener ensures that setState is called whenever the animation value changes, triggering a rebuild of the widget.
We then use ScaleTransition to apply the _scaleAnimation and Opacity widget to apply the _opacityAnimation. Notice how the detailed information is conditionally rendered based on the _isExpanded state. This is a common pattern to avoid animating widgets that aren't visible.
graph TD; A[User Taps Item] --> B{Toggle _isExpanded}; B --> C{If _isExpanded}; C -- Yes --> D[_controller.forward()]; C -- No --> E[_controller.reverse()]; D --> F[Animation Listener called]; E --> F; F --> G[setState()]; G --> H[UI Rebuild with new animation values]; H --> I[ScaleTransition & Opacity update];
This scenario demonstrates how to combine multiple animation principles to create a fluid and engaging user experience. The key is to break down the desired visual effect into individual animation properties and then orchestrate them using AnimationController and appropriate animation widgets. As you progress, you'll discover more advanced techniques and widgets that can further simplify and enhance your animation implementations.