Repeating animations specific times e.g 20 times by Flutter - flutter

I have a simple animation :
Animation<double> animation;
Tween<double> tween = Tween(begin: 1, end: 1.15);
AnimationController animationController;
#override
void initState() {
super.initState();
animationController =
AnimationController(vsync: this, duration: Duration(milliseconds: 300));
animation = animationController.drive(tween);
}
What i want ?
make this animation repeats e.g 20 times .
What i tried before posted this question ?
1- This method not have a parameter or way that makes me repeats this animation e.g 20 times.
it repeats and repeats and repeats forever :
animationController.repeat();
2- Use simple loops . It hangs my app, it requires to stop entire application and rerun it to solve that hanging . It's seems that loops completely not dealing with futures :
do {
animationController.forward().then((x) {
animationController.reverse().then((x) {
repeats++;
});
});
} while (repeats < 20);
3- Making int variable and add a listener ..etc,it's working , but seems that it's not the best way to do that matter :
int repeats = 0;
animation.addStatusListener((status) {
if (repeats < 20) {
if (status == AnimationStatus.completed) {
animationController.reverse();
} else if (status == AnimationStatus.dismissed) {
animationController.forward();
}
repeats++;
} });
4- make chain of then . **No comment on it ** 😂😂😂 :
animationController.forward().then((x) {
animationController.reverse().then((x) {
animationController.forward().then((x) {
animationController.reverse().then((x) {
animationController.forward().then((x) {
animationController.reverse();
});
});
});
});
});
Now , shortly how can i repeat animation 20 times

You can try it:
5 - Use TickerFuture return from repeat, set timeout then stop animation.
AnimationController _animationController;
#override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: Duration(seconds: 3),
);
_animationController.addListener(() => setState(() {}));
TickerFuture tickerFuture = _animationController.repeat();
tickerFuture.timeout(Duration(seconds: 3 * 10), onTimeout: () {
_animationController.forward(from: 0);
_animationController.stop(canceled: true);
});
}

This can be easily done without the timer with extension method:
extension on AnimationController {
void repeatEx({#required int times}) {
var count = 0;
addStatusListener((status) {
if (status == AnimationStatus.completed) {
if (++count < times) {
reverse();
}
} else if (status == AnimationStatus.dismissed) {
forward();
}
});
}
}
And you could use it like this:
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
)
..repeatEx(times: 10)
..forward();

If you have a Widget with a playing argument that can set to true or false dynamically (to start and stop animation dynamically), you can improve behaviour by using a cancellable Timer() instead of Future.timeout :
void updatePlaying() {
if (widget.playing != false && !_controller.isAnimating && !_loopHasCompleted) {
_controller.repeat();
_timeout?.cancel();
_timeout = Timer(widget.duration * widget.loops, () {
_controller.reset();
_loopHasCompleted = true;
});
} else if (widget.playing == false) {
if (_controller.isAnimating)
_controller.reset();
_loopHasCompleted = false;
}
}

AnimationController(
lowerBound: 0.0,
upperBound: 20.0,
vsync: this, duration: Duration(milliseconds: 300))

It's easy to use just like below
var counter = 0;
int repeatTimes = 10 // how many times you want to repeat animation
animationController.addStatusListener((status) {
if (status == AnimationStatus.completed) {
if (++counter < repeatTimes) {
animation.reverse();
}
} else if (status == AnimationStatus.dismissed) {
animation.forward();
}
});

I created a simple widget for this purpose
import 'package:flutter/widgets.dart';
import 'package:lottie/lottie.dart';
class LottieRepeat extends StatefulWidget {
final String asset;
final int repeatTimes;
final VoidCallback onComplete;
const LottieRepeat(
{Key? key,
required this.asset,
required this.repeatTimes,
required this.onComplete})
: super(key: key);
#override
State<LottieRepeat> createState() => _MyLottieRepeatState();
}
class _MyLottieRepeatState extends State<LottieRepeat>
with TickerProviderStateMixin {
late final AnimationController _controller;
int count = 0;
#override
void initState() {
super.initState();
/// validate
if (widget.repeatTimes <= 0) throw Exception('invalid repeat time');
_controller = AnimationController(vsync: this);
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
count++;
if (count < widget.repeatTimes) {
_controller.reset();
_controller.forward();
} else {
widget.onComplete();
}
}
});
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Lottie.asset(
widget.asset,
controller: _controller,
onLoaded: (composition) {
// Configure the AnimationController with the duration of the
// Lottie file and start the animation.
_controller
..duration = composition.duration
..forward();
},
);
}
}
sample usage:
showDialog(
context: context,
builder: (context) {
return AlertDialog(
content: LottieRepeat(
asset: 'assets/image/tick.json',
repeatTimes: 2,
onComplete: () => Navigator.of(context).pop(true),
),
);
});

Related

Flutter How can I use setState and only change one area and leave the other as it is?

I want a page that has a timer and also displays math problems. and whenever the correct answer has been entered, a new task should appear. But the problem is that whenever a new task appears, the timer is reset. How can I prevent this ?
late Timer timer;
double value = 45;
void startTimer() {
timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (value > 0) {
setState(() {
value--;
});
} else {
setState(() {
timer.cancel();
});
}
});
}
#override
void initState() {
// TODO: implement initState
super.initState();
startTimer();
}
#override
void dispose() {
controller.dispose();
super.dispose();
}
void userTextFieldInput() {
controller.addListener(
() {
String rightResult = (firstIntValue + secondIntValue).toString();
String userResult = controller.text;
if (rightResult == userResult) {
setState(() {
DatabaseHelper(
firstUserValue: firstIntValue,
secondUserValue: secondIntValue,
finalUserResult: int.parse(controller.text),
).setDB();
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const CalculatePage(),
),
);
});
} else if (controller.text.length >= 2) {
controller.clear();
}
},
);
}
You should create two different state, one for timer and another one for the problems.
You can use the package flutter bloc to manage these state easily.

How to Access Custom Hook functions from another class

i have a custom hook timer_hook.dart
The way this hooks works is whenever the user open's up the page, the counter begins to count down, so i am trying to access the method "restartTimer()"
from the main class where i am using the hook. but i dont know how to achieve that, sorry i am still learning flutter. and i don't want to use the stateful widget.
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
Duration useCountDownTimer() {
return use(const CountdownTimer());
}
class CountdownTimer extends Hook<Duration> {
const CountdownTimer();
#override
_CountdownTimerState createState() => _CountdownTimerState();
}
class _CountdownTimerState extends HookState<Duration, CountdownTimer> {
Timer? _timer;
Duration _duration = const Duration(seconds: 6);
void countDownTime() {
const reduceSeconds = 1;
setState(() {
final seconds = _duration.inSeconds - reduceSeconds;
_duration = Duration(seconds: seconds);
if (_duration.inSeconds == 0) {
_timer?.cancel();
}
});
}
void restartTimer() {
setState(() {
_duration = const Duration(seconds: 90);
_timer =
Timer.periodic(const Duration(seconds: 1), (_) => countDownTime());
});
}
#override
void initHook() {
super.initHook();
_timer = Timer.periodic(const Duration(seconds: 1), (_) => countDownTime());
}
#override
Duration build(BuildContext context) {
return _duration;
}
#override
void dispose() {
_timer?.cancel();
super.dispose();
}
}

Where to prevent re-animation of ListView.builder items when scrolling back to previously animated items?

I have a listview in which the text of items are animated when they first appear - and when they reappear after enough scrolling. When the list grows to certain size and the user scrolls back far enough items are animated again - presumably they've been removed from the widget tree and are now being re-inserted and thus get re-initiated etc. I want to prevent this from happening so that they only animate the first time they appear.
I think this means I need to have state stored somewhere per item that keeps track and tells the individual items whether they should animate on them being built or not. I am not sure where to put and how to connect that though, partly because it seems to overlap between presentation and business logic layers. I think perhaps it should be a variable in the list items contained in the list object that the listview builder is constructing from - or should it somehow be in the actual widgets in the listview?
class _StockListViewBuilderState extends State<StockListViewBuilder> with asc_alertBar {
final ScrollController _scrollController = ScrollController();
late double _scrollPosition;
late double _maxScrollExtent;
late bool isThisTheEnd = false;
_scrollListener() async {
setState(() {
_scrollPosition = _scrollController.position.pixels;
_maxScrollExtent = _scrollController.position.maxScrollExtent;
});
if (!isThisTheEnd && _scrollPosition / _maxScrollExtent > 0.90) {
isThisTheEnd = true;
if (widget.stockListicle.getIsRemoteEmpty()) {
alertBar('No more items available', /* null,*/ context: context);
} else {
await widget.stockListicle.fetch(numberToFetch: 5);
}
}
if (isThisTheEnd && _scrollPosition / _maxScrollExtent <= 0.90) {
isThisTheEnd = false;
}
}
#override
void initState() {
super.initState();
late String? userFullName = GetIt.I.get<Authenticate>().user?.fullName;
developer.log('Authenticated user $userFullName', name: '_StockListViewBuilderState');
developer.log("init ", name: "_StockListViewBuilderState ");
int listCount;
_scrollController.addListener(_scrollListener);
WidgetsBinding.instance.addPostFrameCallback((_) async {
//developer.log("stckLtcl init pf con ");
listCount = widget.stockListicle.items.length;
if (listCount < 10 && !widget.stockListicle.getIsRemoteEmpty()) {
try {
await widget.stockListicle.fetch(numberToFetch: 10);
} catch (e) {
super.setState(() {
//developer.log("Can't load stock:$e");
alertBar(
"Couldn't load from the internet.",
context: context,
backgroundColor: Colors.purple,
);
});
}
}
});
WidgetsBinding.instance.addPostFrameCallback((_) async {
final ConnectionNotifier connectionNotifier = context.read<ConnectionNotifier>();
if (connectionNotifier.isConnected() != true) {
await connectionNotifier.check();
if (connectionNotifier.isConnected() != true) {
alertBar("Please check the internet connection.", context: context);
}
}
});
}
#override
Widget build(BuildContext context) {
return ListView.builder(
scrollDirection: Axis.vertical,
controller: _scrollController,
shrinkWrap: true,
key: widget.theKey,
itemCount: widget.stockListicle.items.length + 1,
itemBuilder: (context, index) {
if (index <= widget.stockListicle.items.length - 1) {
return InkWell(
onTap: (() => Navigator.pushNamed(
context,
'/stocks/stock',
arguments: ScreenArguments(widget.stockListicle.items[index] as Stock),
)),
child: StockListItem(
stock: widget.stockListicle.items[index] as Stock,
));
} else {
return LoadingItemNotifier(
isLoading: widget.stockListicle.getIsBusyLoading(),
);
}
},
);
}
}
//...
Currently StockListItem extends StatelessWidget and returns a 'ListTile' which as its title parameter has ...title: AnimatedText(textContent: stock.title),...
I was trying to keep track of first-time-animation inside AnimatedText widget until I realized from an OOP & Flutter perspective, it's probably wrong place...
class AnimatedText extends StatefulWidget {
final bool doShowMe;
final String textContent;
final Duration hideDuration;
final double durationFactor;
const AnimatedText({
Key? key,
this.doShowMe = true,
this.textContent = '',
this.hideDuration = const Duration(milliseconds: 500),
this.durationFactor = 1,
}) : super(key: key);
#override
State<AnimatedText> createState() => _AnimatedTextState();
}
class _AnimatedTextState extends State<AnimatedText> with SingleTickerProviderStateMixin {
late AnimationController _appearanceController;
late String displayText;
late String previousText;
late double durationFactor;
late Duration buildDuration = Duration(
milliseconds: (widget.textContent.length / 15 * widget.durationFactor * 1000).round());
#override
void initState() {
super.initState();
developer.log('init ${widget.textContent}', name: '_AnimatedTextState');
displayText = '';
previousText = widget.textContent;
_appearanceController = AnimationController(
vsync: this,
duration: buildDuration,
)..addListener(
() => updateText(),
);
if (widget.doShowMe) {
_doShowMe();
}
}
void updateText() {
String payload = widget.textContent;
int numCharsToShow = (_appearanceController.value * widget.textContent.length).ceil();
if (widget.doShowMe) {
// make it grow
displayText = payload.substring(0, numCharsToShow);
// developer.log('$numCharsToShow / ${widget.textContent.length} ${widget.textContent}');
} else {
// make it shrink
displayText = payload.substring(payload.length - numCharsToShow, payload.length);
}
}
#override
void didUpdateWidget(AnimatedText oldWidget) {
super.didUpdateWidget(oldWidget);
if ((widget.doShowMe != oldWidget.doShowMe) || (widget.textContent != oldWidget.textContent)) {
if (widget.doShowMe) {
_doShowMe();
} else {
_doHideMe();
}
}
if (widget.doShowMe && widget.textContent != previousText) {
previousText = widget.textContent;
developer.log('reset');
_appearanceController
..reset()
..forward();
}
}
#override
void dispose() {
_appearanceController.dispose();
displayText = '';
super.dispose();
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _appearanceController,
builder: (context, child) {
return Text(displayText);
});
}
void _doShowMe() {
_appearanceController
..duration = buildDuration
..forward();
}
void _doHideMe() {
_appearanceController
..duration = widget.hideDuration
..reverse();
}
}

Flutter: I need help! My random icon pop up animations work but give errors when moving to next page

Map and icons
I have a map and I want to create icons that pop in and fade out after a few
seconds to show that there are other people in the area. These icons generate
random so I call set state to reset their locations, but I am getting errors
when a new page is built. These errors say that my controller was disposed and
then reverse was called and that the null check operator was used on a null
value.
Error Controller disposed then reverse called
Error null check operator used on a null value
Here is my code
late AnimationController _controller1;
late AnimationController _controller2;
late AnimationController _controller3;
Timer? timer1F;
Timer? timer1R;
Timer? timer2F;
Timer? timer2R;
Timer? timer3F;
Timer? timer3R;
#override
void initState() {
super.initState();
_controller1 =
AnimationController(vsync: this, duration: Duration(seconds: 1))
..addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.completed) {
if (mounted) {
timer1R = Timer(Duration(seconds: 10), () {
_controller1.reverse();
// print('1 Reverse');
timer1F = Timer(Duration(seconds: 30), () {
// setState(() {});
_controller1.forward(from: 0.0);
// print('1 forward');
});
});
}
}
});
_controller1.forward();
_controller2 =
AnimationController(vsync: this, duration: Duration(seconds: 1))
..addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.completed) {
if (mounted) {
timer2R = Timer(Duration(seconds: 20), () {
_controller2.reverse();
// print('2 Reverse');
timer2F = Timer(Duration(seconds: 20), () {
// setState(() {});
// print('2 forward');
_controller2.forward(from: 0.0);
});
});
}
}
});
// Future.delayed(Duration(seconds: 10), () {
// });
_controller2.forward();
_controller3 =
AnimationController(vsync: this, duration: Duration(seconds: 1))
..addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.completed) {
if (mounted) {
timer3R = Timer(Duration(seconds: 30), () {
_controller3.reverse();
// print('3 Reverse');
timer3F = Timer(Duration(seconds: 10), () {
// print('3 forward');
setState(() {});
_controller3.forward(from: 0.0);
});
});
}
}
});
_controller3.forward();
}
#override
void dispose() {
_controller1.dispose();
_controller2.dispose();
_controller3.dispose();
timer1F!.cancel();
timer1R!.cancel();
timer2F!.cancel();
timer2R!.cancel();
timer3F!.cancel();
timer3R!.cancel();
super.dispose();
}
Each controller is linked to a different icon, but the error is being called when I reverse the controller. It says it disposes first and then reverse is called. It also shows that if one of the controllers isn't initialized (due to the timer widget calling the controllers forward or reverse) then it gives me that null check assigned to null operator error.

Showing Simple Duration timer left on screen

Is there any simple way that i can show the time remaing in DURATION on screen?
void onTapDown(TapDownDetails details){
if(state == State.menu){
timer = Timer(duration = new Duration(seconds: 5), () {
state = State.playing;
});
print("STARTING IN [secondsremaing]");
}
or should i have to make it complex and implement any other class to do so ?
It will not work that way because a Timer will only call the given callback after the given Duration, but it will not tick for you.
If you want to show an updated widget indicating the remaining time, you will have to use a ticker and this is normally achieved by setting up an AnimationController.
In your case, it could look something like this (assuming you are in a StatefulWidget):
class _YourWidgetsState extends State<YourWidget> with SingleTickerProviderMixin {
AnimationController remainingTimeController;
#override
void initState() {
super.initState();
remainingTimeController = AnimationController(vsync: this, duration: const Duration(seconds: 5));
}
/// Used somewhere in your build method.
void onTapDown(TapDownDetails details) {
if(state == State.menu){
timer = Timer(duration = new Duration(seconds: 5), () {
state = State.playing;
});
print("STARTING IN [secondsremaing]");
}
}
#override
Widget build(BuildContext context) {
// Obviously return your other widgets in here as well.
return AnimatedBuilder(
animation: remainingTimeController,
builder: (context, _) => Text('${remainingTimeController.value * 60 * 5}'),
);
}
}