I want to play to 2 Lottie files in sequence, i.e. after one Lottie has completed its animation it should play the second Lottie file.
I tried to achieve this by adding a statuslistener (via AnimationController) to the Lottie widget and calling setstate() on the asset file after first Lottie has completed its animation. It did work but there was a lag while switching to the next Lottie file.
void statusListener(AnimationStatus status) {
if (status == AnimationStatus.completed) {
setState(() {
asset = asset2;
});
controller.reset();
controller.forward();
}
}
Can anyone help me figure it out?
Thanks.
Define two different controller for both the animations.
then play the first animation and hide the second animation for now.
After the first animation gets completed, hide it through visibility.
for example :
Visibility(
child: Text("Gone"),
visible: false,
),
Refer this for more detail : stackoverflow : how to hide widget programmatically
then play the second animation and hide the first animation.
for the time delay, use Future.delayed.
this will execute the code after specific time which you chosed.
example :
Let's say your first animation completes in 2 seconds then, you will play the next animation after 2 seconds so that you will execute the next line of code after 2 seconds.
Future.delayed(const Duration(seconds: 2), () {
setState(() {
_controller.forward();
});
});
There is an example in the lottie repo.
I effectively spent an entire day figuring out a solution, so posting this to calm the mind.
Repo Example that plays many different lottie files in sequence:
import 'package:flutter/material.dart';
import 'package:lottie/lottie.dart';
class App extends StatefulWidget {
const App({Key? key}) : super(key: key);
#override
State<App> createState() => _AppState();
}
class _AppState extends State<App> with TickerProviderStateMixin {
int _index = 0;
late final AnimationController _animationController;
#override
void initState() {
super.initState();
_animationController = AnimationController(vsync: this)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
setState(() {
++_index;
});
}
});
}
#override
void dispose() {
_animationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
color: Colors.lightBlue,
home: Scaffold(
backgroundColor: Colors.lightBlue,
appBar: AppBar(
title: Text('$_index'),
),
body: SingleChildScrollView(
child: Center(
child: Column(
children: [
Lottie.asset(files[_index % files.length],
controller: _animationController, onLoaded: (composition) {
_animationController
..duration = composition.duration
..reset()
..forward();
}),
],
),
),
),
),
);
}
}
Related
I am using the Rive package in order to have some nice animations within my Flutter application and I have 2 doubts:
I have a simple animation where some docs gets animated. I want to play this animation on Tap of it, so I'm using OneShotAnimation. The play on tap works, however when the animation ends, it immediately gets reset to the first frame.
When I load the page, in addition, the animation is loaded from the last frame.
How to avoid those 2 problems?
My code:
import 'package:flutter/material.dart';
import 'package:rive/rive.dart';
class Square extends StatefulWidget {
final Widget page;
final String title;
const Square({
required this.page,
required this.title,
Key? key,
}) : super(key: key);
#override
State<Square> createState() => _SquareState();
}
class _SquareState extends State<Square> {
late RiveAnimationController _esamiController;
bool _isPlaying = false;
#override
void initState() {
_esamiController = OneShotAnimation(
'Animation 1',
autoplay: false,
onStart: () => setState(() => _isPlaying = true),
onStop: () => setState(() => _isPlaying = false),
);
super.initState();
}
#override
void dispose() {
_esamiController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return InkWell(
onTap: () => _isPlaying ? null : _esamiController.isActive = true,
child: SizedBox(
width: 192,
height: 192,
child: Card(
color: Colors.black26,
elevation: 10,
child: Center(
child: RiveAnimation.asset(
'assets/animations/esami.riv',
controllers: [_esamiController],
onInit: (_) => setState(() {}),
),
),
),
),
);
}
}
As you can see the sheets should start unordered and end ordered, while here I get the opposite.
I have used your code and a sample rive's community-made animation to reproduce the issue. If I understood your needs right, here are the two solutions:
1- If you take a look at the source code of rive's controller(OneShotAnimation) that you use in your application:
one_shot_controller.dart
import 'package:flutter/widgets.dart';
import 'package:rive/src/controllers/simple_controller.dart';
/// Controller tailered for managing one-shot animations
class OneShotAnimation extends SimpleAnimation {
/// Fires when the animation stops being active
final VoidCallback? onStop;
/// Fires when the animation starts being active
final VoidCallback? onStart;
OneShotAnimation(
String animationName, {
double mix = 1,
bool autoplay = true,
this.onStop,
this.onStart,
}) : super(animationName, mix: mix, autoplay: autoplay) {
isActiveChanged.addListener(onActiveChanged);
}
/// Dispose of any callback listeners
#override
void dispose() {
super.dispose();
isActiveChanged.removeListener(onActiveChanged);
}
/// Perform tasks when the animation's active state changes
void onActiveChanged() {
// If the animation stops and it is at the end of the one-shot, reset the
// animation back to the starting time
if (!isActive) {
reset();
}
// Fire any callbacks
isActive
? onStart?.call()
// onStop can fire while widgets are still drawing
: WidgetsBinding.instance?.addPostFrameCallback((_) => onStop?.call());
}
}
As we can see in the source code, it resets the animation when it ends. So there is nothing to do if you want to use OneShotAnimation. The only way is that you can fork the source code and change the related line. And add a modified version to your project.
one_shot_controller.dart
import 'package:flutter/widgets.dart';
import 'package:rive/src/controllers/simple_controller.dart';
/// Controller tailered for managing one-shot animations
class OneShotAnimation extends SimpleAnimation {
/// Fires when the animation stops being active
final VoidCallback? onStop;
/// Fires when the animation starts being active
final VoidCallback? onStart;
OneShotAnimation(
String animationName, {
double mix = 1,
bool autoplay = true,
this.onStop,
this.onStart,
}) : super(animationName, mix: mix, autoplay: autoplay) {
isActiveChanged.addListener(onActiveChanged);
}
/// Dispose of any callback listeners
#override
void dispose() {
super.dispose();
isActiveChanged.removeListener(onActiveChanged);
}
/// Perform tasks when the animation's active state changes
void onActiveChanged() {
// Fire any callbacks
isActive
? onStart?.call()
// onStop can fire while widgets are still drawing
: WidgetsBinding.instance?.addPostFrameCallback((_) => onStop?.call());
}
}
I have already tried it out, and it works as you wanted. But running animation for the second time could be problematic. For that, please check the second solution.
2- You can use SimpleAnimation. Please check the following solution:
class Square extends StatefulWidget {
final String title;
const Square({
required this.title,
Key? key,
}) : super(key: key);
#override
State<Square> createState() => _SquareState();
}
class _SquareState extends State<Square> {
late SimpleAnimation _esamiController;
bool get isPlaying => _esamiController.isActive;
#override
void initState() {
_esamiController = SimpleAnimation('bell', autoplay: false);
super.initState();
}
void _reset() {
if (!isPlaying) {
_esamiController.reset();
}
}
Future<void> _togglePlay() async {
if (isPlaying) return;
_reset();
_esamiController.isActive = true;
await Future.delayed(
const Duration(milliseconds: 20),
);
_esamiController.isActive = true;
}
#override
void dispose() {
_esamiController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) => Scaffold(
body: Container(
alignment: Alignment.center,
child: InkWell(
onTap: _togglePlay,
child: SizedBox(
width: 192,
height: 192,
child: Card(
color: Colors.black26,
elevation: 10,
child: Center(
child: RiveAnimation.asset(
'assets/alarm.riv',
controllers: [_esamiController],
onInit: (_) => setState(() {}),
),
),
),
),
),
),
);
}
As you can see, I have changed RiveAnimationController with the SimpleAnimation to access the reset method. Because another way, once the animation runs, there is no way to run it for a second time through RiveAnimationController.
If you have further problems, please don't hesitate to write in the comments.
Note: Don't forget to modify animationName and provide a correct asset directory in RiveAnimation.asset widget.
I am using the Flutter Video_Player plugin and am noticing some issues when I want to fast forward 10 seconds, so want to check if I am using the correct code.
What I am experiencing is that is takes a long time, to fast forward the play, if this happens then the tendency is to press fast forward again, and If I press it several times then i notice that the sound either is not in sync or can't be heard any longer.
Here is my code:
GestureDetector(
onTap: () async {
print('FORWARD 10 SECS');
await _controller.seekTo(Duration(
seconds:
_controller.value.position.inSeconds + 10));
},
I'd really appreciate it if I can get some help with this as the app I am working on is video focused so these controls do have to work correctly.
Thank you so much for any help with this. If you have any questions please let me know.
You don't need to await seekTo(), here is working code:
Check the complete code, hope it can help :)
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
void main() => runApp(const VideoPlayerApp());
class VideoPlayerApp extends StatelessWidget {
const VideoPlayerApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Video Player Demo',
home: VideoPlayerScreen(),
);
}
}
class VideoPlayerScreen extends StatefulWidget {
const VideoPlayerScreen({Key? key}) : super(key: key);
#override
_VideoPlayerScreenState createState() => _VideoPlayerScreenState();
}
class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
late VideoPlayerController _controller;
late Future<void> _initializeVideoPlayerFuture;
#override
void initState() {
// Create and store the VideoPlayerController. The VideoPlayerController
// offers several different constructors to play videos from assets, files,
// or the internet.
_controller = VideoPlayerController.network(
'https://media.w3.org/2010/05/sintel/trailer.mp4',
);
// Initialize the controller and store the Future for later use.
_initializeVideoPlayerFuture = _controller.initialize();
// Use the controller to loop the video.
_controller.setLooping(true);
super.initState();
_controller.play();
}
#override
void dispose() {
// Ensure disposing of the VideoPlayerController to free up resources.
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Video'),
),
// Use a FutureBuilder to display a loading spinner while waiting for the
// VideoPlayerController to finish initializing.
body: FutureBuilder(
future: _initializeVideoPlayerFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
// If the VideoPlayerController has finished initialization, use
// the data it provides to limit the aspect ratio of the video.
return AspectRatio(
aspectRatio: _controller.value.aspectRatio,
// Use the VideoPlayer widget to display the video.
child: VideoPlayer(_controller),
);
} else {
// If the VideoPlayerController is still initializing, show a
// loading spinner.
return const Center(
child: CircularProgressIndicator(),
);
}
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Duration currentPosition = _controller.value.position;
Duration targetPosition = currentPosition + const Duration(seconds: 10);
_controller.seekTo(targetPosition);
},
child: const Icon(
Icons.arrow_forward,
),
),
);
}
}
According to this git link. I think you don't need to add await _controller.seekTo(Duration(seconds:_controller.value.position.inSeconds + 10). Instead add this await _controller.seekTo((await _controller.position)! + Duration(seconds: 10));. Not sure if setState is neccesory.
I'd like to create a beginning icon as Icon.add and end icon which is Icon.close in the AnimatedIcon widget. For e.g. their is a prebuilt animation of add_event that corresponds to begin animation = add and end animation = event. I'd like to change the end animation to be Icon.close. It's unclear how to do this as there's no documentation readily available for creating custom animations. The most relevant code I could find is: https://github.com/flutter/flutter/blob/e10df3c1a65f9d7db3fc5340cffef966f7bd40a6/packages/flutter/lib/src/material/animated_icons/data/add_event.g.dart. I believe I should use vitool. How can I go about creating new animations?
Yes, friend, you need to create your own animation
I have written code for the situation that you talked about
I used to Transform widget , and AnimationController
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
AnimationController animatedController;
double _angle = 0;
#override
void initState() {
animatedController =
AnimationController(vsync: this, duration: Duration(milliseconds: 300));
animatedController.addListener(() {
setState(() {
_angle = animatedController.value * 45 / 360 * pi * 2;
});
});
super.initState();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: InkResponse(
onTap: () {
if (animatedController.status == AnimationStatus.completed)
animatedController.reverse();
else if (animatedController.status == AnimationStatus.dismissed)
animatedController.forward();
},
child: Transform.rotate(
angle: _angle,
child: Icon(
Icons.add,
size: 50,
),
),
),
)));
}
}
If you need to customize more than this, Take a look at the AnimatedContainer
So I had some free time and this is something many people might want to use at some point as we do not have much options for AnimatedIcons that are already given to us.
So I went ahead and built this small package and uploaded it on pub dart that solves what you are looking for.
With this package you can animate any two icons.
Check on pub dart animate_icons
Simply add the package into pubspec.yaml like this:
animate_icons:
Then use this Widget like this:
AnimateIcons(
startIcon: Icons.add,
endIcon: Icons.close,
size: 60.0,
onStartIconPress: () {
print("Clicked on Add Icon");
},
onEndIconPress: () {
print("Clicked on Close Icon");
},
duration: Duration(milliseconds: 500),
color: Theme.of(context).primaryColor,
clockwise: false,
),
if the simple transition between the two icons is enough, then simple_animated_icon package might be useful.
class AnimatedIconButton extends StatefulWidget {
#override
_AnimatedIconButtonState createState() => _AnimatedIconButtonState();
}
class _AnimatedIconButtonState extends State<AnimatedIconButton>
with SingleTickerProviderStateMixin {
bool _isOpened = false;
AnimationController _animationController;
Animation<double> _progress;
#override
void initState() {
super.initState();
_animationController =
AnimationController(vsync: this, duration: Duration(milliseconds: 300))
..addListener(() {
setState(() {});
});
_progress =
Tween<double>(begin: 0.0, end: 1.0).animate(_animationController);
}
#override
void dispose() {
_animationController.dispose();
super.dispose();
}
void animate() {
if (_isOpened) {
_animationController.reverse();
} else {
_animationController.forward();
}
setState(() {
_isOpened = !_isOpened;
});
}
#override
Widget build(BuildContext context) {
return IconButton(
onPressed: animate,
icon: SimpleAnimatedIcon(
startIcon: Icons.add,
endIcon: Icons.close,
progress: _progress,
));
}
}
I've created a screen in Flutter that displays a countdown timer. I'm able to play, pause and restart the timer, but I am trying to figure out how to perform an action when the timer reaches 0 (for example, restart itself).
As the dart file is fairly lengthy, I'm just copying below what I believe to be the relevant portions here. But I can add more if needed.
First I create a widget/class for the countdown timer:
class Countdown extends AnimatedWidget {
Countdown({ Key key, this.animation }) : super(key: key, listenable: animation);
Animation<int> animation;
#override
build(BuildContext context){
return Text(
animation.value.toString(),
style: TextStyle(
fontSize: 120,
color: Colors.deepOrange
),
);
}
}
I then have a stateful widget which creates the controller and also imports some data (gameData.countdownClock is the countdown timer's start time, it comes from user input at an earlier screen):
class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
AnimationController _controller;
_GameScreenState(this.gameData);
GameData gameData;
#override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: gameData.countdownClock),
);
}
And then the container that displays the clock:
Container(
child: Countdown(
animation: StepTween(
begin: gameData.countdownClock,
end: 0,
).animate(_controller),
),
),
Do I have to add a listener in that last container? Or somewhere else? (Or something else entirely!)
Any help is appreciated. Thank you
I found the answer on this page:
After complete the widget animation run a function in Flutter
I needed to add .addStatusListener to the animation controller in the initState().
So the new initState() code looks like this:
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: gameData.countdownClock),
);
_controller.addStatusListener((status){
if(status == AnimationStatus.completed){
_controller.reset();
}
}
);
}
Possible value of Animation controller is between 0 to 1.
So I think you have to add listener on gamedata.countDownClock
Please check the below code you might get some idea from it.
import 'dart:async';
import 'package:flutter/material.dart';
class GameScreen extends StatefulWidget {
#override
_GameScreenState createState() => _GameScreenState();
}
class _GameScreenState extends State<GameScreen> with SingleTickerProviderStateMixin {
int countdownClock = 10;
#override
void initState() {
super.initState();
// Your Game Data Counter Change every one Second
const oneSec = const Duration(seconds:1);
new Timer.periodic(oneSec, (Timer t) {
// Restart The Counter Logic
if (countdownClock == 0)
countdownClock = 11;
setState(() { countdownClock = countdownClock - 1; });
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("")),
body: Center(
child: Text('$countdownClock')
),
);
}
}
I was playing around with showModalBottomSheet() in Flutter and I was thinking about changing the default slide from bottom animation. Looking throught flutter documentation I saw that there is in fact a BottomSheet class that accept animation as parameters but, accordingly to the page, showModalBottomSheet() is preferrable.
Is possible to control the animation in some way? I just need to change the default curve and duration.
Thanks
You can use the AnimationController drive method to modify the animation curve, and duration and reverseDuration to set how long the animation will go on. These can be declared on your initState() if you're using a StatefulWidget.
late AnimationController controller;
#override
initState() {
super.initState();
controller = BottomSheet.createAnimationController(this);
// Animation duration for displaying the BottomSheet
controller.duration = const Duration(seconds: 1);
// Animation duration for retracting the BottomSheet
controller.reverseDuration = const Duration(seconds: 1);
// Set animation curve duration for the BottomSheet
controller.drive(CurveTween(curve: Curves.easeIn));
}
Then configure the BottomSheet transtionAnimationController
showModalBottomSheet(
transitionAnimationController: controller,
builder: (BuildContext context) {
return ...
}
)
Here's a sample that you can try out.
import 'package:flutter/material.dart';
class BottomSheetAnimation extends StatefulWidget {
const BottomSheetAnimation({Key? key}) : super(key: key);
#override
_BottomSheetAnimationState createState() => _BottomSheetAnimationState();
}
class _BottomSheetAnimationState extends State<BottomSheetAnimation>
with TickerProviderStateMixin {
late AnimationController controller;
#override
initState() {
super.initState();
controller = BottomSheet.createAnimationController(this);
// Animation duration for displaying the BottomSheet
controller.duration = const Duration(seconds: 1);
// Animation duration for retracting the BottomSheet
controller.reverseDuration = const Duration(seconds: 1);
// Set animation curve duration for the BottomSheet
controller.drive(CurveTween(curve: Curves.easeIn));
}
#override
void dispose() {
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'BottomSheet',
home: Scaffold(
appBar: AppBar(
title: const Text('BottomSheet'),
),
body: Center(
child: TextButton(
child: const Text("Show bottom sheet"),
onPressed: () {
showModalBottomSheet(
context: context,
transitionAnimationController: controller,
builder: (BuildContext context) {
return const SizedBox(
height: 64,
child: Text('Your bottom sheet'),
);
},
);
},
),
),
),
);
}
}
Demo