Recently I needed a to create an animation to dismiss a banner so that it slides up under the app bar. I was sure that using SlideTransition is going to be the end of it but I was quite surprised that it keeps the original space of the widget as it was at the start of the animation. So I kept looking, trying Transform and FractionalTranslation, couldn’t make neither work. So I looked into the code of something similar from the flutter framework, the ExpansionTile, and that is where I got the inspiration from.
This is the starting point
I always try my code in a side project when I want to have less distractions.
import 'package:flutter/material.dart';
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> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(
children: <Widget>[
MaterialBanner(
content: Text('This is a banner. Dismiss me'),
actions: <Widget>[
FlatButton(
onPressed: () {},
child: Text('DISMISS'),
),
],
),
Container(
height: 200,
alignment: Alignment.center,
color: Colors.blue,
child: Text('A Container'),
),
],
),
);
}
}
Now, with animation
import 'package:flutter/material.dart';
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();
}
// We add the [SingleTickerProviderStateMixin] because it allows us to
// create an
//[AnimationController] within the class
class _MyHomePageState extends State<MyHomePage>
with SingleTickerProviderStateMixin<MyHomePage> {
///The animation controller that starts the slide effect
AnimationController _slideAnimationController;
///The animation that creates the slide up effect by controlling the
// height
///factor of the [Align] widget
Animation<double> _heightFactorAnimation;
@override
void initState() {
_slideAnimationController = AnimationController(
vsync: this,
//whatever duration you want
duration: Duration(milliseconds: 400),
);
_heightFactorAnimation = CurvedAnimation(
parent: _slideAnimationController.drive(
// a Tween from 1.0 to 0.0 is what makes the slide effect by
// shrinking
// the container using the [Align.heightFactor] parameter
Tween<double>(
begin: 1.0,
end: 0.0,
),
),
curve: Curves.ease);
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(
children: <Widget>[
//This is where the magic begins
AnimatedBuilder(
animation: _slideAnimationController,
builder: (BuildContext context, Widget child) {
return ClipRect(
child: Align(
alignment: Alignment.bottomCenter,
heightFactor: _heightFactorAnimation.value,
child: child,
),
);
},
// This is where the magic ends. Just so we're clear
// we wrap our content in the child param as it is
// not affected by
// the animation itself
child: MaterialBanner(
content: Text('This is a banner. Dismiss me'),
actions: <Widget>[
FlatButton(
onPressed: () {
//pressing the button will start the animation
_slideAnimationController.forward();
},
child: Text('DISMISS'),
),
],
),
),
Container(
height: 200,
alignment: Alignment.center,
color: Colors.blue,
child: Text('A Container'),
),
],
),
);
}
}
If you look closer you can see that while visually the widget has been dismissed it is still there in the widget tree after the animation finishes. To achieve that we need an extra step and here to each his own, some people would use setState() I personally prefer having a model and for my widget to listen to it. So I wrap the widget with ValueListenableBuilder and Visibility.
Here is the final result
import 'package:flutter/material.dart';
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();
}
//We add the [SingleTickerProviderStateMixin] because it allows us to
//create an
//[AnimationController] within the class
class _MyHomePageState extends State<MyHomePage>
with SingleTickerProviderStateMixin<MyHomePage> {
///The animation controller that starts the slide effect
AnimationController _slideAnimationController;
///The animation that creates the slide up effect by controlling the
//height
///factor of the [Align] widget
Animation<double> _heightFactorAnimation;
///Governs whether to show the banner or not. We use a [ValueNotifier]
///because the visibility changes asynchronously when the animation
//finishes,
///which we want to trigger the rebuild of [ValueListenableBuilder] That
///listens to this value
ValueNotifier<bool> _isVisibleValueNotifier = ValueNotifier(true);
@override
void initState() {
_slideAnimationController = AnimationController(
vsync: this,
//whatever duration you want
duration: Duration(milliseconds: 400),
);
_heightFactorAnimation = CurvedAnimation(
parent: _slideAnimationController.drive(
//a Tween from 1.0 to 0.0 is what makes the slide effect by
//shrinking
// the container using the [Align.heightFactor] parameter
Tween<double>(
begin: 1.0,
end: 0.0,
),
),
curve: Curves.ease);
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(
children: <Widget>[
//This is where the magic begins
ValueListenableBuilder<bool>(
valueListenable: _isVisibleValueNotifier,
builder: (context, isVisible, child) {
return Visibility(
visible: isVisible,
child: child,
);
},
child: AnimatedBuilder(
animation: _slideAnimationController,
builder: (BuildContext context, Widget child) {
return ClipRect(
child: Align(
alignment: Alignment.bottomCenter,
heightFactor: _heightFactorAnimation.value,
child: child,
),
);
},
//This is where the magic ends. Just so we're clear
// we wrap our content in the child param as it is not
// affected by
// the animation itself
child: MaterialBanner(
content: Text('This is a banner. Dismiss me'),
actions: <Widget>[
FlatButton(
onPressed: () async {
//pressing the button will start the animation
await _slideAnimationController.forward();
_isVisibleValueNotifier.value = false;
},
child: Text('DISMISS'),
),
],
),
),
),
Container(
height: 200,
alignment: Alignment.center,
color: Colors.blue,
child: Text('A Container'),
),
],
),
);
}
}
Comments