How to animate old page during transition in flutter? - flutter

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.

Related

how to access to ScrollControll of a class from another class in flutter?

Hello i`m new in flutter.
I made a special listView for myself and made a controller for it.
In the main page, I have called my own list view and I have a button that I want to execute the following method by pressing it from the Main class and my list view scrolls inside another class.
scroll method:
void _animateToIndex(int index) {
_controller.animateTo(
index * 50,
duration: Duration(seconds: 1),
curve: Curves.fastOutSlowIn,
);
}
In fact, I want to execute the method of another class that contains my scrollview from the Main class.
my listView class:
class CustomNumberScroller extends StatefulWidget {
const CustomNumberScroller(
{Key? key, required this.curStep, required this.pageNumberOfMainDart})
: super(key: key);
#override
State<CustomNumberScroller> createState() => _CustomNumberScrollerState();
}
final dataKey = new GlobalKey();
final ScrollController _controller = ScrollController();
void _animateToIndex(int index) {
_controller.animateTo(
index * 50,
duration: Duration(seconds: 1),
curve: Curves.fastOutSlowIn,
);
}
class _CustomNumberScrollerState extends State<CustomNumberScroller> {
#override
Widget build(BuildContext context) {
Size media = MediaQuery.of(context).size;
return ...
}
}
you can use state management as provide and put scroll controller in class and access to controller in any place in widget tree
hope that helps you
get started with provider

how to get id sent with a Screen in flutter

i send an idd from another screen when i want to navigate to this WaitingScreen
Now when i want to use the idd , I get an error in the line
FirebaseFirestore.instance.collection('ambulance').doc(idd).get()
class WaitingScreen extends StatefulWidget {
final String idd;
String? Key;
WaitingScreen({required this.idd}) : super();
_WaitingScreenState createState () => _WaitingScreenState( ) ;
}
class _WaitingScreenState extends State<WaitingScreen> with SingleTickerProviderStateMixin{
late AnimationController _controller;
#override
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: Duration(minutes: 5));
_controller.forward();
}
#override
Widget build(BuildContext context) {
String? valuee;
return FutureBuilder<DocumentSnapshot>(
future: FirebaseFirestore.instance.collection('ambulance').doc(idd).get(),
When using a stateful widget, you need to reference variables on the widget itself by using the following construction:
widget.idd
By using the prefix widget., you make sure to access the variables on the widget, and not on the state you are in.

Flutter SizeTransition only at first widget build

I made a widget 'ExpandableSection' which animates the child content by using a SizeTransition. It also works fine so far but I would like to have an additional parameter like "disableAtFirstBuild" so there is no initial animation but the widget is instantly shown. And only on rebuild the animation should be triggered. It seems like an easy task but I searched multiple hours for a solution without luck. For example I tried to set the animation duration to zero at first, invert a state boolean to save the fact that one build is done and afterwards set the duration to the normal value again. But somehow you cannot change an active controller. Is there a way to do it? Maybe it is quite easy and obvious but it is just not clear to me.
Any help would be much appreciated.
import 'package:flutter/widgets.dart';
class ExpandableSection extends StatefulWidget {
final Widget? child;
final bool? expand, useSliverScrollSafeMode;
final Axis axis;
ExpandableSection({this.expand = false, this.useSliverScrollSafeMode = false, this.axis = Axis.vertical, this.child});
#override
_ExpandableSectionState createState() => _ExpandableSectionState();
}
class _ExpandableSectionState extends State<ExpandableSection> with SingleTickerProviderStateMixin {
late AnimationController expandController;
late Animation<double> animation;
#override
void initState() {
super.initState();
prepareAnimations();
_runExpandCheck();
}
void prepareAnimations() {
expandController = AnimationController(vsync: this, duration: const Duration(milliseconds: 650));
animation = CurvedAnimation(
parent: expandController,
curve: Curves.easeInOutQuart,
);
}
void _runExpandCheck() {
if (widget.expand!) {
expandController.forward();
} else {
expandController.reverse();
}
}
#override
void didUpdateWidget(ExpandableSection oldWidget) {
super.didUpdateWidget(oldWidget);
_runExpandCheck();
}
#override
void dispose() {
expandController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
if (widget.useSliverScrollSafeMode! && !expandController.isAnimating && !widget.expand!) {
return SizedBox.shrink();
} else {
return SizeTransition(axisAlignment: -1, axis: widget.axis, sizeFactor: animation, child: Center(child: widget.child));
}
}
}
Add 'value' to your controller while creating it; like this:
expandController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 650),
value: widget.expand ? 650 : 0,
);
Try stoping the animation controller in initState and then calling .forward() whenever you want it to run.
#override
void initState() {
super.initState();
prepareAnimations();
_runExpandCheck();
expandController.stop();
}

Flutter: re-render child widget on parent state change

So a have a stateful parent with a timer value. This timer can be reset by the user. I also have a child stateful widget with an animation controller. The parent passes the timer value to this child to reset the animation. Here is some code:
class _ParentState extends State<Parent> {
int timer = 20;
void _userInput() {
setState(() {
timer = 20;
}
}
#override
Widget build(BuildContext context) {
return Child(
time: timer
);
}
}
Note: this is striped down for simplicity
So when my user triggers the _userInput method, the timer value gets changed but the child doesn't. Here is my full child widget:
class TimerProgress extends StatefulWidget {
TimerProgress({
Key key,
#required this.time,
#required this.onCompleted
}) : super(key: key);
final int time;
final Function onCompleted;
#override
_TimerProgressState createState() => _TimerProgressState();
}
class _TimerProgressState extends State<TimerProgress> with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation<double> _widthAnimation;
#override
void initState() {
_controller = AnimationController(
duration: Duration(seconds: widget.time),
vsync: this
);
_widthAnimation = Tween<double>(
begin: 200,
end: 0
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.linear
));
_controller.addListener(() {
if (_controller.value == 1) {
widget.onCompleted();
}
});
_controller.forward();
super.initState();
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return ...
}
}
So this all works, but ideally I want the animation to be reset, which doesn't work...
initState runs only once per state instance.
Implement resetting the time inside didUpdateWidget: https://api.flutter.dev/flutter/widgets/RawScrollbarState/didUpdateWidget.html
You can't (cause it will cause recursive loop, setState will trigger didUpdateWidget again) and shouldn't (after this method is invoked, build is being called) have to call setState inside this method.
Runnable example: https://www.dartpad.dev/848976603f866f4e3aa6030aba8addf7?null_safety=true

Stateful widget dispose method not called

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.