I would like to change the child of some widget, and then see it animate to the new child's height, also with a fade transition.
I can do that with AnimatedCrossFade
, but then I must keep both firstChild
and secondChild
, which I cannot.
If I use AnimatedSwitcher
, it lets me simply change its child, but then it only animates the fade, not the size.
The AnimatedContainer
also doesn't work, since I don't know the size of the children in advance.
Is there some widget I am missing that does what I need, and if not, how can I do that without resorting to AnimationController
s?
This solves the question. It fades and animates the size at the same time, without having to specify two children:
import 'package:flutter/material.dart';
/// A widget that does a fade and size transition between a "new" widget and the "old "widget
/// previously set as a child.
///
/// The "old" and the "new" child must have the same width, but can have different heights.
///
/// If the "new" child is the same widget type as the "old" child, but with different parameters,
/// then [AnimatedSizeAndFade] will not do a transition between them, since as far as the framework
/// is concerned, they are the same widget, and the existing widget can be updated with the new
/// parameters. To force the transition to occur, set a [Key] (typically a [ValueKey] taking any
/// widget data that would change the visual appearance of the widget) on each child widget that
/// you wish to be considered unique.
class AnimatedSizeAndFade extends StatelessWidget {
final Widget child;
final TickerProvider vsync;
final Duration fadeDuration;
final Duration sizeDuration;
final Curve fadeCurve;
final Curve sizeCurve;
const AnimatedSizeAndFade({
this.child,
this.vsync,
this.fadeDuration = const Duration(milliseconds: 500),
this.sizeDuration = const Duration(milliseconds: 500),
this.fadeCurve = Curves.easeInOut,
this.sizeCurve = Curves.easeInOut,
});
@override
Widget build(BuildContext context) {
return ClipPath(
child: AnimatedSize(
vsync: vsync,
child: AnimatedSwitcher(
child: child,
duration: fadeDuration,
layoutBuilder: _layoutBuilder,
),
duration: sizeDuration,
curve: Curves.easeInOut,
),
);
}
Widget _layoutBuilder(Widget currentChild, List<Widget> previousChildren) {
List<Widget> children = previousChildren;
if (currentChild != null) {
if (previousChildren == null || previousChildren.isEmpty)
children = [currentChild];
else {
children = [
Positioned(
left: 0.0,
right: 0.0,
child: Container(
child: previousChildren[0],
),
),
Container(
child: currentChild,
),
];
}
}
return new Stack(
overflow: Overflow.visible,
children: children,
alignment: Alignment.center,
);
}
}
Use it like this:
bool toggle=true;
Widget widget1 = ...;
Widget widget2 = ...;
AnimatedSizeAndFade(
vsync: this,
child: toggle ? widget1 : widget2,
),
Note you can also define a duration and curve for both the fade and the size, separately.
Note: If you want to use the above code, please read the documentation:
The "old" and the "new" child must have the same width, but can have different heights.
If the "new" child is the same widget type as the "old" child, but with different parameters, then AnimatedSizeAndFade
will not do a transition between them, since as far as the framework is concerned, they are the same widget, and the existing widget can be updated with the new parameters. To force the transition to occur, set a Key
(typically a ValueKey
taking any widget data that would change the visual appearance of the widget on each child widget that you wish to be considered unique.
This is a runnable example:
import 'package:flutter/material.dart';
import 'package:widgets/widgets.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with TickerProviderStateMixin {
bool toggle;
@override
void initState() {
super.initState();
toggle = false;
}
@override
Widget build(BuildContext context) {
var toggleButton = Padding(
padding: const EdgeInsets.all(8.0),
child: MaterialButton(
child: const Text("Toggle"),
color: Colors.grey,
onPressed: () {
setState(() {
toggle = !toggle;
});
},
),
);
var widget1 = Container(
key: ValueKey("first"),
color: Colors.blue,
width: 200.0,
child: const Text(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt "
"ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation "
"ullamco laboris nisi ut aliquip ex ea commodo consequat.",
),
);
var widget2 = Container(
key: ValueKey("second"),
color: Colors.red,
width: 200.0,
child: const Text(
"I am ready for my closeup.",
),
);
return MaterialApp(
home: Material(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(height: 100.0),
toggleButton,
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text("Some text above."),
AnimatedSizeAndFade(
vsync: this,
child: toggle ? widget1 : widget2,
fadeDuration: const Duration(milliseconds: 300),
sizeDuration: const Duration(milliseconds: 600),
),
const Text("Some text below."),
],
),
],
),
),
);
}
}
There are many ways to achieve that. This is just an example:
class LogoApp extends StatefulWidget {
_LogoAppState createState() => _LogoAppState();
}
class _LogoAppState extends State<LogoApp> with TickerProviderStateMixin {
Animation animation;
Animation animationOpacity;
AnimationController controller;
initState() {
super.initState();
controller = AnimationController(
duration: const Duration(milliseconds: 2000), vsync: this);
final CurvedAnimation curve =
CurvedAnimation(parent: controller, curve: Curves.easeIn);
animation = Tween(begin: 0.0, end: 300.0).animate(curve);
animationOpacity = Tween(begin: 0.0, end: 1.0).animate(curve);
controller.forward();
}
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: controller,
builder: (context, widget) {
return Opacity(
opacity: animationOpacity.value,
child: Container(
margin: EdgeInsets.symmetric(vertical: 10.0),
height: animation.value,
width: animation.value,
child: FlutterLogo(),
),
);
},
);
}
dispose() {
controller.dispose();
super.dispose();
}
}
Usage:
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: Material(child: Center(child: LogoApp())));
}
Refer to this documentation Flutter Animations
Updated
class LogoApp extends StatefulWidget {
_LogoAppState createState() => _LogoAppState();
}
class _LogoAppState extends State<LogoApp> with TickerProviderStateMixin {
Animation controllerAnimation;
AnimationController controller;
initState() {
super.initState();
controller = AnimationController(
duration: const Duration(milliseconds: 1000), vsync: this);
final CurvedAnimation curve =
CurvedAnimation(parent: controller, curve: Curves.easeIn);
controllerAnimation = Tween(begin: 0.0, end: 1.0).animate(curve);
controller.forward();
}
bool isSelected = false;
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
AnimatedSwitcher(
duration: Duration(seconds: 10),//it is ignored
child: isSelected
? Container(
width: 200.0,
height: 200.0,
child: FlutterLogo(
colors: Colors.red,
),
)
: Container(
width: 200.0,
height: 200.0,
child: FlutterLogo(
colors: Colors.blue,
)),
transitionBuilder: defaultTransitionBuilder,
),
MaterialButton(
child: Text("Texting"),
onPressed: () {
if (controller.isCompleted) {
controller.reset();
}
controller.forward();
setState(() {
isSelected = !isSelected;
});
},
)
],
);
}
Widget defaultTransitionBuilder(Widget child, Animation<double> animation) {
return AnimatedBuilder(
animation: controller,
builder: (context, widget) {
return Opacity(
opacity: controllerAnimation.value,
child: ScaleTransition(
scale: controllerAnimation,
child: widget,
),
);
},
child: child,
);
}
dispose() {
controller.dispose();
super.dispose();
}
}