Show modal bottom sheet with custom animation - flutter

I was playing around with showModalBottomSheet() in Flutter and I was thinking about changing the default slide from bottom animation. Looking throught flutter documentation I saw that there is in fact a BottomSheet class that accept animation as parameters but, accordingly to the page, showModalBottomSheet() is preferrable.
Is possible to control the animation in some way? I just need to change the default curve and duration.
Thanks

You can use the AnimationController drive method to modify the animation curve, and duration and reverseDuration to set how long the animation will go on. These can be declared on your initState() if you're using a StatefulWidget.
late AnimationController controller;
#override
initState() {
super.initState();
controller = BottomSheet.createAnimationController(this);
// Animation duration for displaying the BottomSheet
controller.duration = const Duration(seconds: 1);
// Animation duration for retracting the BottomSheet
controller.reverseDuration = const Duration(seconds: 1);
// Set animation curve duration for the BottomSheet
controller.drive(CurveTween(curve: Curves.easeIn));
}
Then configure the BottomSheet transtionAnimationController
showModalBottomSheet(
transitionAnimationController: controller,
builder: (BuildContext context) {
return ...
}
)
Here's a sample that you can try out.
import 'package:flutter/material.dart';
class BottomSheetAnimation extends StatefulWidget {
const BottomSheetAnimation({Key? key}) : super(key: key);
#override
_BottomSheetAnimationState createState() => _BottomSheetAnimationState();
}
class _BottomSheetAnimationState extends State<BottomSheetAnimation>
with TickerProviderStateMixin {
late AnimationController controller;
#override
initState() {
super.initState();
controller = BottomSheet.createAnimationController(this);
// Animation duration for displaying the BottomSheet
controller.duration = const Duration(seconds: 1);
// Animation duration for retracting the BottomSheet
controller.reverseDuration = const Duration(seconds: 1);
// Set animation curve duration for the BottomSheet
controller.drive(CurveTween(curve: Curves.easeIn));
}
#override
void dispose() {
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'BottomSheet',
home: Scaffold(
appBar: AppBar(
title: const Text('BottomSheet'),
),
body: Center(
child: TextButton(
child: const Text("Show bottom sheet"),
onPressed: () {
showModalBottomSheet(
context: context,
transitionAnimationController: controller,
builder: (BuildContext context) {
return const SizedBox(
height: 64,
child: Text('Your bottom sheet'),
);
},
);
},
),
),
),
);
}
}
Demo

Related

How to make text transition by animation upon button press?

Currently, when opacity = 0 & button is pressed, opacity becomes one. But I would like the button also having the function to turn opacity to zero and back to one when opacity is already one.
How can this be made possible ? Thanks
Im thinking it has something to do with tween? Eg. When the button is pressed, the opacity of the quote turns to zero and back to 1 (duration of one second).
But Im not sure how to write that.
Attached below is the main chunk of the code that's involved in this action:
double opacity1 = 0.0;
String quoteCat1 = List1[Random().nextInt(List1.length)];
void generateConvoTopic1() {
setState(() {
quoteCat1 = List1[Random().nextInt(List1.length)];
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
...
body: ...
AnimatedOpacity(
opacity: opacity1,
duration: Duration(seconds: 1),
child:Text(quoteCat1),
]),
...
ElevatedButton(
...
onPressed: () {
generatequoteCat1();
opacity1 = 1.0;
opacity2 = 0.0;
opacity3 = 0.0;
},),
...
If you only want to switch between two values, you could use a boolean and make your button switch its value. In the toggleOpacity function is also an example of how to animate back the function as #Aayush said using a Future.delayed.
Now if you want more complex animations, or infinite ones, you should use an animationController to have full control of it
See example:
class Demo extends StatefulWidget {
Demo({Key key, this.title}) : super(key: key);
final String title;
#override
_DemoState createState() => _DemoState();
}
class _DemoState extends State<Demo> {
bool visible = true;
var animateBack=false;
void toggleOpacity() {
setState(() {
visible = !visible;
});
if(animateBack)
Future.delayed(Duration(seconds: 1)).then((value) {
setState(() {
visible = !visible;
});
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: AnimatedOpacity(
opacity: visible ? 1 : 0,
duration: Duration(seconds: 1),
child: Text(
'Now you see me',
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: toggleOpacity,
child: Icon(visible?Icons.wb_sunny:Icons.wb_sunny_outlined),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}

Animate widget on creation

I'm very new to Flutter and I'm trying to understand animations.
This widget will slide up from bottom to center when rendered.
It seems to work, but I'm uncertain if this is the way to do it.
So what I want is that every time this Text widget is put on the screen it should animate from bottom to center. I did that by calling a function from WidgetsBinding.instance.addPostFrameCallback that changes the state in the initState .
Is there a better, cleaner way to this?
class AnimatedCenterText extends StatefulWidget {
final String text;
AnimatedCenterText(this.text);
#override
_AnimatedCenterTextState createState() => _AnimatedCenterTextState();
}
class _AnimatedCenterTextState extends State<AnimatedCenterText> {
AlignmentGeometry _alignment = Alignment.bottomCenter;
void _changeAlignment() {
setState(() {
_alignment = Alignment.center;
});
}
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_changeAlignment();
});
}
#override
Widget build(BuildContext context) {
return AnimatedAlign(
alignment: _alignment,
duration: Duration(milliseconds: 300),
curve: Curves.easeInCubic,
child: Text(
widget.text,
style: TextStyle(fontSize: 28),
),
);
}
}

Flutter - Play 2 or more Lottie files in sequence

I want to play to 2 Lottie files in sequence, i.e. after one Lottie has completed its animation it should play the second Lottie file.
I tried to achieve this by adding a statuslistener (via AnimationController) to the Lottie widget and calling setstate() on the asset file after first Lottie has completed its animation. It did work but there was a lag while switching to the next Lottie file.
void statusListener(AnimationStatus status) {
if (status == AnimationStatus.completed) {
setState(() {
asset = asset2;
});
controller.reset();
controller.forward();
}
}
Can anyone help me figure it out?
Thanks.
Define two different controller for both the animations.
then play the first animation and hide the second animation for now.
After the first animation gets completed, hide it through visibility.
for example :
Visibility(
child: Text("Gone"),
visible: false,
),
Refer this for more detail : stackoverflow : how to hide widget programmatically
then play the second animation and hide the first animation.
for the time delay, use Future.delayed.
this will execute the code after specific time which you chosed.
example :
Let's say your first animation completes in 2 seconds then, you will play the next animation after 2 seconds so that you will execute the next line of code after 2 seconds.
Future.delayed(const Duration(seconds: 2), () {
setState(() {
_controller.forward();
});
});
There is an example in the lottie repo.
I effectively spent an entire day figuring out a solution, so posting this to calm the mind.
Repo Example that plays many different lottie files in sequence:
import 'package:flutter/material.dart';
import 'package:lottie/lottie.dart';
class App extends StatefulWidget {
const App({Key? key}) : super(key: key);
#override
State<App> createState() => _AppState();
}
class _AppState extends State<App> with TickerProviderStateMixin {
int _index = 0;
late final AnimationController _animationController;
#override
void initState() {
super.initState();
_animationController = AnimationController(vsync: this)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
setState(() {
++_index;
});
}
});
}
#override
void dispose() {
_animationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
color: Colors.lightBlue,
home: Scaffold(
backgroundColor: Colors.lightBlue,
appBar: AppBar(
title: Text('$_index'),
),
body: SingleChildScrollView(
child: Center(
child: Column(
children: [
Lottie.asset(files[_index % files.length],
controller: _animationController, onLoaded: (composition) {
_animationController
..duration = composition.duration
..reset()
..forward();
}),
],
),
),
),
),
);
}
}

Splash screen - AnimationController won't start before async call is done on initState

I'm trying to make a splash screen for my Flutter application. I want my logo to rotate while checking if the user is logged on firebase authentifaction and then going to the views concerned depending of the return value.
The problem is that my application doesn't build properly before my async call (I see my backGround but not the AnimatedBuilder).
I tried running my CheckUser() using the after_layout package, or using this function :
WidgetsBinding.instance.addPostFrameCallback((_) => yourFunction(context));
but it always wait for the CheckUser() function to finish so I don't see the animation as it navigates directly to my other views.
Here's my code if you want to test it :
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:skull_mobile/connexion/login.dart';
import 'accueil.dart';
class SplashPage extends StatefulWidget {
SplashPage({Key key}) : super(key: key);
#override
_SplashPage createState() => _SplashPage();
}
class _SplashPage extends State<SplashPage>
with SingleTickerProviderStateMixin {
AnimationController animationController;
#override
void initState() {
super.initState();
animationController = new AnimationController(
vsync: this,
duration: new Duration(seconds: 5),
);
animationController.repeat();
checkUser();
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[800],
body: Center(
child: Container(
child: new AnimatedBuilder(
animation: animationController,
child: new Container(
height: 150.0,
width: 150.0,
child: new Image.asset('assets/skull.png'),
),
builder: (BuildContext context, Widget _widget) {
return new Transform.rotate(
angle: animationController.value * 6.3,
child: _widget,
);
},
),
),
),
);
}
void checkUser() async {
FirebaseAuth.instance.currentUser().then((currentUser) => {
if (currentUser == null)
{Navigator.pushNamed(context, LoginPage.routeName)}
else
{Navigator.pushNamed(context, AccueilPage.routeName)}
});
}
}
Following on my comment I'm sharing here a snippet of my own code and how I handle a splash screen, here called "WaitingScreen", the device's Connection state and then send the user to different pages with different properties depending on the results:
#override
Widget build(BuildContext context) {
switch (authStatus) {
case AuthStatus.notDetermined:
if(_connectionStatus == ConnectionStatus.connected){
return _buildWaitingScreen();
}else{
return _buildNoConnectionScreen();
}
break;
case AuthStatus.notSignedIn:
return LoginPage(
onSignedIn: _signedIn,
setThemePreference: widget.setThemePreference,
);
case AuthStatus.signedIn:
return MainPage(
onSignedOut: _signedOut,
setThemePreference: widget.setThemePreference,
getThemePreference: widget.getThemePreference,
);
}
return null;
}

Update SnackBar content in Flutter

I have a button that displays a SnackBar (or toast) before moving to the next page. I have a countdown and after 5 seconds I push Page2.
RaisedButton(
onPressed: () {
_startTimer();
final snackBar = SnackBar(
behavior: SnackBarBehavior.floating,
content: Text(
'Prepare yourself to start in ${widget._current.toString()}!'), // doesn't work here
duration: new Duration(seconds: widget._start),
action: SnackBarAction(
label: widget._current.toString(), // and neither does here
onPressed: () {
// Some code to undo the change.
},
),
);
Scaffold.of(context).showSnackBar(snackBar);
},
child: Text(
"I'm ready",
style: TextStyle(fontSize: 20),
),
),
Nothing to see on the countdown but I'll paste it just in case:
void _startTimer() {
CountdownTimer countDownTimer = new CountdownTimer(
new Duration(seconds: widget._start),
new Duration(seconds: 1),
);
var sub = countDownTimer.listen(null);
sub.onData((duration) {
setState(() {
widget._current = widget._start - duration.elapsed.inSeconds;
});
});
sub.onDone(() {
print("Done");
sub.cancel();
});
}
So if I display the countdown somewhere else (inside a Text for example) it works but it seems that the SnackBar doesn't change its contain, it always get the max number of the countdown.
you need to implement a custom widget with countdown logic in side for the content field of snackbar, like this:
class TextWithCountdown extends StatefulWidget {
final String text;
final int countValue;
final VoidCallback? onCountDown;
const TextWithCountdown({
Key? key,
required this.text,
required this.countValue,
this.onCountDown,
}) : super(key: key);
#override
_TextWithCountdownState createState() => _TextWithCountdownState();
}
class _TextWithCountdownState extends State<TextWithCountdown> {
late int count = widget.countValue;
late Timer timer;
#override
void initState() {
super.initState();
timer = Timer.periodic(Duration(seconds: 1), _timerHandle);
}
#override
void dispose() {
timer.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Container(
child: Text("[$count] " + widget.text),
);
}
void _timerHandle(Timer timer) {
setState(() {
count -= 1;
});
if (count <= 0) {
timer.cancel();
widget.onCountDown?.call();
}
}
}
That is because the snack bar is built once, when the button is clicked. When the state updates, it rebuilds the widget tree according to the changes. The snack bar initially isn't in the widget tree, so it doesn't update.
Try to use stack and show a snack bar, and then you should be able to manipulate it however you need.
Hope it helps.
Update SnackBar Content
SnackBar content can be updated/rebuilt while it's visible by making its content: widget dynamic.
In the code sample below we're using a ValueNotifier and ValueListenableBuilder to dynamically rebuild the content of the SnackBar whenever ValueNotifier is given a new value.
(There are many ways to to maintain state values & rebuild widgets when it changes, such as RiverPod, GetX, StreamBuilder, etc. This example uses the Flutter native ValueListenableBuilder.)
When running this Flutter page, click the FAB to show the SnackBar, then click on the center text to update the SnackBar's content (multiple times if you like).
Example
Use SnackBarUpdateExample() widget in your MaterialApp home: argument to try this example in an emulator or device.
class SimpleCount {
int count = 0;
}
class SnackBarUpdateExample extends StatelessWidget {
static const _initText = 'Initial text here';
/// This can be "listened" for changes to trigger rebuilds
final ValueNotifier<String> snackMsg = ValueNotifier(_initText);
final SimpleCount sc = SimpleCount();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('SnackBar Update'),
),
/// ↓ Nested Scaffold not necessary, just prevents FAB being pushed up by SnackBar
body: Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Click FAB to show SnackBar'),
SizedBox(height: 20,),
Text('Then...'),
SizedBox(height: 20,),
InkWell(
child: Text('Click → here ← to update SnackBar'),
onTap: () {
sc.count++;
snackMsg.value = "Hey! It changed! ${sc.count}";
},
), /// When snackMsg.value changes, the ValueListenableBuilder
/// watching this value, will call its builder function again,
/// and update its SnackBar content widget
],
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.play_arrow),
onPressed: () => _showSnackBar(context),
),
),
);
}
void _showSnackBar(BuildContext context) {
var _controller = ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: SnackContent(snackMsg))
);
/// This resets the snackBar content when it's dismissed
_controller.closed.whenComplete(() {
snackMsg.value = _initText;
sc.count = 0;
});
}
}
/// The ValueListenableBuilder rebuilds whenever [snackMsg] changes.
class SnackContent extends StatelessWidget {
final ValueNotifier<String> snackMsg;
SnackContent(this.snackMsg);
#override
Widget build(BuildContext context) {
/// ValueListenableBuilder rebuilds whenever snackMsg value changes.
/// i.e. this "listens" to changes of ValueNotifier "snackMsg".
/// "msg" in builder below is the value of "snackMsg" ValueNotifier.
/// We don't use the other builder args for this example so they are
/// set to _ & __ just for readability.
return ValueListenableBuilder<String>(
valueListenable: snackMsg,
builder: (_, msg, __) => Text(msg));
}
}