How to run a function when route changes? - flutter

I have made a circular menu widget that opens to show the menu options, when the user clicks on it, it goes to another screen, but if the user press the back button on android it goes back to the menu screen with the circular menu still open(also the distance of the submenus to the main circle increase):
(I cropped the video just to show the button, the downward pointing triangle is part of the next screen)
I want to close the circular menu whenever the user selects any option in the submenu, so basically I want to run the close() function before changing routes, or any other way of making the button close
import 'package:flutter/material.dart';
import 'package:frontend/utils/degrees_to_radians.dart';
import 'package:frontend/utils/indexed_iterables.dart';
class CircularMenu extends StatefulWidget {
final Widget menuIcon;
final List<Widget>? children;
CircularMenu({Key? key, this.children, required this.menuIcon})
: super(key: key);
#override
_CircularMenuState createState() => _CircularMenuState();
}
class _CircularMenuState extends State<CircularMenu>
with TickerProviderStateMixin {
bool _isOpen = false;
bool _isAnimating = false;
late AnimationController _animationController;
late AnimationController _subMenuPositionController;
late Animation<double> _sizeAnimation;
late Animation<double> _subMenuPositionAnimation;
void open() {
setState(() {
_isOpen = true;
_animationController.forward();
_subMenuPositionController.forward();
});
}
#override
void initState() {
_animationController =
AnimationController(duration: Duration(milliseconds: 400), vsync: this);
_subMenuPositionController =
AnimationController(duration: Duration(milliseconds: 500), vsync: this);
_sizeAnimation = TweenSequence([
TweenSequenceItem<double>(
tween: Tween<double>(begin: 1, end: 0.4)
.chain(CurveTween(curve: Curves.easeInOut)),
weight: 50),
TweenSequenceItem<double>(
tween: Tween<double>(begin: 0.4, end: 0.9)
.chain(CurveTween(curve: Curves.easeInOut)),
weight: 50),
TweenSequenceItem<double>(
tween: Tween<double>(begin: 0.9, end: 0.3)
.chain(CurveTween(curve: Curves.easeInOut)),
weight: 50),
]).animate(_animationController);
/* ..addStatusListener((status) {
if (status == AnimationStatus.completed) {
_subMenuPositionController.forward();
}
});
*/
_subMenuPositionAnimation = Tween<double>(begin: 0, end: 75)
.chain(CurveTween(curve: Curves.bounceIn))
.animate(_subMenuPositionController);
super.initState();
}
void close() {
setState(() {
_isOpen = false;
_animationController.reverse();
_subMenuPositionController.reverse();
});
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationController,
builder: (context, _) {
return Container(
child: Stack(
alignment: Alignment.center,
children: [
if(widget.children != null)
..._renderSubMenu(
widget.children!, _subMenuPositionAnimation.value),
//Center Button
FractionallySizedBox(
widthFactor: _sizeAnimation.value,
heightFactor: _sizeAnimation.value,
child: Container(
child: RawMaterialButton(
fillColor: Colors.blue,
shape: CircleBorder(),
onPressed: () {
if (_isAnimating) return;
if (_isOpen) {
close();
} else {
open();
}
},
child: Center(child: _isOpen ? Icon(Icons.close) : widget.menuIcon),
),
),
),
],
),
);
});
}
List<Widget> _renderSubMenu(List<Widget> children, double distance) {
List<Widget> _subMenu = [];
double _angleRatio = 360 / children.length;
children.forEachIndexed((child, index) {
_subMenu.add(
Transform.translate(
offset: Offset.fromDirection(
convertDegreesToRadians(index * _angleRatio), distance),
child: GestureDetector(
child: child)),
);
});
return _subMenu;
}
}

you should just pass close callback to submenu elements and call it after navigator push, without awaiting push completion. though you should change children list to builder one, so you can adjust callback to specific button.
also you should mutate state first and after check if widget is mounted before setting its state, smth like this:
void close() {
_isOpen = false;
_animationController.reverse();
_subMenuPositionController.reverse();
if (mounted) {
setState(() {});
}
}

Related

Flutter how to make FAB invisible when scrolling down and visible when scrolling up

I have a floating action button at the right bottom of a SingleChildScrollView that I will like to disappear when scrolling down and appear when scrolling up like the attached gif file :
My code is below and will appear with any suggestion
final dataKey = new GlobalKey();
initState(){
super.initState();
_isVisible = true;
_hideButtonController = new ScrollController();
_hideButtonController.addListener((){
if(_hideButtonController.position.userScrollDirection == ScrollDirection.reverse){
if(_isVisible == true) {
/* only set when the previous state is false
* Less widget rebuilds
*/
print("**** ${_isVisible} up"); //Move IO away from setState
setState((){
_isVisible = false;
});
}
} else {
if(_hideButtonController.position.userScrollDirection == ScrollDirection.forward){
if(_isVisible == false) {
/* only set when the previous state is false
* Less widget rebuilds
*/
print("**** ${_isVisible} down"); //Move IO away from setState
setState((){
_isVisible = true;
});
}
}
}});
}
floatingActionButton: new Visibility(
visible: _isVisible,
child: new FloatingActionButton(backgroundColor: colorBlue,
onPressed: () => Scrollable.ensureVisible(dataKey.currentContext,duration: Duration(seconds: 1)),
child: Icon(Icons.arrow_upward),)),
body:SingleChildScrollView(
key: dataKey,
physics: BouncingScrollPhysics(),
controller: _hideButtonController,
)
As you can see that I have key: dataKey, that simply automatically scrolls to the top of the page when click you can see I tried using Visibility but it didn't work for me, and not sure what I did wrong but I will like the FAB to appear and disappear as shown in the attached GIF. Thanks in advance.
About the fab animation, you can use SlideTransition
Run on dartPad
class FabAnimation extends StatefulWidget {
const FabAnimation({Key? key}) : super(key: key);
#override
State<FabAnimation> createState() => _FabAnimationState();
}
class _FabAnimationState extends State<FabAnimation>
with SingleTickerProviderStateMixin {
late ScrollController _hideButtonController;
late final AnimationController _controller = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
)
..addListener(() {
setState(() {});
})
..forward();//first time load
late final Animation<Offset> _offsetAnimation = Tween<Offset>(
end: Offset.zero,
begin: const Offset(0, 2.0),
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.linear,
));
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
initState() {
super.initState();
_hideButtonController = ScrollController();
_hideButtonController.addListener(() {
//add more logic for your case
if (_hideButtonController.position.userScrollDirection ==
ScrollDirection.reverse) {
if (_offsetAnimation.isCompleted) _controller.reverse();
}
if (_hideButtonController.position.userScrollDirection ==
ScrollDirection.forward) {
_controller.forward();
}
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: SlideTransition(
position: _offsetAnimation,
child: FloatingActionButton(
backgroundColor: Colors.blue,
onPressed: () {},
child: Icon(Icons.arrow_upward),
)),
body: SingleChildScrollView(
physics: BouncingScrollPhysics(),
controller: _hideButtonController,
child: Column(
children: List.generate(
33,
(index) => ListTile(
tileColor: index.isEven ? Colors.deepPurple : Colors.blue,
),
),
),
),
);
}
}

Flutter staggered animation on same property

I want to animate the scale of a widget.
On tap down it should scale down and on tap up, scale up.
Problem is how can I use staggered animations on the same property?
There is an older question here on SO (Chaining seperate animations that work on the same properties) but it has no working answers.
Ive tried to adapt its solution but the problem is each animation listener is called even if the specified Interval has not been reached.
Ive added a condition which checks if _animationFuture is set but with this pattern you have to keep a value for any running animation you have in your widget which is a bit cumbersome.
Is there a "native" solution where you dont need a work around or are multiple animations on same value just not supported?
I do not want to reverse() the animation. Each tween must be able to have a custom curve, begin and end values.
Heres my widget:
class AnimatedIconButton extends StatefulWidget {
final Widget child;
final Function onPress;
AnimatedIconButton({#required this.child, this.onPress}): assert(child != null);
#override
_AnimatedIconButtonState createState() => _AnimatedIconButtonState();
}
class _AnimatedIconButtonState extends State<AnimatedIconButton> with SingleTickerProviderStateMixin {
AnimationController _animationController;
Animation<double> _animationIn;
Animation<double> _animationOut;
double _scale = 1;
TickerFuture _animationFuture;
Function get onPress => widget.onPress ?? () => null;
_tapDown(TapDownDetails details) {
print("_tapDown progress=${_animationController.value} scale=$_scale");
_animationFuture = _animationController.animateTo(0.5);
}
_tapUp(TapUpDetails details) {
assert(_animationFuture != null);
print("_tapUp progress=${_animationController.value} scale=$_scale");
_animationFuture.then((_) {
_animationFuture = null;
_animationController.animateTo(1);
});
}
#override
void initState() {
super.initState();
_animationController = AnimationController(value: 0, vsync: this, duration: const Duration(milliseconds: 1500), animationBehavior: AnimationBehavior.preserve);
_animationIn = Tween<double>(begin: 1.0, end: 0.9).animate(CurvedAnimation(
parent: _animationController,
curve: Interval(
0, 0.5, curve: Curves.easeOut,
),
))..addListener(() {
if (_animationFuture == null) return;
print("_animationIn.value ${_animationOut.value}");
_scale = _animationIn.value;
});
_animationOut = Tween<double>(begin: 0.9, end: 1).animate(CurvedAnimation(
parent: _animationController,
curve: Interval(
0.5, 1, curve: Curves.elasticOut,
),
))..addListener(() {
if (_animationFuture != null) return;
print("_animationOut.value ${_animationOut.value}");
_scale = _animationOut.value;
});
}
#override
void dispose() {
_animationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTapUp: _tapUp,
onTapDown: _tapDown,
child: AnimatedBuilder(
animation: _animationController,
child: widget.child,
builder: (_, child) {
return Transform.scale(
scale: _scale,
transformHitTests: false,
child: child,
);
},
),
);
}
}
You can use a TweenSequence to stagger multiple animations behind each other. Borrowing the example from the documentation and changing it a bit, here's a copy-paste widget that animates a blue container from 100x100 to 200x200, pauses for a bit, and animates it back:
class _StaggeredState extends State<Staggered>
with SingleTickerProviderStateMixin {
/// Controller managing the animation
late final AnimationController controller;
/// The aninmation of our TweenSequence
late final Animation<double> animation;
/// An Animatable consisting of a series of tweens
final Animatable<double> tweenSequence = TweenSequence<double>(
<TweenSequenceItem<double>>[
// Animate from .5 to 1 in the first 40/80th of this animation
TweenSequenceItem<double>(
tween: Tween<double>(begin: 100, end: 200)
.chain(CurveTween(curve: Curves.ease)),
weight: 40.0,
),
// Maintain still at 1.0 for 20/80th
TweenSequenceItem<double>(
tween: ConstantTween<double>(200),
weight: 20.0,
),
// Animate back from 1 to 0.5 for the last 40/80th
TweenSequenceItem<double>(
tween: Tween<double>(begin: 200, end: 100)
.chain(CurveTween(curve: Curves.ease)),
weight: 40.0,
),
],
);
#override
void initState() {
super.initState();
controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
);
// Animate our TweenSequence using our controller
animation = tweenSequence.animate(controller);
// Add a listener that calls [setState] so our widget rebuilds
// when the animation value changes.
controller.addListener(() => setState(() {}));
// Set the controller to repeat indefinitely
controller.repeat();
}
#override
void dispose() {
super.dispose();
controller.dispose();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Container(
width: animation.value,
height: animation.value,
color: Colors.blue,
),
),
),
);
}
}

Flutter image animate bigger and smaller

I want to make a simple recurring animation to an image. It should keep animating until the user closes the screen. What I want it that the image slowly gets bigger and then smaller.
I've checked the animatedContainer, but that doesn't seem to do this dynamically. Here is the code I used with AnimatedContainer:
#override
void didChangeDependencies() {
increaseSize(widget.seconds);
super.didChangeDependencies();
}
void increaseSize(int toSize) {
for (i = 0; i > toSize; i++) {
setState(() {
_width += i;
_height += i;
});
}
}
#override
Widget build(BuildContext context) {
return AnimatedContainer(
// Use the properties stored in the State class.
width: _width,
height: _height,
decoration: BoxDecoration(
color: _color,
borderRadius: _borderRadius,
),
// Define how long the animation should take.
duration: Duration(seconds: 30),
// Provide an optional curve to make the animation feel smoother.
curve: Curves.easeInOutBack,
);
;
}
Has anyone come across this before, any help would be appreciated.
Many thanks
Try something like this:
It's an example of a container which grows and shrinks from 50 to 100 and back to 50.
class SizeWidget extends StatefulWidget {
final int seconds;
const SizeWidget({this.seconds = 3});
#override
_SizeWidgetState createState() => _SizeWidgetState();
}
class _SizeWidgetState extends State<SizeWidget> with TickerProviderStateMixin {
AnimationController _animationController;
Animation _sizeAnimation;
bool reverse = false;
#override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this, duration: Duration(seconds: widget.seconds))
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
_animationController.repeat(reverse: !reverse);
reverse = !reverse;
}
});
_sizeAnimation =
Tween<double>(begin: 50.0, end: 100.0).animate(_animationController);
_animationController.forward();
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _sizeAnimation,
builder: (context, child) => Container(
width: _sizeAnimation.value,
height: _sizeAnimation.value,
color: Colors.red,
),
);
}
}
Another option would be to use a TweenSequence and tell the animation controller to repeat itself:
class SizeWidget extends StatefulWidget {
final int seconds;
const SizeWidget({this.seconds = 3});
#override
_SizeWidgetState createState() => _SizeWidgetState();
}
class _SizeWidgetState extends State<SizeWidget> with TickerProviderStateMixin {
AnimationController _animationController;
Animation _sizeAnimation;
#override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this, duration: Duration(seconds: widget.seconds));
_sizeAnimation = TweenSequence<double>(
[
TweenSequenceItem<double>(
tween: Tween<double>(
begin: 50,
end: 100,
),
weight: 50,
),
TweenSequenceItem<double>(
tween: Tween<double>(
begin: 100,
end: 50,
),
weight: 50,
),
],
).animate(_animationController);
_animationController.repeat(reverse: true);
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _sizeAnimation,
builder: (context, child) => Container(
width: _sizeAnimation.value,
height: _sizeAnimation.value,
color: Colors.red,
),
);
}
}

Rotating container indefinitely

I would like to rotate an Image indefinitely.
This container is one of the widget within the stack and would like this to be rotating continuously non stop.
final AnimationController animation = AnimationController(
duration: const Duration(milliseconds: 1800),
vsync: const NonStopVSync(),
)..repeat();
final Tween tween = Tween(begin: 0.0, end: math.pi);
var square = Container(
width: 100,
height: 100,
transform: Matrix4.identity(),
color: Colors.amber,
);
...
class Foo extends State<Bar> {
...
animation.addListener((){
square.transform = Matrix4.rotationZ(tween.evaluate(animation));
});
Widget build(BuildContext context) {
return Stack(
children: [
...
Center(
child: square
)
]
)
}
}
and I get this error: 'transform' can't be used as a setter because it's final. (assignment_to_final at [digital_clock] lib/digital_clock.dart:139)
How would I do what I'm trying to do?
Try something like this:
class InfiniteAnimation extends StatefulWidget {
final Widget child;
final int durationInSeconds;
InfiniteAnimation({#required this.child, this.durationInSeconds = 2,});
#override
_InfiniteAnimationState createState() => _InfiniteAnimationState();
}
class _InfiniteAnimationState extends State<InfiniteAnimation>
with SingleTickerProviderStateMixin {
AnimationController animationController;
Animation<double> animation;
​
#override
void initState() {
super.initState();
animationController = AnimationController(
vsync: this,
duration: Duration(seconds: widget.durationInSeconds),
);
animation = Tween<double>(
begin: 0,
end: 12.5664, // 2Radians (360 degrees)
).animate(animationController);
​
animationController.forward();
​
animation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
animationController.repeat();
}
});
}
​
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: animationController,
builder: (context, child) => Transform.rotate(
angle: animation.value,
child: widget.child,
),
);
}
#override
void dispose() {
animationController?.dispose();
super.dispose();
}
}
You basically need to create a StatefulWidget that mixes in (with keyword) the SingleTickerProviderStateMixin, provide an AnimationController, start the animation, then when the animation completes, repeat.
The AnimationBuilder is a better way of telling the widget to update on every frame without having to listen to the animationController and call setState explicitly.
You can use it like this:
InfiniteAnimation(
durationInSeconds: 2, // this is the default value
child: Icon(
Icons.expand_more,
size: 50.0,
color: Colors.white,
),
)

How do i access an inner widget's animation controller (or state) from outer widget?

So I’ve been experimenting with Flutter & Dart for the past few days.
I’m stuck on this one for over a day now, so I’m here.
So I have the AlarmScreen, and we have 2 objects inside it. One is the DraggableMoonWidget, and the other is the RisingSunWidget.
Currently, the RisingSunWidget animates onto the screen from the bottom, while the DraggableMoonWidget is draggable by touch.
What I want to achieve, is that when the RisingSunWidget’s animation would stop and change when the DraggableMoonWidget is being dragged. So I have the MoonDragListener in place and working, but I still can’t figure it out. (currently calls the listener back to the AlarmScreen, but then what?)
I have tried a whole bunch of methods to do that, all deleted since then, as not a single one worked.
TLDR
How do I control the RisingSunWidget’s animation controller when the user touches the DraggableMoonWidget, for example, I want to stop the controller and animate it to a different point.
What is the best approach in dart/flutter?
AlarmScreen
import 'package:flutter/widgets.dart';
import 'package:moonworshiper_app/backgrounds.dart';
import 'package:moonworshiper_app/ui/alarm/moon_draggable.dart';
import 'package:moonworshiper_app/ui/alarm/rising_sun.dart';
class AlarmScreen extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return new _AlarmScreenState();
}
}
class _AlarmScreenState extends State<AlarmScreen> {
bool _moonWasTouched = false;
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return new Stack(
children: <Widget>[
new DraggableMoonWidget(new MoonDragListener(this)),
new LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return new RisingSunWidget(constraints.heightConstraints().maxHeight, _moonWasTouched);
})
],
);
}
void _refreshSun() {
setState(() {
_moonWasTouched = true;
});
}
}
class MoonDragListener {
_AlarmScreenState state;
MoonDragListener(this.state);
void onMoonDragStarted() {
state._refreshSun();
}
}
DraggableMoonWidget
import 'package:flutter/widgets.dart';
import 'package:moonworshiper_app/ui/alarm/alarm_screen.dart';
class DraggableMoonWidget extends StatefulWidget {
final MoonDragListener moonStartListener;
DraggableMoonWidget(this.moonStartListener);
State<StatefulWidget> createState() => new _DraggableMoonState();
}
class _DraggableMoonState extends State<DraggableMoonWidget>
with TickerProviderStateMixin {
final moonDragTween = new Tween<Offset>(
begin: new Offset(0.0, -0.5),
end: new Offset(0.0, 0.5),
);
var moonAnimListener;
AnimationController _animationController;
Animation<Offset> _dragAnimation;
AnimationController _dragAnimationController;
bool isFirstDraw = true;
#override
initState() {
super.initState();
_animationController = new AnimationController(
vsync: this,
duration: const Duration(milliseconds: 3000),
);
_dragAnimationController = new AnimationController(vsync: this);
moonAnimListener = (AnimationStatus status) {
if (status == AnimationStatus.dismissed) {
_animationController.forward();
} else if (status == AnimationStatus.completed) {
_animationController.reverse();
} else if (status == AnimationStatus.forward) {}
};
_dragAnimation = moonDragTween.animate(new CurvedAnimation(
parent: _dragAnimationController,
curve: Curves.easeInOut,
reverseCurve: Curves.easeInOut));
_dragAnimationController.animateTo(0.5, duration: new Duration());
_animationController.addStatusListener(moonAnimListener);
_animationController.forward();
}
#override
Widget build(BuildContext context) {
return new Container(
child: new Center(
child: new SlideTransition(
position: _dragAnimation,
child: new GestureDetector(
child: new Image.asset(
"assets/moon.png",
width: 280.0,
height: 280.0,
),
onVerticalDragStart: (DragStartDetails details) {
print("start:" + details.globalPosition.toString());
_animationController.removeStatusListener(moonAnimListener);
_animationController.stop();
_dragStartDetails = details;
_dragAnimationController.animateTo(0.5,
duration: new Duration(milliseconds: 50));
if (isFirstDraw) {
isFirstDraw = false;
widget.moonStartListener.onMoonDragStarted();
}
},
),
),
),
// margin: new EdgeInsets.only(top: 48.0),
);
}
#override
void dispose() {
_animationController.dispose();
super.dispose();
}
}
RisingSunWidget
import 'package:flutter/widgets.dart';
class RisingSunWidget extends StatefulWidget {
// needed to calculate the offset map
final double screenHeight;
// that's how we know if the use touched the moon
final bool moonWasTouched;
RisingSunWidget(this.screenHeight, this.moonWasTouched);
#override
State<StatefulWidget> createState() {
return new RisingSunState();
}
}
class RisingSunState extends State<RisingSunWidget> with TickerProviderStateMixin {
AnimationController _animationController;
Animation<Offset> _sunAnimation;
final double sunSize = 320.0;
#override
initState() {
super.initState();
_animationController = new AnimationController(
vsync: this,
duration: const Duration(milliseconds: 6000),
);
// how many suns fit in the height of our screen
assert(widget.screenHeight > sunSize);
double sunsInHeight = widget.screenHeight / sunSize;
print(sunsInHeight.toString() + " suns could fit on the user's screen");
var sunsPlusMargins = sunsInHeight + 1; // required margins
final moonTween = new Tween<Offset>(
begin: new Offset(0.0, -0.5 * sunsPlusMargins),
end: new Offset(0.0, 0.5 * sunsPlusMargins), //move by 8% of height max
);
_sunAnimation = moonTween.animate(new CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
reverseCurve: Curves.easeInOut,
));
if (widget.moonWasTouched) {
_animationController.stop();
_animationController.animateTo(0.68,
duration: new Duration(milliseconds: 2000));
} else {
_animationController.animateTo(0.88,
duration: new Duration(milliseconds: 0));
_animationController.animateTo(0.75,
duration: new Duration(milliseconds: 15000));
}
}
#override
Widget build(BuildContext context) {
return new Center(
child: new SlideTransition(
position: _sunAnimation,
child: new Image.asset(
"assets/sun.png",
width: sunSize,
height: sunSize,
),
),
);
}
#override
void dispose() {
_animationController.dispose();
super.dispose();
}
}
2 possibilities :
Use the BuildContext context that is provided in your build method. BuildContext has a few methods get the closest parent of a specific type.
Pass a key attribute to the desired widget.
GlobalKey to be exact.
GlobalKey allows you directly access to a Widget or it's state.