Flutter: How I can fix page controller animation error - animation

I have a page containing a page view slider with a page controller and I get this error:
[ERROR:flutter/lib/ui/ui_dart_state.cc(157)] Unhandled Exception:
Looking up a deactivated widget's ancestor is unsafe. E/flutter (
7013): At this point the state of the widget's element tree is no
longer stable. E/flutter ( 7013): To safely refer to a widget's
ancestor in its dispose() method, save a reference to the ancestor by
calling dependOnInheritedWidgetOfExactType() in the widget's
didChangeDependencies() method.
I'm trying to solve it but I can't and I don't know the reason for this error.
This my code which I use:
var pageController = PageController(initialPage: 0);
var pageViewModelData = List<PageViewData>();
Timer sliderTimer;
var currentShowIndex = 0;
#override
void initState() {
pageViewModelData.add(PageViewData(
titleText: 'Cape Town',
subText: 'Extraordinary five-star\noutdoor activites',
assetsImage: 'assets/images/explore_2.jpg',
));
pageViewModelData.add(PageViewData(
titleText: 'Find best deals',
subText: 'Extraordinary five-star\noutdoor activites',
assetsImage: 'assets/images/explore_1.jpg',
));
pageViewModelData.add(PageViewData(
titleText: 'Find best deals',
subText: 'Extraordinary five-star\noutdoor activites',
assetsImage: 'assets/images/explore_3.jpg',
));
sliderTimer = Timer.periodic(Duration(seconds: 4), (timer) {
if (currentShowIndex == 0) {
pageController.animateTo(MediaQuery.of(context).size.width, duration: Duration(seconds: 1), curve: Curves.fastOutSlowIn);
} else if (currentShowIndex == 1) {
pageController.animateTo(MediaQuery.of(context).size.width * 2, duration: Duration(seconds: 1), curve: Curves.fastOutSlowIn);
} else if (currentShowIndex == 2) {
pageController.animateTo(0, duration: Duration(seconds: 1), curve: Curves.fastOutSlowIn);
}
});
super.initState();
}
#override
void dispose() {
sliderTimer?.cancel();
super.dispose();
}
I get the error start from this line:
MediaQuery.of(context).size.width
Can anyone tell me the reason for this issue! Thanks

You can copy paste run full code below
Value of MediaQuery.of(context).size.width is not not ready in initState()
You can use WidgetsBinding.instance.addPostFrameCallback
code snippet
WidgetsBinding.instance.addPostFrameCallback((_) {
print('width ${MediaQuery.of(context).size.width}');
sliderTimer = Timer.periodic(Duration(seconds: 4), (timer) {
if (currentShowIndex == 0) {
pageController.animateTo(MediaQuery.of(context).size.width, duration: Duration(seconds: 1), curve: Curves.fastOutSlowIn);
} else if (currentShowIndex == 1) {
pageController.animateTo(MediaQuery.of(context).size.width * 2, duration: Duration(seconds: 1), curve: Curves.fastOutSlowIn);
} else if (currentShowIndex == 2) {
pageController.animateTo(0, duration: Duration(seconds: 1), curve: Curves.fastOutSlowIn);
}
});
});
working demo
output
I/flutter ( 1876): width 411.42857142857144
full code
import 'dart:io';
import 'dart:async';
import 'package:flutter/material.dart';
void main() {
runApp(new MaterialApp(
title: 'Flutter Tutorial',
home: new AnimatedIconExample(),
));
}
class AnimatedIconExample extends StatefulWidget {
#override
_AnimatedIconExampleState createState() => _AnimatedIconExampleState();
}
class _AnimatedIconExampleState extends State<AnimatedIconExample>
with SingleTickerProviderStateMixin {
PageController pageController = PageController();
AnimationController controller;
Timer sliderTimer;
var currentShowIndex = 0;
#override
void initState() {
super.initState();
controller =
AnimationController(vsync: this, duration: Duration(seconds: 1));
WidgetsBinding.instance.addPostFrameCallback((_) {
print('width ${MediaQuery.of(context).size.width}');
sliderTimer = Timer.periodic(Duration(seconds: 4), (timer) {
if (currentShowIndex == 0) {
pageController.animateTo(MediaQuery.of(context).size.width, duration: Duration(seconds: 1), curve: Curves.fastOutSlowIn);
} else if (currentShowIndex == 1) {
pageController.animateTo(MediaQuery.of(context).size.width * 2, duration: Duration(seconds: 1), curve: Curves.fastOutSlowIn);
} else if (currentShowIndex == 2) {
pageController.animateTo(0, duration: Duration(seconds: 1), curve: Curves.fastOutSlowIn);
}
});
});
}
#override
void dispose() {
controller?.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: GestureDetector(
child: Center(
child: AnimatedIcon(
icon: AnimatedIcons.menu_close, progress: controller),
),
onTap: () {
controller.reverse();
pageController.animateToPage(0,
duration: Duration(seconds: 1), curve: Curves.linear);
},
),
title: Text("Animated PageView Controller")),
body: PageView(
controller: pageController,
scrollDirection: Axis.vertical,
children: <Widget>[
buildPage0(),
Container(color: Colors.green),
Container(color: Colors.blue),
Container(color: Colors.yellow),
Container(color: Colors.pink),
],
),
);
}
show(int page) {
controller.forward();
pageController.animateToPage(page,
duration: Duration(seconds: 1), curve: Curves.linear);
}
Container buildPage0() {
return Container(
color: Colors.white,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ListTile(onTap: () => show(1), title: Center(child: Text("Green"))),
ListTile(onTap: () => show(2), title: Center(child: Text("Blue"))),
ListTile(onTap: () => show(3), title: Center(child: Text("Yellow"))),
ListTile(onTap: () => show(4), title: Center(child: Text("Pink"))),
],
),
);
}
}

Related

How to create a Slide-In and Slide-Out animation in Flutter?

I am trying to create a Slide in and Slide out animation in Flutter. Animation should look like this:
----- Widget slides in ---> Wait for 1 seconds -----Widget slides out of screen -->
I have tried following code but my animation is stuck in a loop.
class _MyStatefulWidgetState extends State<MyStatefulWidget> with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation<Offset> _positionAnimation;
Animation<double> opacityAnimation;
#override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
_positionAnimation = Tween<Offset>(
begin: const Offset(-1, 0),
end: const Offset(0, 0.0),
).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.elasticOut),
)..addStatusListener((status) {
print('animation 1 status $status');
if (status == AnimationStatus.completed) {
_controller.reset();
}
if (status == AnimationStatus.dismissed) {
_positionAnimation = Tween<Offset>(
begin: const Offset(0, 0),
end: const Offset(1, 0.0),
).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.elasticIn),
)..addStatusListener((status2) {
print('animation 2 status $status2');
if (status == AnimationStatus.dismissed) {
_controller.reset();
}
});
_controller.forward();
}
});
_controller.forward();
}
#override
void dispose() {
super.dispose();
_controller.dispose();
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
builder: _buildAnimation,
animation: _controller,
);
}
Widget _buildAnimation(BuildContext context, Widget child) {
return Opacity(
opacity: 1,
child: SlideTransition(
position: _positionAnimation,
child: Container(
color: Colors.blueAccent,
height: 100,
child: Center(
child: Text(
'Hello, World!',
style: Theme.of(context).textTheme.display1,
),
),
),
),
);
}
With this approach animation is stuck in a loop.
I cannot use staggered animation as I am trying to animate same property. (Or is there a way to use staggered animation on same property?).
Any better way for implementing this?
Taking idea from #pskink code in comments, achieved desired effect with following code using TweenAnimationBuilder:
class _SlideInOutWidgetState extends State<SlideInOutWidget>
with SingleTickerProviderStateMixin {
double startPos = -1.0;
double endPos = 0.0;
Curve curve = Curves.elasticOut;
#override
Widget build(BuildContext context) {
return TweenAnimationBuilder(
tween: Tween<Offset>(begin: Offset(startPos, 0), end: Offset(endPos, 0)),
duration: Duration(seconds: 1),
curve: curve,
builder: (context, offset, child) {
return FractionalTranslation(
translation: offset,
child: Container(
width: double.infinity,
child: Center(
child: child,
),
),
);
},
child: Text('animated text', textScaleFactor: 3.0,),
onEnd: () {
print('onEnd');
Future.delayed(
Duration(milliseconds: 500),
() {
curve = curve == Curves.elasticOut
? Curves.elasticIn
: Curves.elasticOut;
if (startPos == -1) {
setState(() {
startPos = 0.0;
endPos = 1.0;
});
}
},
);
},
);
}
}
Use Marquee plugin for text Animations
Install :
marquee: ^1.3.1
Example :
Marquee(
text: 'There once was a boy who told this story about a boy: "',
)
Fore more info try marquee | flutterpckage

How to scroll or jump to position of PageView.builder or PageController in Flutter?

Issue: Unable to scroll to POSITION after loading the Pageviews using PageController *
like ViewPager scroll to specific page in Android
Widget _buildCarousel(BuildContext context, int selectedIndex) {
PageController controller = PageController(viewportFraction: 1, keepPage: true);
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
SizedBox(
// you may want to use an aspect ratio here for tablet support
height: 400.0,
width: 240,
child: PageView.builder(
itemCount: assetImageList.length,
controller: controller,
itemBuilder: (BuildContext context, int itemIndex) {
return _buildCarouselItem(context, selectedIndex, itemIndex);
},
),
)
],
);
}
Finally found the answer. Just set the initialPage: mSelectedPosition attribute like:
child: PageView.builder(
itemCount: mTemplateModelList.length,
controller: PageController(initialPage: mSelectedPosition, keepPage: true, viewportFraction: 1),
itemBuilder: (BuildContext context, int itemIndex) {
return _buildCarouselItem(context, selectedIndex, itemIndex);
},
),
OR if you want to scroll the page after the button is clicked then, you can use jumpTo() method using PageController which is clearly mentioned below by another user: #android.
Currently there's 2 options to handle your request:
PageView.builder(
controller: _pageController,
itemCount: _list.length,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
_pageController.jumpToPage(index); // for regular jump
_pageController.animateToPage(_position, curve: Curves.decelerate, duration: Duration(milliseconds: 300)); // for animated jump. Requires a curve and a duration
},
child: Container();
);
}
),
You can use jumpTo() method to scroll position for PageView.
I have create one changePageViewPostion() method in below example:
import 'package:flutter/material.dart';
class MyPageView extends StatefulWidget {
createState() {
return StateKeeper();
}
}
class StateKeeper extends State<MyPageView> {
PageController controller = PageController(viewportFraction: 1, keepPage: true);
var currentPageValue = 0.0;
var mItemCount = 10;
#override
void initState() {
// TODO: implement initState
super.initState();
controller.addListener(() {
setState(() {
currentPageValue = controller.page;
});
});
}
void changePageViewPostion(int whichPage) {
if(controller != null){
whichPage = whichPage + 1; // because position will start from 0
double jumpPosition = MediaQuery.of(context).size.width / 2;
double orgPosition = MediaQuery.of(context).size.width / 2;
for(int i=0; i<mItemCount; i++){
controller.jumpTo(jumpPosition);
if(i==whichPage){
break;
}
jumpPosition = jumpPosition + orgPosition;
}
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('PageView position change'),
),
body: PageView.builder(
controller: controller,
itemBuilder: (context, position) {
return Container(
color: position % 2 == 0 ? Colors.blue : Colors.pink,
child: Column(
children: <Widget>[
Center(
child: Text(
"Page " + (position + 1).toString(),
style: TextStyle(color: Colors.white, fontSize: 22.0),
),
),
Align(
alignment: FractionalOffset.bottomCenter,
child: Padding(padding: EdgeInsets.only(bottom: 20),
child: FloatingActionButton(
elevation: 0.0,
child: new Icon(Icons.check),
backgroundColor: new Color(0xFFE57373),
onPressed: (){
changePageViewPostion(5);
}
),),
),
],
),
);
},
itemCount: mItemCount,
)
);
}
}
We can get current position with controller as below:
controller.addListener(() {
setState(() {
currentPageValue = controller.page.toInt();
print((currentPageValue + 1).toString());
});
});
Hope it helps :)
If you just want to scroll to the next page using a button, you can simply use the following method.
//Create a PageController variable
late PageController _pageController;
//Initialize the variable in the init method.
#override
void initState() {
_pageController = PageController(
initialPage: _activePage, keepPage: true, viewportFraction: 1);
super.initState();
}
//Use this nextPage() method in the onPressed() method.
onPressed: () {
setState(() {
_activePage < 2
? _activePage++
: Navigator.pushReplacementNamed(
context, LoginScreen.id);
});
_pageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.decelerate,
);
}

setState does not update wiget UI

I spent few hours trying to understand why clicking on the IconButton does not toggle change its icon.
import 'package:flutter/material.dart';
import 'dart:core';
class TestIconChange extends StatefulWidget {
#override
_TestIconChangeState createState() => _TestIconChangeState();
}
class _TestIconChangeState extends State<TestIconChange>
with TickerProviderStateMixin {
IconData _iconData = Icons.add;
AnimationController _animationController1;
Widget _child;
#override
void initState() {
super.initState();
_animationController1 = AnimationController(
vsync: this,
value: 1,
duration: Duration(seconds: 1),
);
}
#override
Widget build(BuildContext context) {
if (_child == null) _child = _buildButton();
return Scaffold(
body: Container(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
AnimatedSwitcher(
duration: Duration(milliseconds: 100),
child: _child,
),
RaisedButton(
child: Text('Text Child'),
onPressed: (() {
setState(() {
_child = Text('Dummy text');
});
}),
),
RaisedButton(
child: Text('Button Child'),
onPressed: (() {
setState(() {
_child =_buildButton();
},);
}),
)
],
),
),
),
);
}
Widget _buildButton() {
return IconButton(
onPressed: () {
setState(() {
(_iconData == Icons.add)
? _iconData = Icons.remove
: _iconData = Icons.add;
});
},
icon: Icon(_iconData),
);
}
}
When you call setState it rebuild only Widgets that build in build method.
The main problem was that you didn't do it. You didn't rebuild _child in build method.
This line in your code wrong: if (_child == null) _child = _buildButton();
If you make it like this _child = _buildButton(); then work only button +/-, but not works change to text. Need refactoring of your code!
So, I made refactoring of your code and add ChildType that indicate what type of Widget you want to show: text or button. And then use it in setState method. Now it works, as you expected :)
import 'package:flutter/material.dart';
import 'dart:core';
class TestIconChange extends StatefulWidget {
#override
_TestIconChangeState createState() => _TestIconChangeState();
}
enum ChildType {text, button}
class _TestIconChangeState extends State<TestIconChange>
with TickerProviderStateMixin {
ChildType curChildType = ChildType.button;
IconData _iconData = Icons.add;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
AnimatedSwitcher(
duration: Duration(milliseconds: 100),
child: _buildButton(),
),
RaisedButton(
child: Text('Text Child'),
onPressed: (() {
setState(() {
curChildType = ChildType.text;
});
}),
),
RaisedButton(
child: Text('Button Child'),
onPressed: (() {
setState(() {
curChildType = ChildType.button;
},);
}),
)
],
),
),
);
}
Widget _buildButton() {
if (curChildType == ChildType.text) {
return Text('Dummy text');
}
else {
return IconButton(
icon: Icon(_iconData),
onPressed: () {
setState(() {
_iconData = (_iconData == Icons.add) ? Icons.remove : _iconData = Icons.add;
});
},
);
}
}
}
Good Luck!

Flutter conditional animation

I have a Stack of 5 Text as children. Each child is wrapped with a FadeTransition object. Outside the Stack I have 5 RaisedButton that maps to one Text each. By default, Text 1 has opacity of 1 and the rest has 0 opacity. When a button is clicked, the opacity of the text that it maps become 1 and the rest become 0.
To do that I created 5 different AnimationController and hardcoded a very long logic. I am not sure that this is the correct way to do that in Flutter.
I believe there must be some easier way.
Moreover, this is a simplified example. The issue in my real application has even complex conditions. (For example, show only Text 2 and Text 3 when Button 5 is clicked and the boolean hasViewedText1 is true .)
import 'dart:core';
import 'package:flutter/material.dart';
class Test extends StatefulWidget {
#override
_State createState() {
return _State();
}
}
class _State extends State<Test> with TickerProviderStateMixin {
AnimationController _animationController1;
AnimationController _animationController2;
AnimationController _animationController3;
AnimationController _animationController4;
AnimationController _animationController5;
#override
void initState() {
super.initState();
_animationController1 = new AnimationController(
vsync: this,
duration: new Duration(seconds: 1),
);
_animationController2 = new AnimationController(
vsync: this,
duration: new Duration(seconds: 1),
);
_animationController3 = new AnimationController(
vsync: this,
duration: new Duration(seconds: 1),
);
_animationController4 = new AnimationController(
vsync: this,
duration: new Duration(seconds: 1),
);
_animationController5 = new AnimationController(
vsync: this,
duration: new Duration(seconds: 1),
);
_everyThingBackward();
_animationController1.forward();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: new AppBar(
elevation: 0.5,
title: new Text(
"Testing views",
style: Theme.of(context).textTheme.title,
),
),
// body: _buildBodyAnimationTest(),
// body: _buildTuto(),
// body: _builtLayoutBuilder(),
body: _builtLayoutConditionalAnimation(),
);
}
Widget _builtLayoutConditionalAnimation() {
return new Column(
children: <Widget>[
new Column(
children: <Widget>[
new RaisedButton(child: new Text("Button 1"), onPressed: () {
_everyThingBackward();
_animationController1.forward();
}),
new RaisedButton(child: new Text("Button 2"), onPressed: () {
_everyThingBackward();
_animationController2.forward();
}),
new RaisedButton(child: new Text("Button 3"), onPressed: () {
_everyThingBackward();
_animationController3.forward();
}),
new RaisedButton(child: new Text("Button 4"), onPressed: () {
_everyThingBackward();
_animationController4.forward();
}),
new RaisedButton(child: new Text("Button 5"), onPressed: () {
_everyThingBackward();
_animationController5.forward();
}),
],
),
new Stack(
children: <Widget>[
FadeTransition(opacity: _animationController1,child: new Text('Text 1 is clicked')),
FadeTransition(opacity: _animationController2,child: new Text('Text 2 is clicked')),
FadeTransition(opacity: _animationController3,child: new Text('Text 3 is clicked')),
FadeTransition(opacity: _animationController4,child: new Text('Text 4 is clicked')),
FadeTransition(opacity: _animationController5,child: new Text('Text 5 is clicked')),
],
),
],
);
}
void _everyThingBackward() {
_animationController1.reverse();
_animationController2.reverse();
_animationController3.reverse();
_animationController4.reverse();
_animationController5.reverse();
}
}
This can me made a lot simpler by using the AnimatedSwitcher widget, link to docs.
Here is a full example:
import 'package:flutter/material.dart';
void main() => runApp(App());
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: SafeArea(child: Center(child: Test())),
),
);
}
}
class Test extends StatefulWidget {
#override
_TestState createState() => _TestState();
}
class _TestState extends State<Test> {
Widget _child = Text(
"No Button Tapped!",
key: UniqueKey(),
);
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
RaisedButton(
child: Text("Button 1"),
onPressed: () {
setState(() {
_child = Text(
"Button 1 Tapped!",
key: UniqueKey(),
);
});
},
),
RaisedButton(
child: Text("Button 2"),
onPressed: () {
setState(() {
_child = Text(
"Button 2 Tapped!",
key: UniqueKey(),
);
});
},
),
RaisedButton(
child: Text("Button 3"),
onPressed: () {
setState(() {
_child = Text(
"Button 3 Tapped!",
key: UniqueKey(),
);
});
},
),
AnimatedSwitcher(
duration: Duration(milliseconds: 200),
child: _child,
),
],
);
}
}
This medium article might be useful too: https://medium.com/flutter-community/what-do-you-know-about-aniamtedswitcher-53cc3a4bebb8

How to chain multiple controller/animation?

Issue
I made an ellipse loading animation inside of flutter, but had to use a Timer on all three different controllers. (See example below...)
Is there any widget that can help chaining three different animations?
I tried using Interval Widget for multiple curves, but it did not provide a smooth transition. e.g. Interval(0.0, 0.3), Interval(0.3, 0.6), Interval(0.6, 0.9) for the animation curves.
Sample
Sample Code
import 'package:flutter/material.dart';
import 'package:flutter/animation.dart';
import 'dart:async';
void main() {
runApp(
new MaterialApp(
home: new Scaffold(
body: new CircleLoader(),
)
)
);
}
class CircleLoader extends StatefulWidget {
#override
_CircleLoaderState createState() => new _CircleLoaderState();
}
class _CircleLoaderState extends State<CircleLoader>
with TickerProviderStateMixin {
Animation<double> animation;
Animation<double> animation2;
Animation<double> animation3;
AnimationController controller;
AnimationController controller2;
AnimationController controller3;
int duration = 1000;
Widget circle = new Container(
height: 10.0,
width: 10.0,
decoration: new BoxDecoration(
shape: BoxShape.circle,
color: Colors.grey[300],
),
);
#override
initState() {
super.initState();
controller = new AnimationController(
duration: new Duration(milliseconds: duration), vsync: this);
controller2 = new AnimationController(
duration: new Duration(milliseconds: duration), vsync: this);
controller3 = new AnimationController(
duration: new Duration(milliseconds: duration), vsync: this);
final CurvedAnimation curve =
new CurvedAnimation(parent: controller, curve: Curves.easeInOut);
final CurvedAnimation curve2 =
new CurvedAnimation(parent: controller2, curve: Curves.easeInOut);
final CurvedAnimation curve3 =
new CurvedAnimation(parent: controller3, curve: Curves.easeInOut);
animation = new Tween(begin: 0.85, end: 1.5).animate(curve)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
controller.forward();
}
});
animation2 = new Tween(begin: 0.85, end: 1.5).animate(curve2)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller2.reverse();
} else if (status == AnimationStatus.dismissed) {
controller2.forward();
}
});
animation3 = new Tween(begin: 0.85, end: 1.5).animate(curve3)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller3.reverse();
} else if (status == AnimationStatus.dismissed) {
controller3.forward();
}
});
controller.forward();
new Timer(const Duration(milliseconds: 300), () {
controller2.forward();
});
new Timer(const Duration(milliseconds: 600), () {
controller3.forward();
});
}
#override
Widget build(BuildContext context) {
return new Center(
child: new Container(
width: 100.0,
height: 50.0,
color: Colors.grey,
child: new Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
new ScaleTransition(scale: animation, child: circle),
new ScaleTransition(scale: animation2, child: circle),
new ScaleTransition(scale: animation3, child: circle),
],
),
),
);
}
}
The trick is that you can create your own tween.
In short, what you want is a smooth curve that goes from 0 to 1 and then 1 to 0 smoothly. Which you can assimilate to a (sin(t * 2 * PI) + 1) / 2 where 0 <= t <= 1
And then delay that curve for each cicles.
class TestTween extends Tween<double> {
final double delay;
TestTween({double begin, double end, this.delay}) : super(begin: begin, end: end);
#override
double lerp(double t) {
return super.lerp((sin((t - delay) * 2 * PI) + 1) / 2);
}
}
Which allows to do
_controller = new AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
)..repeat();
instead of having to add an animation listener and reverse the animation.
The end result is
class CircleLoader extends StatefulWidget {
#override
_CircleLoaderState createState() => new _CircleLoaderState();
}
class _CircleLoaderState extends State<CircleLoader>
with SingleTickerProviderStateMixin {
AnimationController _controller;
#override
initState() {
super.initState();
_controller = new AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
)..repeat();
}
#override
dispose() {
_controller.dispose();
super.dispose();
}
buildCircle(double delay) {
return new ScaleTransition(
scale: new TestTween(begin: .85, end: 1.5, delay: delay)
.animate(_controller),
child: new Container(
height: 10.0,
width: 10.0,
decoration: new BoxDecoration(
shape: BoxShape.circle,
color: Colors.grey[300],
),
),
);
}
#override
Widget build(BuildContext context) {
return new Center(
child: new Container(
width: 100.0,
height: 50.0,
color: Colors.grey,
child: new Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
buildCircle(.0),
buildCircle(.2),
buildCircle(.4),
],
),
),
);
}
}
class TestTween extends Tween<double> {
final double delay;
TestTween({double begin, double end, this.delay})
: super(begin: begin, end: end);
#override
double lerp(double t) {
return super.lerp((sin((t - delay) * 2 * PI) + 1) / 2);
}
}

Resources