I am working On Tap Bounce feature on my widget. I have referred to these links for information:
Bouncing Button Flutter
Create on tap bounce on flutter
I got this package named as bouncing_widget, which was good enough for the workaround, but there is one limitation to it, i.e., It is not good for scrolling widget. You cannot scroll the page by tapping on the widget which uses this flutter bouncing widget
Somebody has already created a bug for the above widget, and no solution is found. Here is where you can find the issue: Bouncing Widget GitHub Issue
So, I decided to make my own widget which does the job for me. I have made the widget, and it works fine, but there is a limitation, i.e.,
When you scroll the page by holding on the widget, the widget stays there in one fixed position, that means, like it remains in pressed position
Here is my code:
import 'package:flutter/material.dart';
class CLBounceWidget extends StatefulWidget {
final Function onPressed;
final Widget child;
CLBounceWidget({Key key, #required this.onPressed, #required this.child}): super(key:key);
#override
CLBounceWidgetState createState() => CLBounceWidgetState();
}
class CLBounceWidgetState extends State<CLBounceWidget> with SingleTickerProviderStateMixin{
double _scale;
final Duration duration = Duration(milliseconds: 200);
AnimationController _animationController;
//Getting onPressed Calback
VoidCallback get onPressed => widget.onPressed;
#override
void initState() {
_animationController = AnimationController(
vsync: this,
duration: duration,
lowerBound: 0.0,
upperBound: 0.1
)..addListener((){ setState((){}); });
super.initState();
}
#override
void dispose() {
// TODO: implement dispose
_animationController?.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
_scale = 1 - _animationController.value;
return GestureDetector(
onTapDown: _onTapDown,
onTapUp: _onTapUp,
child: Transform.scale(
scale: _scale,
child: widget.child
)
);
}
// Triggering the onPressed callback with a check
void _onTapTrigger(){
if(onPressed != null){
onPressed();
}
}
//Start the animation
_onTapDown(TapDownDetails details){
_animationController.forward();
}
// We revise the animation and notify the user of the event
_onTapUp(TapUpDetails details){
Future.delayed(Duration(milliseconds: 100), (){
_animationController.reverse();
});
//Finally calling the callback function
_onTapTrigger();
}
}
Also: I have tried doing this, without using Future, but with that, Animation only happens when long pressed, just _onTapTrigger() works:
_onTapUp(TapUpDetails details){
_animationController.reverse();
_onTapTrigger();
}
Tried: I have tried using onLongPress, onLongPressEnd, onLongPressMoveUpdate to at least do the _animationController.reverse();, but nothing worked out for me.
I want the widget to stay normal when I scroll the page while holding the widget, like a normal widget performs.
Since, after a long more researches, and hours and hours of toil, I finally found out what I was willing to have in my project. I have created a package on flutter pub.dev to help other developers in need of what the package flutter_bounce has to offer.
You can search for flutter_bounce on the above link, or you can directly go to this: flutter_bounce
To make use of it, you just need to do this:
Bounce(
duration: Duration(milliseconds: 110),
onPressed: (){ YOUR_FUNCTION },
child: YOUR_WIDGET
)
For more details, please feel free to read the README of the link. It has all necessary details. Happy coding :)
Another new package that you guys can try is flutter_bounceable that I've developed. If you guys prefer a much more simple, interactive and customizable bounce widget, then this package is just for you.
Bounceable(
onTap: yourFunction, // set to null if want to disable bounceable
child: YourWidget(),
duration: const Duration(milliseconds: 200), // optional
reverseDuration: const Duration(milliseconds: 200), // optional
curve: Curves.decelerate, // optional
reverseCurve: Curves.decelerate, // optional
);
Related
I have developed an anagram solving game, which presents an anagram to the user that they have to solve as per below.
The layout is a table with a series of table rows, each row containing table cells. Each table cell has a container with a Text widget that can be empty or contain a character. The characters themselves are held in a List that is the same length as the number of table cells on the screen. Each Text widget in the table cells draws its text value from the corresponding item in the List.
The player can shuffle the characters if they want to display them in a different order. I have added animations to the Text widgets so that they fade out of view, the underlying List is randomly shuffled, and then the characters fade back into view in their new positions. The basic workflow is:
User presses shuffle
The program iterates over the List of characters and triggers the fade out animation for
any Text widgets that have a text value The fade out animations last for 800 milliseconds
The programme then shuffles the target word in the text List The programme iterates of the
List of characters again and triggers the fade-in animations for any Text widgets that have
a text value
My problem is that the animations do not always perform as planned. Sometimes the characters disappear and then fade-in. Sometimes they fade-out and remain hidden. Sometimes they work as planned above. I am assuming that this is because of the timing of the animations and my code. Currently I have a sequence of code in one class that executes the activities above in one go, as per the pseudo-code below
For each table cell {
if (table cell Text widget has a value) then {
trigger the Text widget fade-out animation;
}
}
Shuffle the text List;
For each table cell {
if (table cell Text widget has a value) then {
trigger the Text widget fade-in animation;
}
}
I assume that executing the code this way is causing the problem because it means that my fade-out animations will be triggered, the underlying text List will be shuffled whilst those animations are still running and the fade-out animations will also be triggered before the fade-out animations have finished.
My question is, what is the correct design pattern to control the execution timing of the animations and the shuffle function so that they execute sequentially without overlapping?
I have looked at creating a type of stack where I push the animations and shuffle functions onto a stack and then execute them, but that feels clunky because I need to differentiate between a number of parallel fade-out animations (for example, if the word to be guessed has 8 characters then my program triggers 8 fade-out animations) and then calling the shuffle function.
As per this post, I have also looked at using the .whenComplete() method:
_animationController.forward().whenComplete(() {
// put here the stuff you wanna do when animation completed!
});
But have the same issue that I would with a stack approach in terms of coordinating this for a number of parallel animations.
I have thought about designing my Text character widget so that I could pass a flag that would trigger the .whenComplete() method for the first Text widget in the grid with a value, and just let the other Text widget fade-out animations run separately. I could then shuffle the text at the end of the first Text widget fade-out animation using a callback and trigger the fade-in animations after the shuffle function.
Again, this feels kind of clunky and I want to know if I am missing something. Is there anything built into Flutter to support animation->non-animation function->animation chaining or is there a design pattern that would specifically address this problem in a graceful way?
I have implemented this using both callback functions and a stack since I felt that this would give me the most flexibility, e.g. if I wanted to hide/show the Text widgets with different start/end times to give it a more organic feel. This works, but as per my original question I am open to suggestions if there is a better way to implement this.
The basic execution workflow in pseudo-code is:
Grid Display
shuffle.onPressed() {
disable user input;
iterate over the grid {
if (cell contains a text value) {
push Text widget key onto a stack (List);
trigger the hide animation (pass callback #1);
}
}
}
Text widget hide animation
hide animation.whenComplete() {
call the next function (callback #1 - pass widget key);
}
Callback function #1
remove Text widget key from the stack;
if (stack is empty) {
executive shuffle function;
iterate over the grid;
if (cell contains a text value) {
push Text widget key onto a stack (List);
trigger the show animation (pass callback #2);
}
}
Text widget show animation
show animation.whenComplete() {
call the next function (callback #2 - pass widget key);
}
Callback function #2
remove Text widget key from the stack
if (stack is empty) {
enable user input;
}
I have included extracts of the code below to show how I have implemented this.
The main class showing the grid on screen has the following variables and functions.
class GridState extends State<Grid> {
// List containing Text widgets to displays in cells including unique
// keys and text values
final List<TextWidget> _letterList = _generateList(_generateKeys());
// Keys of animated widgets - used to track when these finish
final List<GlobalKey<TextWidgetState>> _animations = [];
bool _isInputEnabled = true; // Flag to control user input
#override
Widget build(BuildContext context) {
…
ElevatedButton(
onPressed: () {
if (_isInputEnabled) {
_hideTiles();
}
},
child: Text('shuffle', style: TextStyle(fontSize: _fontSize)),
),
…
}
// Function to hide the tiles on the screen using their animation
void _hideTiles() {
_isInputEnabled = false; // Disable user input
// Hide the existing tiles using animation
for (int i = 0; i < _letterList.length; i++) {
// Only animate if the tile has a text value
if (_letterList[i].hasText()) {
_animations.add(_letterList[i].key as GlobalKey<LetterTileState>);
_letterList[i].hide(_shuffleAndShow);
}
}
}
// Function to shuffle the text on screen and then re-show the tiles using
// their animations
void _shuffleAndShow() {
_animations.remove(key);
if (_animations.isEmpty) {
widget._letterGrid.shuffleText(
widget._letterGrid.getInputText(), widget._options.getCharType());
// Update the tiles with the new characters and show the new tile locations using animation
for (int i = 0; i < _letterList.length; i++) {
// Update tile with new character
_letterList[i].setText(
widget._letterGrid.getCell(i, widget._options.getCharType()));
// If the tile has a character then animate it
if (_letterList[i].hasText()) {
_animations.add(_letterList[i].key as GlobalKey<LetterTileState>);
_letterList[i].show(_enableInput);
}
}
}
}
// Function re-enable user input following the shuffle animations
void _enableInput(GlobalKey<LetterTileState> key) {
_animations.remove(key);
if (_animations.isEmpty) {
_isInputEnabled = true;
}
}
The Text widgets held in _letterList have the following animation functions, which call the callback function when they have finished. Note this code is in the State of a Statefulwidget.
// Animation variables
final Duration _timer = const Duration(milliseconds: 700);
late AnimationController _animationController;
late Animation<double> _rotateAnimation;
late Animation<double> _scaleAnimation;
#override
void initState() {
super.initState();
// Set up the animation variables
_animationController = AnimationController(vsync: this, duration: _timer);
_rotateAnimation = Tween<double>(begin: 0, end: 6 * pi).animate(
CurvedAnimation(
parent: _animationController,
curve: const Interval(0, 1, curve: Curves.easeIn)));
_rotateAnimation.addListener(() {
setState(() {});
});
_scaleAnimation = Tween<double>(begin: 1, end: 0).animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(0, 0.95, curve: Curves.ease)));
_scaleAnimation.addListener(() {
setState(() {});
});
}
///
/// Animation functions
///
// Function to hide the tile - spin and shrink to nothing
void hide(Function callback) {s
_animationController.forward(from: 0).whenComplete(() {
_animationController.reset();
callback(widget.key);
});
}
// Function to show the tile - spin and grow from nothing
void show(Function callback) {
_animationController.reverse(from: 1).whenComplete(() {
_animationController.reset();
callback(widget.key);
});
}
UPDATE:
Having done more reading and built an experimental app using the default counter example, I have found that added Listeners and StatusListeners are another, perhaps better, way to do what I want. This would also work with the stack approach I used in my earlier answer as well.
Example code below:
main class:
import 'package:flutter/material.dart';
import 'counter.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
Counter counter = Counter();
late AnimationController animationController;
late Animation<double> shrinkAnimation;
#override
void initState() {
animationController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 500));
shrinkAnimation = Tween<double>(begin: 1.0, end: 0.0).animate(
CurvedAnimation(
parent: animationController,
curve: const Interval(0.0, 1.0, curve: Curves.linear)));
shrinkAnimation.addListener(() {
setState(() {}); // Refresh the screen
});
shrinkAnimation.addStatusListener((status) {
switch (status) {
// Completed status is after the end of forward animation
case AnimationStatus.completed:
{
// Increment the counter
counter.increment();
// Do some work that isn't related to animation
int value = 0;
for (int i = 0; i < 1000; i++) {
value++;
}
print('finishing value is $value');
// Then reverse the animation
animationController.reverse();
}
break;
// Dismissed status is after the end of reverse animation
case AnimationStatus.dismissed:
{
animationController.reset();
}
break;
}
});
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
AnimatedBuilder(
animation: animationController,
builder: (context, child) {
return Transform.scale(
alignment: Alignment.center,
scale: shrinkAnimation.value,
child: Text(
'${counter.get()}',
style: Theme.of(context).textTheme.headline4,
),
);
}),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
animationController.forward(); // Shrink current value first
},
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
counter class:
import 'package:flutter/cupertino.dart';
class Counter {
int _counter = 0;
void increment() {
_counter++;
}
int get() {
return _counter;
}
}
I have a list if widgets that I swipe off the screen. On swipe, i remove the first widget from the list but i realized the dispose method of that widget is not called therefore the AnimationContoller is still held by flutter and used for the subsequent widgets. Animation for the subsequent widgets are ended by the time they are brought to the front. This makes my app not behave as expected. To test this, I print a string in the initState and dispose method of the widgets. The initState prints but the dispose method does not. Any help please ?
class ProfileWidget extends StatefulWidget {
ProfileWidget({Key key}) : super(key: key);
#override
State<StatefulWidget> createState() => _ProfileWidgetState();
}
class _ProfileWidgetState extends State<ProfileWidget> with TickerProviderStateMixin {
#override
void initState() {
super.initState();
print("New init");
_animationController = new AnimationController(
vsync: this,
duration: const Duration(milliseconds: 400),
);
_fadeAnimationController = new AnimationController(
vsync: this,
duration: const Duration(milliseconds: 200),
);
_fadeAnimation = new Tween(
begin: 0.0,
end: 1.0,
).animate(_fadeAnimationController);
_fadeAnimationController.forward();
}
#override
void dispose() {
print("Disposing");
if (_animationController != null) {
_animationController.dispose();
}
if (_fadeAnimationController != null) {
_fadeAnimationController.dispose();
}
super.dispose();
}
}
it's a known bug in flutter and the discussion is ongoing: https://github.com/flutter/flutter/issues/40940
even if you copy examples from official docs, it will not get called: https://flutter.dev/docs/cookbook/networking/web-sockets#complete-example
however flutter devs claim that this a desired behavior. Please voice your support in the github issue if you think it should be fixed.
i am building an chat app. I want whenever the chat class open it scrolled to max initially.
i tried the below code but it's wrong
void initState() {
super.initState();
scrollController = ScrollController();
scrollController.animateTo(scrollController.position.maxScrollExtent,
duration: Duration(milliseconds: 10), curve: Curves.easeOut);
}
I'm also building an app with chatting integrated, and I had the same problem. I fixed it by making it addPostFrameCallback when building. This is called everytime the widget is rebuilt.
Widget build(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) {
scrollController.jumpTo(positionToScroll);
}
}
You can use animateTo instead of jumpTo and it should still work.
Probably, any scrollable widget is not rendering in initstate yet.
You can try to change the scroll position after your scrollable widget is ready.
Use addPostFrameCallback from SchedulerBinding to force ScrollController to move only when the building process completes, guaranteeing it has an initial position to move from :
Import Scheduler :
import 'package:flutter/scheduler.dart';
Modify your initState() :
void initState() {
super.initState();
scrollController = ScrollController();
SchedulerBinding.instance.addPostFrameCallback((_) => scrollController.animateTo(scrollController.position.maxScrollExtent, duration: Duration(milliseconds: 10), curve: Curves.easeOut));
}
import "package:flutter/material.dart";
import "dart:async";
class JoinScreen extends StatefulWidget {
#override
_JoinScreenState createState() {
return _JoinScreenState();
}
}
class _JoinScreenState extends State<JoinScreen> {
List<Widget> widgetList = [];
#override
void initState() {
new Timer(const Duration(milliseconds: 100), () {
print('timeout');
setState(() {
widgetList.add(secondHalf());
});
});
new Timer(const Duration(milliseconds: 1000), () {
print('timeout');
setState(() {
widgetList.add(firstHalf());
});
});
super.initState();
}
#override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: Duration(seconds: 2),
child: Column(
children: widgetList,
),
);
}
Widget firstHalf() {
return Expanded(
child: Container(
decoration: BoxDecoration(color: Colors.blueAccent),
),
);
}
Widget secondHalf() {
return Expanded(
child: Container(
decoration: BoxDecoration(color: Colors.pinkAccent),
),
);
}
}
If i changed the width and height of the container with the help of timer and setstate, it animates. But when adding two new list of widgets to the build function, nothing animates.
I want to have a expanding animation. Because i am using expanded, i am not able to give specific height which is meaningless with expanded.
How do i do this?
Use AnimatedSize instead of AnimatedContainer.
for AnimatedSize to work we need to use a mixin called SingleTickerProviderStateMixin on the _JoinScreenState and set the vsync property to the current instance (this) after that the AnimatedSize will look for changes on its child and animate accordingly,
Update:
TickerProvider vsync is deprecated after Flutter v2.2.0-10.1.pre. It is now implemented in the widget (AnimatedSize) itself. for reference check the source code.
here is your code
import "package:flutter/material.dart";
import "dart:async";
class JoinScreen extends StatefulWidget {
#override
_JoinScreenState createState() {
return _JoinScreenState();
}
}
class _JoinScreenState extends State<JoinScreen>
with SingleTickerProviderStateMixin {
List<Widget> widgetList = [];
#override
void initState() {
new Timer(const Duration(milliseconds: 100), () {
print('timeout');
setState(() {
widgetList.add(secondHalf());
});
});
new Timer(const Duration(milliseconds: 1000), () {
print('timeout');
setState(() {
widgetList.add(firstHalf());
});
});
super.initState();
}
#override
Widget build(BuildContext context) {
return AnimatedSize(
vsync: this,
duration: Duration(seconds: 2),
child: Column(
children: widgetList,
),
);
}
Widget firstHalf() {
return Expanded(
child: Container(
decoration: BoxDecoration(color: Colors.blueAccent),
),
);
}
Widget secondHalf() {
return Expanded(
child: Container(
decoration: BoxDecoration(color: Colors.pinkAccent),
),
);
}
}
This is almost certainly a duplicate as I think I've answered this before, but I can't find the question easily so I'll just answer it again.
To understand why this isn't working, first you have to understand how Dart does comparisons on objects. If an object is a primitive, simple, or has comparison functions/operators defined (i.e. int, boolean, String, etc) dart can compare the objects. If an object is more complicated and doesn't define compareTo, operator=, operator< or operator>, dart doesn't know how to do that type of comparison on it. Instead, a comparison turns into "is object a the same object as object b".
This is important because Flutter is lazy. It doesn't want to have to rebuild widgets unless it absolutely has to. So when you change the state with setState, flutter then comes along and takes a look at your State to see if it has actually changed. With the cases of height or width it's easy - it can check that those have changed.
The reason it doesn't work when you mutate your list is exactly that; you're mutating an existing list. So when dart checks whether oldState.widgetList == newState.widgetList, it's not actually comparing whether each element of the list is the same but rather checking whether the list is the same. Since it's the same object, the list shows as being the same so flutter moves on to the next step without rebuilding.
There are three main ways to get around this. The first is to make a copy of the list each time you edit it. Depending on how many elements are in the list, this could be a bad idea - when you copy the elements you're not actually copying each little bit of information, but it still is an O(n) operation.
The second is to maintain a separate variable in the State. The reason this helps is because if any part of the state has changed, it triggers a rebuild of all the widgets i.e. calls the build function, whether or not the actual property that each widget uses is 'changed' (mostly because it would be very difficult to manage keeping track of that for each and every widget that is built). I personally do it this way - I maintain an integer counter that I just increment each time a change happens in the list. It might not be the cleanest solution, but it is performant and pretty simple!
The last way would be to implement your own list that does do a 'deep' comparison (i.e. checks whether number of elements are the same, and then possibly checks whether each element is the same). This isn't done by default in dart's lists because it would be easy to cause performance issues if you start comparing strings without realizing each element in the list could be used as part of the comparison.
I made a custom transition for my iOS project, and now I want to move the project to Flutter. The transition is fading out the old page, and fading in the new one.
But I cannot achieve this by overriding the PageRoute.
I did some research on this:
There's a similar question
Animate route that is going out / being replaced
From the accepted answer, I know there's a parameter 'secondaryAnimation' which may be useful to achieve it, but after trying to use the code from it, I still cannot animate the old page, all transitions have happened to the new page (the 'child' widget).
Can I get an 'old page' instance from the buildTransition method for animating? Or is there a better way to animate the old page?
Thanks!
I think that secondaryAnimation is used when transitioning to another page. So for your initial route, you'd have to specify the animation of it going away using secondaryAnimation and on your second page you use animation to animate how it appears.
It's a bit awkward that you have to use secondaryAnimation when creating the first route, because it means that it will be used for any transition away from that route. So, with PageRouteBuilder, you can't, for example, let the old page slide to the left when transitioning to page B but slide up when transitioning to page C.
I wrote two classes to achieve animating the old page.
PageSwitcherBuilder is a widget builder to animate old page.
PageSwitcherRoute is a route class to navigate new page.
Examples on DartPad
Here is my script.
class PageSwitcherBuilder extends StatefulWidget {
const PageSwitcherBuilder(
{Key? key,
required this.builder,
this.duration = const Duration(milliseconds: 500),
this.reverseDuration = const Duration(milliseconds: 500)})
: super(key: key);
final Duration duration;
final Duration reverseDuration;
final Widget Function(AnimationController controller) builder;
#override
State<PageSwitcherBuilder> createState() => _PageSwitcherBuilderState();
}
class _PageSwitcherBuilderState extends State<PageSwitcherBuilder>
with TickerProviderStateMixin {
late final AnimationController _controller = AnimationController(
vsync: this,
duration: widget.duration,
reverseDuration: widget.reverseDuration,
);
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return widget.builder(_controller);
}
}
class PageSwitcherRoute extends PageRouteBuilder {
PageSwitcherRoute({
required this.controller,
required Widget page,
}) : super(pageBuilder: (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
return page;
}) {
willDisposeAnimationController = false;
}
#override
final AnimationController controller;
#override
AnimationController createAnimationController() {
return controller;
}
}
You may also check this ZoomPageTransitionsBuilder class to change default transitions.
Workaround that I've come up with is to observe pushes and pops of routes and animte during these.
Create a Route Wrapper using RouteObserver
Run animation on didPushNext
Reverse animation on didPopNext
Wrap all screens that you want to animate on exit and on reenter.
import 'package:flutter/material.dart';
class ScreenSlideTransition extends StatefulWidget {
const ScreenSlideTransition({super.key, required this.child});
final Widget child;
#override
State<ScreenSlideTransition> createState() => _ScreenSlideTransitionState();
}
class _ScreenSlideTransitionState extends State<ScreenSlideTransition>
with RouteAware, TickerProviderStateMixin {
late final AnimationController _controller;
#override
Widget build(BuildContext context) {
final screenWidth = -MediaQuery.of(context).size.width;
return AnimatedBuilder( // Whatever animation you need goes here
animation: _controller,
builder: (BuildContext context, Widget? child) => Transform.translate(
offset: Offset(screenWidth * _controller.value, 0),
child: widget.child));
}
#override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this, duration: const Duration(milliseconds: 1000));
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
routeObserver.subscribe(this, ModalRoute.of(context)!);
}
#override
void dispose() {
_controller.dispose();
routeObserver.unsubscribe(this);
super.dispose();
}
#override
void didPopNext() {
_controller.reverse();
super.didPopNext();
}
#override
void didPushNext() {
_controller.forward();
super.didPushNext();
}
}
In order to make the above code work make sure to initialise RouteObserver (see docs)
final RouteObserver<ModalRoute<void>> routeObserver = RouteObserver<ModalRoute<void>>();
void main() {
runApp(MaterialApp(
home: Container(),
navigatorObservers: <RouteObserver<ModalRoute<void>>>[ routeObserver ],
));
}
Remember to set the same Duration for AnimationController in ScreenSlideTransition and duration for transitionsBuilder if you want to synchronise in/out animation.