I have simple page that contains a single AnimationController.
This animation is triggered on button tap.
The issue is that on some iOS devices this animation is extremely fast like 100ms instead of 1s! On Android this works fine, also in iOS and Android emulator.
var c = AnimationController(duration: const Duration(seconds: 1), vsync: this);
c.addStatusListener((AnimationStatus s) {
if(s == AnimationStatus.completed)
c.reverse();
else if(s == AnimationStatus.dismissed)
c.forward();
});
AnimationBuilder(
animation: c,
builder: (BuildContext context, Widget child) {
return RaisedButton(
color: ColorTween(begin: Colors.blue, end: Colors.red),
child: Text('Animate Me'),
onPressed: () {
c.forward();
}
);
}
)
Edit 1
IOS is animating at about 1s interval if duration is set to 6s.
This is fixed in Flutter v. 0.9.4.
Related
This question already has an answer here:
variable won't change Text dynamically in flutter
(1 answer)
Closed 3 years ago.
I have declared a variable called ticketCounter that is getting stored in SharedPrefs. This variable is getting incremented and decremented as needed. However I wanted to display this counter on my appBar and want its value to update. Although the value is getting stored in the variable it isn't showing up.
int tickets;
void fetchTickets() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
tickets = prefs.getInt('ticketRW') ?? 0;
}
void initState() {
portraitModeOnly();
fetchTickets();
super.initState();
}
Widget build(BuildContext context) {
return Material(
child: Scaffold(
appBar: AppBar(
actions: <Widget>[
Padding(
padding: const EdgeInsets.only(top: 14, right: 14),
child: Text(
tickets.toString(),//isn't getting updated
style: TextStyle(
fontSize: 22,
fontFamily: "Netflix",
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
),
),
),
}
You should use setState() to update your state.
Change this method:
void fetchTickets() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
tickets = prefs.getInt('ticketRW') ?? 0;
}
with this:
void fetchTickets() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
setState(() {
tickets = prefs.getInt('ticketRW') ?? 0;
});
}
In Flutter, there are StatefulWidget and StatelessWidget.
Stateless widgets have an initial state and it never changes.
Stateful widgets have state objects paired with them. Widgets are UI representations of those state objects. If you have a counter variable in a state object, stateful widget will draw the value of counter. However, chaning the counter variable will not update the UI. You should tell the widget "Hey, I changed your state object, you better check it and update yourself." with setState().
This is an introduction to the state management in Flutter. I suggest you to read further here.
My goal is, to have a container who can be dragged around in the vertical axis. After removing the finger the container should move a bit forward with the velocity I had after ending the touch. I want to have the exact same animation/feeling after you stop scrolling on an android phone and it continues to scroll a little bit further.
This is what it looks so far:
This is my code:
class _MyHomePageState extends State<MyHomePage>
with SingleTickerProviderStateMixin {
double _offeset = 0;
double get offset => _offeset;
set offset(double offeset) {
if(offeset < 500)
setState(() {
_offeset = offeset;
});
}
AnimationController controller;
ClampingScrollSimulation simulation;
#override
void initState() {
super.initState();
controller = AnimationController(vsync: this, upperBound: 5)
..addListener(() {
offset += controller.value;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: <Widget>[
Positioned(
top: offset,
left: 0,
right: 0,
child: GestureDetector(
onVerticalDragUpdate: (DragUpdateDetails details) {
offset += details.delta.dy;
},
onVerticalDragEnd: (DragEndDetails details) {
simulation = ClampingScrollSimulation(
position: 0, velocity: details.primaryVelocity,
);
controller.animateWith(simulation);
},
child: Container(height: 50, width: 50, color: Colors.red),
),
),
],
),
);
}
}
The problem is, that this is not the Android scroll animation and I don't know how to get it. (It feels like the animation is linear, stops too early and doesn't take advantage of the velocity parameter) I am even using the ClampingScrollSimulation class, which is used in all Scroll widgets in Flutter to simulate Android scrolling.
The solution was to multiply the _controller.value with _controller.velocity. Like this:
offset += controller.value * _controller.velocity / 100;
I have a grid of cards depicting products. When a card is tapped, I want it to flip over (around the Y axis) to reveal the "other side" that shows the details, while simultaneously growing to fill the screen.
Duration 0.0 - Card shows front side and is in grid view
Duration 0.5 - Card is 50% of the way to full-screen and perpendicular to the screen (front side facing to the right, "back" side facing to the left)
Duration 1.0 - Card is fully expanded and the "back" card is showing.
I have managed to get a flip animation working, but am having trouble figuring out how to also get it to run during the Hero transition. From this article it seems like I may need to make use of flightShuttleBuilder to be able to animate the overlay but my animation does not run during the transition:
return Hero(
tag: 'test',
flightShuttleBuilder: (
BuildContext flightContext,
Animation<double> animation,
HeroFlightDirection flightDirection,
BuildContext fromHeroContext,
BuildContext toHeroContext,
) {
final Hero toHero = toHeroContext.widget;
return Transform(
transform: Matrix4.identity()..rotateY(-pi * animation.value),
alignment: FractionalOffset.center,
child: toHero,
);
},
child: Card(...),
);
As it turns out, flightShuttleBuilder only emits values at the start and end of the transition instead of throughout the animation. Capturing from this issue on GitHub, it is apparently expected behavior.
The workaround is to create your own transition which extends from AnimatedWidget; that will emit values normally and can be used in the flightShuttleBuilder:
class FlipcardTransition extends AnimatedWidget {
final Animation<double> flipAnim;
final Widget child;
FlipcardTransition({#required this.flipAnim, #required this.child})
: assert(flipAnim != null),
assert(child != null),
super(listenable: flipAnim);
#override
Widget build(BuildContext context) {
return Transform(
transform: Matrix4.identity()
..rotateY(-pi * flipAnim.value),
alignment: FractionalOffset.center,
child: child,
);
}
}
...
flightShuttleBuilder: (BuildContext flightContext,
Animation<double> animation,
HeroFlightDirection flightDirection,
BuildContext fromHeroContext,
BuildContext toHeroContext,) {
final Hero toHero = toHeroContext.widget;
return FlipcardTransition(
flipAnim: animation,
child: toHero,
);
},
As Matt said, the default behavior is only emitting start and end value, so we should listen the animation to get the full animatable widget. Here is an example:
Hero(
tag: "YourHeroTag",
flightShuttleBuilder: (BuildContext flightContext,
Animation<double> animation,
HeroFlightDirection flightDirection,
BuildContext fromHeroContext,
BuildContext toHeroContext,) {
return AnimatedBuilder(
animation: animation,
builder: (context, value) {
return Container(
color: Color.lerp(Colors.white, Colors.black87, animation.value),
);
},
);
},
child: Material( // Wrap in Material to prevent missing theme, mediaquery, ... features....
// ...
)
)
It's recommended to wrap in Material widget to prevent unexpected missing style.
So I'm trying to create an animation in Flutter that requires a different outcome every time the user presses a button.
I've implemented the following code according to the Flutter Animations tutorial and created a function to update it.
class _RoulettePageWidgetState extends State<RoulettePageWidget>
with SingleTickerProviderStateMixin {
Animation<double> _animation;
Tween<double> _tween;
AnimationController _animationController;
int position = 0;
#override
void initState() {
super.initState();
_animationController =
AnimationController(duration: Duration(seconds: 2), vsync: this);
_tween = Tween(begin: 0.0, end: 100.0);
_animation = _tween.animate(_animationController)
..addListener(() {
setState(() {});
});
}
void setNewPosition(int newPosition) {
_tween = Tween(
begin: 0.0,
end: math.pi*2/25*newPosition);
_animationController.reset();
_tween.animate(_animationController);
_animationController.forward();
}
#override
Widget build(BuildContext context) {
return Container(
child: Column(
children: <Widget>[
Center(
child: Transform.rotate(
angle: _animationController.value,
child: Icon(
Icons.arrow_upward,
size: 250.0,
),
)),
Expanded(
child: Container(),
),
RaisedButton(
child: Text('SPIN'),
onPressed: () {
setState(() {
setNewPosition(math.Random().nextInt(25));
});
},
)
],
)
);
}
}
As you can see I'm updating the _tween's begin: and end: but this doesn't seem to change the animation.
So what should I be doing to create a 'different' animation every time the users presses the button?
The general idea is to make the animations build upon each other with a random new value so for example:
first spin: 0 -> 10
second spin: 10 -> 13
third spin: 13 -> 18
... etc
So I wondered if I could update the animation, or should I create a new animation every time?
Another thing I could think of was tracking the positions and use the same animation every time (0.0 -> 100.0) to act as a percentage of the transfer.
So instead of creating a new animation from 10 -> 15 I would be doing something like:
currentValue = 10 + (15-10)/100*_animationController.value
I'm going to skip your code a bit, and focus on what you're really asking:
The general idea is to make the animations build upon each other with a random new value so for example:
first spin: 0 -> 10
second spin: 10 -> 13
third spin: 13 -> 18
... etc
With an explicit animation like this, there are three objects you are interested in:
a controller, which is a special kind of Animation that simply generates values linearly from its lower to its upper bound (both doubles, typically 0.0 and 1.0). You can control the flow of the animation - send it running forward, reverse it, stop it, or reset it.
a tween, which isn't an Animation but rather an Animatable. A tween defines the interpolation between two values, which don't even have to be numbers. It implements a transform method under the hood that takes in the current value of an animation and spits out the actual value you want to work with: another number, a color, a linear gradient, even a whole widget. This is what you should use to generate your angles of rotation.
an animation, which is the animation whose value you're actually going to work with (so this is where you'd grab values to build with). You get this by giving your tween a parent Animation to transform - this might be your controller directly but can also be some other sort of animation you've built on it (like a CurvedAnimation, which would give you easing or bouncy/elastic curves and so on). Flutter's animations are highly composable that way.
Your code is failing largely because you're not actually using the top-level animation you created in your build method and you're creating a new tween and animation every time you call setNewPosition. You can use the same tween and animation for multiple animation "cycles" - simply change the begin and end properties of the existing tween and it bubbles up to the animation. That ends up something like this:
class _RoulettePageWidgetState extends State<RoulettePageWidget>
with SingleTickerProviderStateMixin {
Animation<double> _animation;
Tween<double> _tween;
AnimationController _animationController;
math.Random _random = math.Random();
int position = 0;
double getRandomAngle() {
return math.pi * 2 / 25 * _random.nextInt(25);
}
#override
void initState() {
super.initState();
_animationController =
AnimationController(duration: Duration(seconds: 2), vsync: this);
_tween = Tween(begin: 0.0, end: getRandomAngle());
_animation = _tween.animate(_animationController)
..addListener(() {
setState(() {});
});
}
void setNewPosition() {
_tween.begin = _tween.end;
_animationController.reset();
_tween.end = getRandomAngle();
_animationController.forward();
}
#override
Widget build(BuildContext context) {
return Container(
child: Column(
children: <Widget>[
Center(
child: Transform.rotate(
angle: _animation.value,
child: Icon(
Icons.arrow_upward,
size: 250.0,
),
)),
Expanded(
child: Container(),
),
RaisedButton(
child: Text('SPIN'),
onPressed: setNewPosition,
)
],
)
);
}
}
Hope that helps!
While working, in no situation will you actually want to make these animations within your layout as explained by #filleduchaos.
This is under optimized, as you're rebuilding far more than you should for the animation. And it's a pain to write yourself.
You'll want to use the AnimatedWidget family for this. They are divided into two
kinds:
XXTransition
AnimatedXX
The first is a low layer that consumes an Animation and listens to it so that you don't need to do that ugly :
..addListener(() {
setState(() {});
});
The second handles the remaining pieces: AnimationController, TickerProvider and Tween.
This makes using animations much easier as it's almost entirely automatical.
In your case a rotation example would be as followed:
class RotationExample extends StatefulWidget {
final Widget child;
const RotationExample({
Key key,
this.child,
}) : super(key: key);
#override
RotationExampleState createState() {
return new RotationExampleState();
}
}
class RotationExampleState extends State<RotationExample> {
final _random = math.Random();
double rad = 0.0;
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _rotate,
child: AnimatedTransform(
duration: const Duration(seconds: 1),
alignment: Alignment.center,
transform: Matrix4.rotationZ(rad),
child: Container(
color: Colors.red,
height: 42.0,
width: 42.0,
),
),
);
}
void _rotate() {
setState(() {
rad = math.pi * 2 / 25 * _random.nextInt(25);
});
}
}
Easier right?
The irony is that Flutter forgot to provide an AnimatedTransform (even although we have many others !). But no worries, I made it for you!
The AnimatedTransform implementation is as followed :
class AnimatedTransform extends ImplicitlyAnimatedWidget {
final Matrix4 transform;
final AlignmentGeometry alignment;
final bool transformHitTests;
final Offset origin;
final Widget child;
const AnimatedTransform({
Key key,
#required this.transform,
#required Duration duration,
this.alignment,
this.transformHitTests = true,
this.origin,
this.child,
Curve curve = Curves.linear,
}) : assert(transform != null),
assert(duration != null),
super(
key: key,
duration: duration,
curve: curve,
);
#override
_AnimatedTransformState createState() => _AnimatedTransformState();
}
class _AnimatedTransformState
extends AnimatedWidgetBaseState<AnimatedTransform> {
Matrix4Tween _transform;
#override
void forEachTween(TweenVisitor<dynamic> visitor) {
_transform = visitor(_transform, widget.transform,
(dynamic value) => Matrix4Tween(begin: value));
}
#override
Widget build(BuildContext context) {
return Transform(
alignment: widget.alignment,
transform: _transform.evaluate(animation),
transformHitTests: widget.transformHitTests,
origin: widget.origin,
child: widget.child,
);
}
}
I will submit a pull request so that in the future you won't need this bit of code.
If you want to reverse your animation with a different path (go/back way). Try this.
In your setNewPosition function, just define new begin/end value for _tween.
void setNewPosition() {
_tween.begin = 0; //new begin int value
_tween.end = 1; //new end int value
_animationController.reverse();
}
I have the following code in place to animate a Positioned widgets, position, when a pan ends. However, it doesn't animate.
Initialising AnimationController and variables:
AnimationController nodesListAnimationController;
Animation<double> nodesListAnimation;
double nodesListOffsetY = 0.0; //widget default offset (will be large, but acts like 0.0)
double nodesListDragOffsetY = 0.0; //how far we have moved the widget from default offset (nodesListOffsetY)
double nodesListTouchOffset = 0.0; //the initial touch when drag begins
final double nodesListHeight = 350.0; //widget height
#override
void initState() {
super.initState();
nodesListAnimationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 350),
)
..addListener((){
setState(() {
});
});
}
Build function; See the Positioned widget. I can drag this up and down within the bound I have created using the panning callbacks on GestureDetector.
When the pan ends, I want to animate the Positioned widget back to a given position. Currently, no animation occurs.
#override
Widget build(BuildContext context) {
nodesListOffsetY = MediaQuery.of(context).size.width + 110.0;
AppBar appBar = AppBar(
...
);
return Scaffold(
appBar: appBar,
backgroundColor: Colors.black,
body: Stack(
children: <Widget>[
Column(
children: <Widget>[
cameraWidget(),
cameraControls(),
],
),
Positioned(
left: 0.0,
top: nodesListAnimationController.isAnimating ? nodesListAnimation.value : nodesListOffsetY + nodesListDragOffsetY,
width: MediaQuery.of(context).size.width,
height: nodesListHeight,
child: GestureDetector(
onPanStart: (dragDetails) {
nodesListTouchOffset = (dragDetails.globalPosition.dy - appBar.preferredSize.height) - nodesListOffsetY - nodesListDragOffsetY;
},
onPanUpdate: (dragDetails) {
setState(() {
double newDragOffset = (dragDetails.globalPosition.dy - nodesListOffsetY) - appBar.preferredSize.height - nodesListTouchOffset;
//allow it only to move up if is clips off screen, otherwise upper limit not needed
double nodesListVisible = ((MediaQuery.of(context).size.height - appBar.preferredSize.height) - nodesListOffsetY);
bool isClipping = nodesListVisible < nodesListHeight ? true : false;
double upperLimit = 0.0;
if (isClipping) upperLimit = -(nodesListHeight - nodesListVisible);
//limit drag bounds. don't drag too high or low.
if (newDragOffset < upperLimit) newDragOffset = upperLimit;
else if (newDragOffset > 0) newDragOffset = 0.0;
else nodesListDragOffsetY = newDragOffset;
});
},
onPanEnd: (dragDetails) {
double currentPos = (nodesListOffsetY + nodesListDragOffsetY);
nodesListAnimation = Tween(begin: currentPos, end: nodesListOffsetY).animate(nodesListAnimationController);
nodesListAnimationController.forward(from: currentPos);
nodesListAnimation.addStatusListener((state) {
print(nodesListAnimation.value);
});
},
child: nodesList(),
),
),
],
));
}
Late to the party, but your issue is because of:
nodesListAnimationController.forward(from: currentPos)
Whose from is incorrectly accepting a value outside of 0.0-1.0. So you should probably change it to just controller.forward()
This isn’t well documented, but forward’s from is meant to accept values from 0.0-1.0, or more specifically, if set, then the range provided by lowerBound and upperBound (with few exceptions). (you can also think of it as 0-100%). It is not meant to accept the value range from the Tween’s begin and end, nor is it aware of that range.
https://api.flutter.dev/flutter/animation/AnimationController-class.html
By default, an AnimationController linearly produces values that range from 0.0 to 1.0
https://flutter.dev/docs/development/ui/animations/overview#addstatuslistener
An animation might then run forward (e.g., from 0.0 to 1.0)
(And I can verify that after I changed your code to use just .forward(), I was able to see it animate)