My app contains a couple of pages. In the appbar I have a selfmade stateful widget with a badge that shows the number of new messages. When I swipe to refresh data the badge will run a small animation if the badge value is changed.
The problem is that the badge value comes from a scoped model. How do I run the animation from the scoped model class. I tried to let the scoped model class hold the animationController as well as a function. It works on the first and second screen. But when I am navigating back to the first page again and pull to refresh. It is like the animationController is in bad state.
Code in the scoped model:
Function _runNotificationAnimation;
set runNotificationAnimation(Function fun) => _runNotificationAnimation = fun;
void _setNotificationCount(int count) {
_notificationCount = count;
if (count > 0 && _runNotificationAnimation != null) {
_runNotificationAnimation();
}
notifyListeners();
}
function that runs the animation
runAnim() {
setState(() {
controller.reset();
controller.forward(from: 0.0);
});
}
Error from flutter:
[VERBOSE-2:shell.cc(184)] Dart Error: Unhandled exception:
NoSuchMethodError: The method 'stop' was called on null.
Receiver: null
Tried calling: stop(canceled: true)
0 Object.noSuchMethod (dart:core/runtime/libobject_patch.dart:50:5)
1 AnimationController.stop (package:flutter/src/animation/animation_controller.dart:650:13)
2 AnimationController.value= (package:flutter/src/animation/animation_controller.dart:349:5)
3 AnimationController.reset (package:flutter/src/animation/animation_controller.dart:370:5)
4 NotificationIconState.runAnim (package:volvopenta/widgets/notificaton_icon.dart:38:16)
5 SettingsModel._setNotificationCount (package:volvopenta/scoped-models/settings-model.dart:57:7)
6 SettingsModel.updateAppData (package:volvopenta/scoped-models/settings-model.dart:185:5)
7 MyMachines.build... (package:volvopenta/pages/fleet.dart:83:27)
8<…>
Since your animation will be built in a stateful Widget, it is better to leave the animationController in that stateful Widget and move only the animation (Tween) in the model class. It is essential that you place the notifyListener(); in the controller.addListerner() and not at the end of the function.
class MyModel extends Model{
Animation animation;
runAnimation(controller) {
animation = Tween(begin: 0,0, end:
400).animate(controller);
controller.forward();
controller.addListener((){
notifyListeners();
});
}
}
You can call this function in your stateful widget as follows:
class _MyScreenState extends State<MyScreen> with
SingleTickerProviderStateMixin{
AnimationController controller;
MyModel myModel = MyModel();
#overide
void initState(){
super.initState();
controller = AnimationController(duration: Duration(seconds: 2), vsync:
this);
myModel.runAnimation(controller);
}
//dispose here
#override
Widget build(Buildcontext context){
return ScopedModel<MyModel>(
model: myModel,
child: Scaffold(
body: Text("Hello", style: TextStyle(fontSize: 13 *
controller.value)),
),
);
}
}
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 am unable to figure out how I can listen to changes in a class that uses the ChangeNotifier class. More specifically, outside the widget build method. So far I've tried simply attaching listeners to the class like so:
final providerObj = SomeProvider();
void initState() {
providerObj.addListener(() {
print("Something happens");
});
}
But this does nothing. I don't know why. I got it working once, but afterwards it just died on me. Nothing gets triggered, when there is a change in the provider class' variable values.
Here's a snippet of the SomeProvider
class SomeProvider with ChangeNotifier {
int _tabIndex = 0;
List<Tab> _tabs = [];
Tab _searchTab = new Tab(id: 0, name: 'Search');
void searchItems(String searchWord) {
_searchTab.items.addAll(_items
.where((item) => product.item.contains(searchWord))
.toList());
setTabIndex(0);
}
void setTabIndex(int index) {
_tabIndex = index;
notifyListeners();
}
/// Get all products
List<Category> get tabs {
return [..._tabs];
}
int get tabIndex {
return _tabIndex;
}
What I'm trying to accomplish is simple: Control the selected tab, when the selected index is in the SomeProvider class. Every time a tab is pressed, the index value gets saved in the provider. When the value changes, it should trigger an animateTo() call for a TabController instance.
The reason for this is because I have a separate search widget, which needs to change the tab to a Search tab. I just have no idea how I can listen to changes in the provider class outside the build() method and change the TabController index there.
So, I have a basic TabBarView to which I give a TabController instance.
class _ProductsState extends State<Products> with TickerProviderStateMixin {
late TabController _tabController;
void initState() {
super.initState();
_tabController = TabController(
vsync: this,
length: 0,
initialIndex: 0,
);
}
#override
void dispose() {
_tabController.dispose();
super.dispose();
}
Future<void> _fetchTabs(context) async {
await Provider.of<SomeProvider>(context, listen: false)
.fetchTabs();
_tabController = TabController(
vsync: this,
length: Provider.of<SomeProvider>(context, listen: false)
.tabs
.length,
);
}
Widget build(BuildContext context) {
// There's a FutureBuilder here etc.
...
child: TabBarView(
controller: _tabController,
...
What I'm trying to do is something like:
void triggeredWhenStateChanges() {
_tabController.animateTo(Provider.of<SomeProvider>(context, listen: false).tabIndex);
}
Little bit off-topic: I've used React before (not React-Native) and with it you can use something called a hook which can perform side effects when a value change is detected. With the useEffect hook I can normally execute some actions when a value in the state changes. I don't know how I can do this with Flutter, if it can be done at all.
I have a "WidgetBackGround" statefullwidget that return an animated background for my app,
I use it like this :
Scaffold( resizeToAvoidBottomInset: false, body: WidgetBackGround( child: Container(),),)
The problem is when I use navigator to change screen and reuse WidgetBackGround an other instance is created and the animation is not a the same state that previous screen.
I want to have the same animated background on all my app, is it possible to instance it one time and then just reuse it ?
WidgetBackGround.dart look like this:
final Widget child;
WidgetBackGround({this.child = const SizedBox.expand()});
#override
_WidgetBackGroundState createState() => _WidgetBackGroundState();
}
class _WidgetBackGroundState extends State<WidgetBackGround> {
double iter = 0.0;
#override
void initState() {
Future.delayed(Duration(seconds: 1)).then((value) async {
for (int i = 0; i < 2000000; i++) {
setState(() {
iter = iter + 0.000001;
});
await Future.delayed(Duration(milliseconds: 50));
}
});
super.initState();
}
#override
Widget build(BuildContext context) {
return CustomPaint(painter: SpaceBackGround(iter), child: widget.child);
}
}
this is not a solution, but maybe a valid workaround:
try making the iter a static variable,
this of course won't preserve the state of WidgetBackGround but will let the animation continue from its last value in the previous screen
A valid solution (not sure if it's the best out there):
is to use some dependency injection tool (for example get_it) and provide your WidgetBackGround object as a singleton for every scaffold in your app
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));
}
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.