How to animate the swap of 2 items in a Row? - flutter

I want to make something very simple. There's a Row with 2 widgets. When I press a button, they swap orders. I want this order swap to be animated.
I've loked at AnimatedPositioned but it requires a Stack. What would be the best way of doing such thing?
I thought Animating position across row cells in Flutter answered this but it's another different problem

You can easily animate widgets in a Row with SlideAnimation. Please see the code below or you may directly run the code on DartPad https://dartpad.dev/e5d9d2c9c6da54b3f76361eac449ce42 Just tap on the colored box to swap their positions with an slide animation.
SlideAnimation
Animates the position of a widget relative to its normal position.
The translation is expressed as an Offset scaled to the child's size.
For example, an Offset with a dx of 0.25 will result in a horizontal
translation of one quarter the width of the child.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage>
with SingleTickerProviderStateMixin {
AnimationController _controller;
List<Animation<Offset>> _offsetAnimation;
#override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 1),
vsync: this,
);
_offsetAnimation = List.generate(
2,
(index) => Tween<Offset>(
begin: const Offset(0.0, 0.0),
end: Offset(index == 0 ? 1 : -1, 0.0),
).animate(_controller),
);
}
#override
void dispose() {
super.dispose();
_controller.dispose();
}
void _animate() {
_controller.status == AnimationStatus.completed
? _controller.reverse()
: _controller.forward();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Flutter Demo Row Animation")),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
BoxWidget(
callBack: _animate,
text: "1",
color: Colors.red,
position: _offsetAnimation[0],
),
BoxWidget(
callBack: _animate,
text: "2",
color: Colors.blue,
position: _offsetAnimation[1],
)
],
),
RaisedButton(
onPressed: _animate,
child: const Text("Swap"),
)
],
),
),
);
}
}
class BoxWidget extends StatelessWidget {
final Animation<Offset> position;
final Function callBack;
final String text;
final Color color;
const BoxWidget(
{Key key, this.position, this.callBack, this.text, this.color})
: super(key: key);
#override
Widget build(BuildContext context) {
return SlideTransition(
position: position,
child: GestureDetector(
onTap: () => callBack(),
child: Container(
margin: const EdgeInsets.all(10),
height: 50,
width: 50,
color: color,
child: Center(
child: Container(
height: 20,
width: 20,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Colors.white,
),
child: Center(child: Text(text)),
),
),
),
),
);
}
}

Related

Persistent Navigation Bar only in some pages

Stackoverflowers!
I'm using an BottomAppBar inside the bottomNavigationBar section of the Scaffold. The problem is that it doesn't persists while I'm navigating. I used the persistent_bottom_nav_bar plugin, but it doesn't work with my custom navigation bar because it has a ripple animation in one button and a bottomSheet that is over the keyboard.
home_page.dart
This file has the CustomNavigationBar and the main pages for each item on it.
class HomePage extends StatefulWidget {
const HomePage({super.key});
#override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with TickerProviderStateMixin {
NavigationProvider? navigationProvider;
AnimationController? rippleController;
AnimationController? scaleController;
Animation<double>? rippleAnimation;
Animation<double>? scaleAnimation;
#override
void initState() {
super.initState();
rippleController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 500));
scaleController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 500))
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
scaleController!.reverse();
Navigator.push(
context,
PageTransition(
type: PageTransitionType.bottomToTop,
child: pages.elementAt(2),
childCurrent: widget,
fullscreenDialog: true,
)).whenComplete(() => setState(() {
buttonColor = Colors.black;
}));
}
});
rippleAnimation =
Tween<double>(begin: 80.0, end: 90.0).animate(rippleController!)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
rippleController!.reverse();
} else if (status == AnimationStatus.dismissed) {
rippleController!.forward();
}
});
scaleAnimation =
Tween<double>(begin: 1.0, end: 30.0).animate(scaleController!);
rippleController!.forward();
}
#override
void dispose() {
rippleController!.dispose();
scaleController!.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
navigationProvider = Provider.of<NavigationProvider>(context);
return Scaffold(
body:
pages.elementAt(navigationProvider!.bottomNavigationBarSelectedIndex),
bottomNavigationBar: CustomNavigationBar(
rippleController: rippleController,
scaleController: scaleController,
rippleAnimation: rippleAnimation,
scaleAnimation: scaleAnimation),
);
}
}
custom_navigation_bar.dart
This file contains the properties of the CustomNavigationBar.
class CustomNavigationBar extends StatefulWidget {
const CustomNavigationBar({
super.key,
this.rippleController,
this.scaleController,
this.rippleAnimation,
this.scaleAnimation,
});
final AnimationController? rippleController;
final AnimationController? scaleController;
final Animation<double>? rippleAnimation;
final Animation<double>? scaleAnimation;
#override
State<CustomNavigationBar> createState() => _CustomNavigationBarState();
}
class _CustomNavigationBarState extends State<CustomNavigationBar> {
#override
Widget build(BuildContext context) {
final navigationProvider = Provider.of<NavigationProvider>(context);
return BottomAppBar(
child: IconTheme(
data: const IconThemeData(color: Colors.black, size: 36),
child: Padding(
padding: const EdgeInsets.fromLTRB(10, 5, 10, 5),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
direction: Axis.vertical,
children: [
IconButton(
icon: ...,
padding: ...,
constraints: ...,
onPressed: () {
//Here I change the selected index with Provider.
...
},
),
Text(
title,
style: ...,
),
],
),
const Spacer(),
Wrap(...),
const Spacer(),
InkWell(
onTap: () {
setState(
() {
//Executes the ripple animation.
widget.scaleController!.forward();
},
);
},
child: AnimatedBuilder(
animation: widget.scaleAnimation!,
builder: (context, child) => Transform.scale(
scale: widget.scaleAnimation!.value,
child: Container(
width: 50,
height: 50,
margin: const EdgeInsets.all(10),
decoration: const BoxDecoration(
shape: BoxShape.circle, color: Colors.blue),
child: Icon(Icons.add,
color: widget.scaleAnimation!.value == 1.0
? Colors.white
: Colors.blue),
),
),
),
),
const Spacer(),
Wrap(...),
const Spacer(),
Wrap(...),
],
),
),
),
);
}
}
As you can see, I use Provider to manage the state of the CustomNavigationBar when it changes the index.
Example of what I want:
This app is Splitwise and it has some pages with the navigation bar and others without it. That ripple animation is similar to mine. Also the bottom sheet has the same effect in my app.
I'll wait for all your suggestions, thanks!

How to have the drawer push the content instead of going on top of it?

I'm trying to build something similar to the slack app (see screenshot below) where the navigation drawer pushes the screen away instead of going on top.
I've been trying with the Drawer component without success. I've also looked at PageView but it seems that the children need to take 100% of the width.
Does someone have an idea of how to implement it?
EDIT
A similar result can be achieved with a Stack and AnimatedPositioned
class SlidingDrawer extends StatefulWidget {
final Widget drawer;
final Widget child;
final int swipeSensitivity;
final double drawerRatio;
final Color overlayColor;
final double overlayOpacity;
final int animationDuration;
final Curve animationCurve;
SlidingDrawer({
Key key,
#required this.drawer,
#required this.child,
this.swipeSensitivity = 25,
this.drawerRatio = 0.8,
this.overlayColor = Colors.black,
this.overlayOpacity = 0.5,
this.animationDuration = 500,
this.animationCurve = Curves.ease,
}) : super(key: key);
#override
_SlidingDrawerState createState() => _SlidingDrawerState();
}
class _SlidingDrawerState extends State<SlidingDrawer> {
bool _opened = false;
void open() {
setState(() {
_opened = true;
});
}
void close() {
setState(() {
_opened = false;
});
}
#override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
final height = MediaQuery.of(context).size.height;
final drawerWidth = width * widget.drawerRatio;
return GestureDetector(
onHorizontalDragUpdate: (details) {
if (details.delta.dx > widget.swipeSensitivity) {
open();
} else if (details.delta.dx < -widget.swipeSensitivity) {
close();
}
},
child: SizedBox(
width: width,
height: height,
child: Stack(
children: [
AnimatedPositioned(
width: drawerWidth,
height: height,
left: _opened ? 0 : -drawerWidth,
duration: Duration(milliseconds: widget.animationDuration),
curve: widget.animationCurve,
child: Container(
color: Colors.amber,
child: widget.drawer,
),
),
AnimatedPositioned(
height: height,
width: width,
left: _opened ? drawerWidth : 0,
duration: Duration(milliseconds: widget.animationDuration),
curve: widget.animationCurve,
child: Stack(
fit: StackFit.expand,
children: [
widget.child,
AnimatedSwitcher(
duration: Duration(milliseconds: widget.animationDuration),
switchInCurve: widget.animationCurve,
switchOutCurve: widget.animationCurve,
child: _opened
? GestureDetector(
onTap: () {
setState(() {
_opened = false;
});
},
child: Container(
color: widget.overlayColor.withOpacity(
widget.overlayOpacity,
),
),
)
: null,
)
],
),
),
],
),
),
);
}
}
ORIGINAL ANSWER
As pointed out by #Yadu in the comment
you could use Single child horizontal scroll view (with disabled scroll physics) with Scrollable.ensureVisible(context) to show the menu
using an horizontal scroll view is working.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool _drawerOpened = false;
final drawerKey = new GlobalKey();
final mainKey = new GlobalKey();
#override
Widget build(BuildContext context) {
return SingleChildScrollView(
physics: NeverScrollableScrollPhysics(),
scrollDirection: Axis.horizontal,
child: Row(
children: [
Container(
key: drawerKey,
color: Colors.green,
width: MediaQuery.of(context).size.width * 0.8,
),
SizedBox(
key: mainKey,
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: Scaffold(
appBar: AppBar(
title: Text("My Page"),
leading: IconButton(
icon: Icon(Icons.menu),
onPressed: _toggleDrawer,
),
),
body: Container(
color: Colors.yellow,
width: MediaQuery.of(context).size.width,
),
),
),
],
),
);
}
void _toggleDrawer() {
setState(() {
_drawerOpened = !_drawerOpened;
});
if (_drawerOpened) {
Scrollable.ensureVisible(drawerKey.currentContext);
} else {
Scrollable.ensureVisible(mainKey.currentContext);
}
}
}

Flutter: Bouncing button animation scroll issue

I want to add some smooth & fluid animations in my Flutter app, especially on buttons like these on the Reflectly app (made on Flutter too).
So I followed this tutorial to add Bouncing Animations on my buttons. Everything is working fine but I noticed a "bug" related to the scroll:
When I touch a button AND keep pressing it, then drag to scroll down or up (always by keeping down), the button keep the down state and do not return to it's original position (see the GIF for more details).
Note: This issue doesn't appear on the Reflecty app.
To reproduce, this "bug", you can download the Bouncing button project here and then create a scroll with some bouncing buttons inside it like this:
return Scaffold(
body: Container(
width: double.infinity,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
AnimatedButton(),
// [...] Many others animated button here
],
),
),
),
);
I tried to add a onVerticalDragEnd callback to restore the state after but it's worse because it is no longer possible to scroll (scroll seems to be catches only on buttons) !
Any help would be greatly appreciated :)
Thanks in advance !
You can copy paste run full code below
When onTapDown and scroll will trigger onTapCancel
You can put _controller.reverse(); in _onTapCancel()
code snippet
void _onTapCancel() {
print("on tap cancel");
_controller.reverse();
}
working demo
full code
import 'package:flutter/material.dart';
class AnimatedButton extends StatefulWidget {
#override
_AnimatedButtonState createState() => _AnimatedButtonState();
}
class _AnimatedButtonState extends State<AnimatedButton>
with SingleTickerProviderStateMixin {
double _scale;
AnimationController _controller;
#override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(milliseconds: 200),
lowerBound: 0.0,
upperBound: 0.1,
)..addListener(() {
setState(() {});
});
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
void _onTapDown(TapDownDetails details) {
_controller.forward();
}
void _onTapUp(TapUpDetails details) {
print("onTapUp");
_controller.reverse();
}
void _onTapCancel() {
print("on tap cancel");
_controller.reverse();
}
#override
Widget build(BuildContext context) {
_scale = 1 - _controller.value;
return GestureDetector(
onTapDown: _onTapDown,
onTapUp: _onTapUp,
onTapCancel: _onTapCancel,
child: Transform.scale(
scale: _scale,
child: _animatedButtonUI,
),
);
}
Widget get _animatedButtonUI => Container(
height: 100,
width: 250,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(100),
boxShadow: [
BoxShadow(
color: Color(0x80000000),
blurRadius: 30.0,
offset: Offset(0.0, 30.0),
),
],
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color(0xFFA7BFE8),
Color(0xFF6190E8),
],
),
),
child: Center(
child: Text(
'tap!',
style: TextStyle(
fontSize: 30,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
);
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
width: double.infinity,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
AnimatedButton(),
AnimatedButton(),
AnimatedButton(),
AnimatedButton(),
AnimatedButton(),
AnimatedButton(),
AnimatedButton(),
AnimatedButton(),
AnimatedButton(),
AnimatedButton(),
AnimatedButton(),
AnimatedButton(),
AnimatedButton(),
AnimatedButton(),
AnimatedButton(),
AnimatedButton(),
AnimatedButton(),
AnimatedButton(),
AnimatedButton(),
AnimatedButton(),
],
),
),
),
);
}
}

Flutter - How to add a label that follows the progress position in a LinearProgressIndicator

The title is pretty self explanatory I think.
Basically I need to have a LinearProgressIndicator with a label in the same position as the current progress. Like this:
I suppose I need to use a Stack to create the Text, but how can I position it based on the progress of the bar?
You can use Align widget to align the text in the stack. Use alignment property as Alignment.lerp(Alignment.topLeft, Alignment.topRight, _progressValue);
The progress value should be from 0 to 1
https://dartpad.dev/bbc452ca5e8370bf2fbf48d34d82eb93
import 'package:flutter/material.dart';
void main() {
runApp(new MaterialApp(
debugShowCheckedModeBanner: false,
home: new MyApp(),
));
}
class MyApp extends StatefulWidget {
#override
MyAppState createState() => new MyAppState();
}
class MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Slider Demo'),
),
body: new Container(
color: Colors.blueAccent,
padding: new EdgeInsets.all(32.0),
child: new ProgressIndicatorDemo(),
),
);
}
}
class ProgressIndicatorDemo extends StatefulWidget {
#override
_ProgressIndicatorDemoState createState() =>
new _ProgressIndicatorDemoState();
}
class _ProgressIndicatorDemoState extends State<ProgressIndicatorDemo>
with SingleTickerProviderStateMixin {
AnimationController controller;
Animation<double> animation;
#override
void initState() {
super.initState();
controller = AnimationController(
duration: const Duration(milliseconds: 2000), vsync: this);
animation = Tween(begin: 0.0, end: 1.0).animate(controller)
..addListener(() {
setState(() {
// the state that has changed here is the animation object’s value
});
});
controller.repeat();
}
#override
void dispose() {
controller.stop();
super.dispose();
}
#override
Widget build(BuildContext context) {
print(animation.value);
return new Center(
child: new Stack(children: <Widget>[
LinearProgressIndicator(
value: animation.value,
),
Align(
alignment :Alignment.lerp(Alignment.topLeft, Alignment.topRight, animation.value),
child: Text("xxxxxxxxxxxxxxxxa"),
),
]));
}
}
Column(children: [
LinearProgressIndicator(
value: value,
backgroundColor: Colors.grey,
color: Colors.blue,
minHeight: 20,
),
Align(
alignment:
AlignmentGeometry.lerp(const Alignment(-1.04, -1), const Alignment(1.04, -1), value)
as AlignmentGeometry,
child: Text(
'${minutes}:${seconds}',
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.blue, fontSize: 12),
)),
]);

how to animate collapse elements in flutter

How can i expand and collapse widget when user taps on different widget ( sibling or parent ) with animation ?
new Column(
children: <Widget>[
new header.IngridientHeader(
new Icon(
Icons.fiber_manual_record,
color: AppColors.primaryColor
),
'Voice Track 1'
),
new Grid()
],
)
I want user to be able to tap on header.IngridientHeader and then Grid widget should toggle ( hide if visible and other way around )
edit:
im trying to do something that in bootstrap is called Collapse. getbootstrap.com/docs/4.0/components/collapse
edit 2:
header.IngridientHeader should stay in place all the time
Grid() is scrollable ( horizontal ) widget.
If you want to collapse a widget to zero height or zero width that has a child that overflow when collapsed, I would recommend SizeTransition or ScaleTransition.
Here is an example of the ScaleTransition widget being used to collapse the container for the four black buttons and status text. My ExpandedSection widget is used with a column to get the following structure.
An example of a Widget that use animation with the SizeTransition widget:
class ExpandedSection extends StatefulWidget {
final Widget child;
final bool expand;
ExpandedSection({this.expand = false, this.child});
#override
_ExpandedSectionState createState() => _ExpandedSectionState();
}
class _ExpandedSectionState extends State<ExpandedSection> with SingleTickerProviderStateMixin {
AnimationController expandController;
Animation<double> animation;
#override
void initState() {
super.initState();
prepareAnimations();
_runExpandCheck();
}
///Setting up the animation
void prepareAnimations() {
expandController = AnimationController(
vsync: this,
duration: Duration(milliseconds: 500)
);
animation = CurvedAnimation(
parent: expandController,
curve: Curves.fastOutSlowIn,
);
}
void _runExpandCheck() {
if(widget.expand) {
expandController.forward();
}
else {
expandController.reverse();
}
}
#override
void didUpdateWidget(ExpandedSection oldWidget) {
super.didUpdateWidget(oldWidget);
_runExpandCheck();
}
#override
void dispose() {
expandController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return SizeTransition(
axisAlignment: 1.0,
sizeFactor: animation,
child: widget.child
);
}
}
AnimatedContainer also works but Flutter can complain about overflow if the child is not resizable to zero width or zero height.
Alternatively you can just use an AnimatedContainer to mimic this behavior.
class AnimateContentExample extends StatefulWidget {
#override
_AnimateContentExampleState createState() => new _AnimateContentExampleState();
}
class _AnimateContentExampleState extends State<AnimateContentExample> {
double _animatedHeight = 100.0;
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(title: new Text("Animate Content"),),
body: new Column(
children: <Widget>[
new Card(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
new GestureDetector(
onTap: ()=>setState((){
_animatedHeight!=0.0?_animatedHeight=0.0:_animatedHeight=100.0;}),
child: new Container(
child: new Text("CLICK ME"),
color: Colors.blueAccent,
height: 25.0,
width: 100.0,
),),
new AnimatedContainer(duration: const Duration(milliseconds: 120),
child: new Text("Toggle Me"),
height: _animatedHeight,
color: Colors.tealAccent,
width: 100.0,
)
],
) ,
)
],
),
);
}
}
I think you are looking for ExpansionTile widget. This takes a title property which is equivalent to header and children property to which you can pass widgets to be shown or hidden on toggle.
You can find an example of how to use it here.
Simple Example Usage:
new ExpansionTile(title: new Text("Numbers"),
children: <Widget>[
new Text("Number: 1"),
new Text("Number: 2"),
new Text("Number: 3"),
new Text("Number: 4"),
new Text("Number: 5")
],
),
Hope that helps!
Output:
Code:
class FooPageState extends State<SoPage> {
static const _duration = Duration(seconds: 1);
int _flex1 = 1, _flex2 = 2, _flex3 = 1;
#override
Widget build(BuildContext context) {
final total = _flex1 + _flex2 + _flex3;
final height = MediaQuery.of(context).size.height;
final height1 = (height * _flex1) / total;
final height2 = (height * _flex2) / total;
final height3 = (height * _flex3) / total;
return Scaffold(
body: Column(
children: [
AnimatedContainer(
height: height1,
duration: _duration,
color: Colors.red,
),
AnimatedContainer(
height: height2,
duration: _duration,
color: Colors.green,
),
AnimatedContainer(
height: height3,
duration: _duration,
color: Colors.blue,
),
],
),
);
}
}
Thanks to #Adam Jonsson, his answer resolved my problem. And this is the demo about how to use ExpandedSection, hope to help you.
class ExpandedSection extends StatefulWidget {
final Widget child;
final bool expand;
ExpandedSection({this.expand = false, this.child});
#override
_ExpandedSectionState createState() => _ExpandedSectionState();
}
class _ExpandedSectionState extends State<ExpandedSection>
with SingleTickerProviderStateMixin {
AnimationController expandController;
Animation<double> animation;
#override
void initState() {
super.initState();
prepareAnimations();
_runExpandCheck();
}
///Setting up the animation
void prepareAnimations() {
expandController =
AnimationController(vsync: this, duration: Duration(milliseconds: 500));
animation = CurvedAnimation(
parent: expandController,
curve: Curves.fastOutSlowIn,
);
}
void _runExpandCheck() {
if (widget.expand) {
expandController.forward();
} else {
expandController.reverse();
}
}
#override
void didUpdateWidget(ExpandedSection oldWidget) {
super.didUpdateWidget(oldWidget);
_runExpandCheck();
}
#override
void dispose() {
expandController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return SizeTransition(
axisAlignment: 1.0, sizeFactor: animation, child: widget.child);
}
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
appBar: AppBar(
title: Text('Demo'),
),
body: Home(),
),
);
}
}
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
bool _expand = false;
#override
Widget build(BuildContext context) {
return Column(
children: [
Header(
onTap: () {
setState(() {
_expand = !_expand;
});
},
),
ExpandedSection(child: Content(), expand: _expand,)
],
);
}
}
class Header extends StatelessWidget {
final VoidCallback onTap;
Header({#required this.onTap});
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
color: Colors.cyan,
height: 100,
width: double.infinity,
child: Center(
child: Text(
'Header -- Tap me to expand!',
style: TextStyle(color: Colors.white, fontSize: 20),
),
),
),
);
}
}
class Content extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
color: Colors.lightGreen,
height: 400,
);
}
}
Another solution that doesn't require an animation controller is using AnimatedSwitcher widget with SizeTransition as a transition builder.
here is a simple example:
AnimatedSwitcher(
duration: Duration(milliseconds: 300),
transitionBuilder: (child, animation) {
return SizeTransition(sizeFactor: animation, child: child);
},
child: expanded ? YourWidget() : null,
)
Of course you can customize the curve and layout builder for the animation.