I'm using OpenContainer animation to open a screen that could display alert dialog upon the opening of the screen - the case of the item the screen is trying to display is no longer valid or deleted.
Because OpenContainer renders the screen during the animation, the alert dialog is displayed several times.
My attempt to address the issue was to modify the OpenContainer buildPage method to return animation status to openBuilder callback. Is there better way to do without modifying OpenContainer code?
child: AnimatedBuilder(
animation: animation,
builder: (BuildContext context, Widget child) {
if (animation.isCompleted) {
return SizedBox.expand(
child: Material(
color: openColor,
elevation: openElevation,
shape: openShape,
child: Builder(
key: _openBuilderKey,
builder: (BuildContext context) {
return openBuilder(context, closeContainer, false); // added false
},
),
),
);
}
Code to reproduce the issue - https://gist.github.com/MartinJLee/0992a986ad641ef5b4f477fb1ce69249
You can add a listener to your AnimationController something like this
Consider you have an AnimationController like this -
AnimationController _animationController = AnimationController(
vsync: this,
duration: Duration(seconds: 5),
);
Then you can add a Status Listener to this _animationController using the addStatusListener method, something like this -
_animationController.addStatusListener((status) {
if(status == AnimationStatus.completed){
//Do something here
}
});
This listener will be called everytime the animation state changes.
Hope this helps!
I was able to do using the following code on the container for openBuilder.
void initState() {
super.initState();
WidgetsBinding.instance
.addPostFrameCallback((_) => yourFunction(context));
}
see original answers - Flutter: Run method on Widget build complete
Related
Currently i am working on music app and according to my ui i have to display download, downloading progress and downloaded status shown inside popup menu item.But according to Popup menu button widget behaviour, it is dispose and unmounted.So when i closed popup menu item and again open the last status always display download instead of downloading.So it is possible to prevent popup menu button after close.
I tried callback functions, provider, getx, auto keep alive and also stateful builder but it is not working.
I am using ValueNotifier to preserve the download progress. To preserve the state you can follow this structure and use state-management property like riverpod/bloc
class DTest extends StatefulWidget {
const DTest({super.key});
#override
State<DTest> createState() => _DTestState();
}
class _DTestState extends State<DTest> {
/// some state-management , also can be add a listener
ValueNotifier<double?> downloadProgress = ValueNotifier(null);
Timer? timer;
_startDownload() {
timer ??= Timer.periodic(
Duration(milliseconds: 10),
(timer) {
downloadProgress.value = (downloadProgress.value ?? 0) + .01;
if (downloadProgress.value! > 1) timer.cancel();
},
);
}
#override
void dispose() {
timer?.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
PopupMenuButton(
itemBuilder: (context) {
return [
PopupMenuItem(
child: ValueListenableBuilder(
valueListenable: downloadProgress,
builder: (context, value, child) => InkWell(
onTap: value == null ? _startDownload : null,
child: Text("${value ?? "Download"}")),
),
)
];
},
)
],
),
);
}
}
I use navigator.push to go to NewPage() from oldPage() onpress event:
Navigator.of(context).push(
PageRouteBuilder(
opaque: false,
pageBuilder: (context, animation, _) {
return NewPage();
}),
);
The NewPage code: (also the tween animation)
class NewPage extends StatefulWidget {
#override
_NewPageState createState() => _NewPageState();
}
class _NewPageState extends State<NewPage> {
Widget build(BuildContext context) {
return TweenAnimationBuilder(
tween: Tween(begin: 0.0, end: 1.0),
duration: Duration(milliseconds: 1500),
child: Scaffold(...),
builder: (context, value, child) {
return ShaderMask(...) }
It works fine. The animation keeps the old page in the background while the NewPage takes over. However, sometimes I need to use pushReplacement to get rid of the oldpage. When I use pushReplacement, the old page is removed immediately and can't be seen during the animation. how can I make it to remove the old page only after the animation is done?
should I let it go through with push, then remove the old one manually in initState() of the NewPage()?
You need to use your own PageRouteBuilder for such Animations. The following article describes the usage of it:
https://flutter.dev/docs/cookbook/animation/page-route-animation
I am trying to do a glow effect on the container. I am using streambuilder to build list items and when item price changes I want to show container glow effect. So far I have done this but the widget doesn't show the effect at all. didUpdateWidget doesn't show any update and when it does, all of the listview items animates themselves for infinite amount of time. Is there something I am missing?
AnimationController _animationController;
#override
void initState() {
super.initState();
_animationController = new AnimationController(
vsync: this, duration: new Duration(milliseconds: 500));
}
#override
void didUpdateWidget(CustomContainer oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.price != widget.price) {
print("changed");
_animationController.repeat(
reverse: true, period: Duration(milliseconds: 1000));
}
}
#override
void dispose() {
_animationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationController,
builder: (context, _) {
return Container(
height:100,
width:double.infinity,
decoration: BoxDecoration(
color: Theme.of(context).canvasColor,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
blurRadius: 4 * _animationController.value,
color: primaryColor.withOpacity(0.7),
),
],
),
);
});
AnimationController.repeat() will start running the animation and repeat it an infinite amount of times. This is why the items are animating themselves for infinitely. You would have to use .forward method instead:
_animationController = AnimationController(
duration: Duration(milliseconds: 500),
vsync: this);
_animationController.forward(); //tell the animation to start without repeating
From Flutter Docs:
AnimationController is a special Animation object that generates a new
value whenever the hardware is ready for a new frame. By default, an
AnimationController linearly produces the numbers from 0.0 to 1.0
during a given duration.
AnimationController derives from Animation, so it can be used
wherever an Animation object is needed. However, the
AnimationController has additional methods to control the animation.
For example, you start an animation with the .forward() method. The
generation of numbers is tied to the screen refresh, so typically 60
numbers are generated per second. After each number is generated, each
Animation object calls the attached Listener objects. To create a
custom display list for each child, see RepaintBoundary.
Here is the docs for AnimationController.forward(): https://api.flutter.dev/flutter/animation/AnimationController/forward.html
im new to bloc pattern in flutter.
One of my states classes have a list of widget and an index as a field. My goal is to update the child of an Animated Switcher using this state's widgets.
return AnimatedSwitcher(
duration: Duration(milliseconds: 500),
child: BlocBuilder<WelcomeBloc, WelcomeBlocState>(
builder: (context, state) {
if(state is MyState)
return state.widgetList[state.index];
else return Container();
},
),
);
I have also tried the other way around, returning the animated switcher in the bloc builder and the result is the same
When yield is called, the widget is changed but without any animation.
What am I missing?
A child widget of AnimatedSwitcher has to change:
return BlocBuilder<WelcomeBloc, WelcomeBlocState>(
builder: (context, state) {
return AnimatedSwitcher(
duration: Duration(milliseconds: 500),
child: state is MyState ? state.widgetList[state.index] : Container(key: Key('key2')),
);
},
);
And don't forget to set different keys for child widgets.
I'm trying to re-play a Flare animation in Flutter. Not loop the animation when it has complete. I want an animation to be played on demand, the same animation.
When I switch between animations it works fine when just swapping the string and calling setState. Is there a simple way to do this.
Here's what I'm currently doing.
class _FlareDemoState extends State<FlareDemo> {
String animationToPlay = 'activate';
#override
Widget build(BuildContext context) {
print('Animation to play: $animationToPlay');
return Scaffold(
backgroundColor: Colors.purple,
body: GestureDetector(
onTap: () {
setState(() {
});
},
child: FlareActor('assets/button-animation.flr',
animation: animationToPlay)));
}
}
My logs result when I tap the animation
I/flutter (18959): Animation to play: activate
I/flutter (18959): Animation to play: activate
I/chatty (18959): uid=10088(com.example.flare_tutorial) Thread-2 identical 2 lines
I/flutter (18959): Animation to play: activate
I/flutter (18959): Animation to play: activate
I/chatty (18959): uid=10088(com.example.flare_tutorial) Thread-2 identical 7 lines
I/flutter (18959): Animation to play: activate
I/flutter (18959): Animation to play: activate
Reloaded 2 of 495 libraries in 672ms.
I/flutter (18959): Animation to play: activate
Everything is called, it plays the first time, but after that the animation doesn't replay.
A cleaner way to do this is using a custom FlareController. There's a concrete FlareControls implementation that fits this use case nicely.
class _MyHomePageState extends State<MyHomePage> {
// Store a reference to some controls for the Flare widget
final FlareControls controls = FlareControls();
void _playSuccessAnimation() {
// Use the controls to trigger an animation.
controls.play("success");
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: FlareActor("assets/Teddy.flr",
animation: "idle",
fit: BoxFit.contain,
alignment: Alignment.center,
// Make sure you use the controls with the Flare Actor widget.
controller: controls),
floatingActionButton: FloatingActionButton(
onPressed: _playSuccessAnimation,
tooltip: 'Play',
child: Icon(Icons.play_arrow),
),
);
}
}
Note that this example also plays an idle background animation which loops. Any call to FlareControls.play will mix the incoming animation over this background idle animation. You simply omit the animation: "idle" argument if you don't want/need a background animation.
Full example here: https://github.com/luigi-rosso/flare_controls
Based on the answer from #Eugene I came up with a temporary solution. I set the value to empty, start a timer for 50ms and then set the value back to the animation I wanted to play again.
class _FlareDemoState extends State<FlareDemo> {
String animationToPlay = 'activate';
#override
Widget build(BuildContext context) {
print('Animation to play: $animationToPlay');
return Scaffold(
backgroundColor: Colors.purple,
body: GestureDetector(
onTap: () {
_setAnimationToPlay('activate');
},
child: FlareActor('assets/button-animation.flr',
animation: animationToPlay)));
}
}
void _setAnimationToPlay(String animation) {
if (animation == _animationToPlay) {
_animationToPlay = '';
Timer(const Duration(milliseconds: 50), () {
setState(() {
_animationToPlay = animation;
});
});
} else {
_animationToPlay = animation;
}
}
It's messy workaround but it gets the job done. Thank you #Eugene for planting the seed.
FlareActor(
"assets/animation/day_night.flr",
alignment: Alignment.center,
fit: BoxFit.cover,
animation: _animation,
callback: (string) {
if(string=="switch_night"){
setState(() {
_animation = _animationNight;
});
}else if(string=="switch_day"){
setState(() {
_animation = _animationDay;
});
}
},
)