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();
}
Related
I want my linear progress bar to update progressively in stages, each time a user answers a question correctly in a quiz I have made.
I can't seem to get the progress bar to animate upon the value changes, however.
To note:
-My animation runs successfully, if I call my _animationController.forward from initState() (with some test values in the tween).
-I notify my AnimationController to run via notifications triggered using the Provider package as shown in the code below, when a question has been correctly answered, which is working.
-there are two values (original and new) that are input into the animation tween to tell the progress bar how much it should animate each time, are also correctly updating as this log shows.
I/flutter (18664): original value 0.0 and new value 0.0
I/flutter (18664): original value 0.0 and new value 0.25
import 'package:flutter/material.dart';
import 'package:learn_to_read/controllers/answerManager.dart';
import 'package:provider/provider.dart';
class AnswerProgressBar extends StatefulWidget {
const AnswerProgressBar({Key? key}) : super(key: key);
#override
_AnswerProgressBarState createState() => _AnswerProgressBarState();
}
class _AnswerProgressBarState extends State<AnswerProgressBar>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _anim;
late double _originalValue = 0;
late double _newValue = 0;
late int _localPoints;
late double _valueCompleted;
#override
void initState() {
double _originalValue = 0.0;
double _newValue = 0.0;
_animationController = AnimationController(
duration: Duration(milliseconds: 800), vsync: this);
_anim = Tween<double>(begin: _originalValue, end: _newValue)
.animate(_animationController)..addListener(() {
setState(() {
});
});
_localPoints = 0;
_valueCompleted = 0;
super.initState();
}
#override
void didChangeDependencies() {
_originalValue = _newValue;
if (Provider.of<AnswerManager>(context).pageTilesCompleted &&
_valueCompleted != 0) {
_localPoints++;
} else {
_localPoints = Provider.of<AnswerManager>(context).points;
}
_valueCompleted =
_localPoints / Provider.of<AnswerManager>(context).totalTilesPerPage;
_newValue = _valueCompleted;
_animationController.forward();
super.didChangeDependencies();
}
#override
void dispose() {
_animationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
print('original value $_originalValue and new value $_newValue');
return AnimatedBuilder(
animation: _animationController,
builder: (context, i) => LinearProgressIndicator(
value: _anim.value,
backgroundColor: Colors.grey
),
);
}
}
Use set state
This will change the state as user will answer
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
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 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.
How to hot-reload fields of a State subclass in Flutter?
I know that the modifying the initial value of fields isn't taken into account during hot-reload and that I can use hot-restart for them. But this is painfully slow.
Is there any way to ease the process?
A typical use-case would be animations, especially AnimationController. As it is stored inside a State field, but we usually want to iterate over its duration. Example:
class MyAnim extends StatefulWidget {
#override
_MyAnimState createState() => _MyAnimState();
}
class _MyAnimState extends State<MyAnim> with SingleTickerProviderStateMixin {
AnimationController animationController;
#override
void initState() {
animationController =
AnimationController(vsync: this, duration: const Duration(seconds: 1));
super.initState();
}
#override
void dispose() {
animationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Container();
}
}
State provides a custom lifecycle hook for hot-reloads: reassemble
You can freely override that method to have custom hot-reload behaviors. Don't worry, this method will never be called in production.
With a small tweak you'd get the following:
class _MyAnimState extends State<MyAnim> with SingleTickerProviderStateMixin {
AnimationController animationController;
#override
void initState() {
animationController = AnimationController(vsync: this);
_initializeFields();
super.initState();
}
void _initializeFields() {
animationController.duration = const Duration(seconds: 1);
}
#override
void reassemble() {
_initializeFields();
super.reassemble();
}
#override
void dispose() {
animationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Container();
}
}
Now, whenever you modify your State class, it will correctly update AnimationController's duration.