AnimationController repeat with curve animation - flutter

How do I make a AnimatedController that repeats, but starts up with a curve animation.
Code:
AnimationController :
var _animating = false;
AnimationController _rotationAnimationController;
Animation<double> _animation;
#override
void initState() {
super.initState();
_rotationAnimationController = AnimationController(
duration: Duration(milliseconds: 2000),
vsync: this,
);
_animation =
Tween<double>(begin: 0, end: 4 * pi ).animate(_rotationAnimationController)
..addListener(() {
setState(() {});
});
}
#override
void dispose() {
_rotationAnimationController.dispose();
super.dispose();
}
Button :
GestureDetector(
onTap: () {
Duration startStopTime = Duration(seconds: 3);
if(_animating) {
_rotationAnimationController.animateBack(1, duration: startStopTime, curve: Curves.easeOut);
setState(() {});
} else {
_rotationAnimationController.repeat() //This needs to start with a curve
}
setState(() {
_animating = !_animating;
});
},
child: [...]
),
If you could also make it so that when the repeat stops, it does that with a curve, that would be amazing too :)
Thanks for the help allready :D

The best I understood your question , you want a CurvedAnimation right ? This means your animation will repeat but follow a specific curve . So here the best I could do for you :
Define your AnimationController like this :
Animation<double> _animation;
AnimationController _animationController;
#override
void initState() {
super.initState();
_animationController =
AnimationController(vsync: this, duration: Duration(seconds: 2))
..addListener(() {
setState(() {});
});
final Animation curve =
CurvedAnimation(parent: _animationController, curve: Curves.easeOut);
_animation = Tween<double>(begin: 0, end: pi * 4).animate(curve);
}
and your GestureDetector like this :
GestureDetector(
onTap: () {
if (_animationController.isAnimating)
{
_animationController.animateBack(0,duration: Duration(seconds: 2), curve: Curves.easeIn);
}
else {
_animationController.repeat();
}
},
child: [...]
),
Edit :
I used a TweenAnimationBuilder to have the effect you want :
import 'package:flutter/material.dart';
import 'dart:math' as math;
class TweenAnimatedBuilderRotate extends StatefulWidget {
TweenAnimatedBuilderRotate({Key key}) : super(key: key);
#override
TweenAnimatedBuilderRotateState createState() =>
TweenAnimatedBuilderRotateState();
}
class TweenAnimatedBuilderRotateState
extends State<TweenAnimatedBuilderRotate> {
double _newAngle = 0;
Curve curveThatChanges = Curves.easeIn;
bool isAnimating = false;
int _millsecs = 2000;
void onCompletion() {
if (isAnimating) {
_newAngle += 4 * math.pi;
curveThatChanges = Curves.linear;
_millsecs = 1000;
setState(() {});
} else {
_newAngle = 0;
_millsecs = 2000;
}
}
void onContainerTap() {
if (isAnimating) {
isAnimating = false;
_newAngle = _newAngle;
setState(() {});
} else {
curveThatChanges = Curves.easeIn;
_newAngle += 4 * math.pi;
isAnimating = true;
setState(() {});
}
}
#override
Widget build(BuildContext context) {
return TweenAnimationBuilder(
tween: Tween<double>(begin: 0, end: _newAngle),
duration: Duration(milliseconds: _millsecs),
onEnd: () => onCompletion(),
curve: curveThatChanges,
builder: (
BuildContext ctx,
double angle,
Widget child,
) {
_newAngle = angle;
return Center(
child: Transform(
transform: Matrix4.identity()..rotateZ(_newAngle),
alignment: FractionalOffset.center,
child: GestureDetector(
child: Container(
color: Colors.blueGrey,
width: 200,
height: 200,
),
onTap: () => onContainerTap(),
),
),
);
});
}
}
You can refer to this Medium article to understand about how TweenAnimationdBuilder Works. You can also modify _millsecs variable to speed up/down the animation. Pass TweenAnimatedBuilderRotate() in the body parameter of the Scaffold(...).

Related

Scroll and controller priority flutter

i have a CupertinoModalBottomSheet that is closing by scrolling down. And in this page i have image that i can pinch by two fingers. Is there any way to change priority to pinching when two fingers on screen. Because when i put two fingers on screen i can still close the page by scrolling down until i start pinching.
class PinchZoom extends StatefulWidget {
final Widget child;
PinchZoom({
Key? key,
required this.child,
}) : super(key: key);
#override
State<PinchZoom> createState() => _PinchZoomState();
}
class _PinchZoomState extends State<PinchZoom>
with SingleTickerProviderStateMixin {
late TransformationController controller;
late AnimationController animationController;
Animation<Matrix4>? animation;
Map<int, Offset> touchPositions = <int, Offset>{};
final double minScale = 1;
final double maxScale = 2;
double scale = 1;
OverlayEntry? entry;
#override
void initState() {
super.initState();
controller = TransformationController();
animationController = AnimationController(
vsync: this,
duration: Duration(milliseconds: 200),
)
..addListener(() => controller.value = animation!.value)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
removeOverlay();
}
});
}
#override
void dispose() {
controller.dispose();
animationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) =>
BlocBuilder<DishInfoCubit, DishInfoState>(
builder: (_, _state) {
final dishCubit = BlocProvider.of<DishInfoCubit>(context);
return Builder(
builder: (context) => Listener(
onPointerMove: (opm) {
savePointerPosition(opm.pointer, opm.position);
if (touchPositions.length > 1) {
dishCubit.setIsPinchInProgress(true);
}
},
onPointerDown: (opm) {
savePointerPosition(opm.pointer, opm.position);
if (touchPositions.length > 1) {
dishCubit.setIsPinchInProgress(true);
}
},
onPointerCancel: (opc) {
clearPointerPosition(opc.pointer);
if (touchPositions.length < 2) {
dishCubit.setIsPinchInProgress(false);
}
},
onPointerUp: (opc) {
clearPointerPosition(opc.pointer);
if (touchPositions.length < 2) {
dishCubit.setIsPinchInProgress(false);
}
},
child: InteractiveViewer(
transformationController: controller,
clipBehavior: Clip.none,
panEnabled: false,
minScale: minScale,
maxScale: maxScale,
onInteractionStart: (details) {
print("trying to start anim");
if (details.pointerCount < 2) return;
animationController.stop();
},
onInteractionEnd: (details) {
if (details.pointerCount != 1) return;
resetAnimation();
dishCubit.setIsPinchInProgress(false);
},
child: AspectRatio(
aspectRatio: 1,
child: widget.child,
),
),
),
);
},
);
void removeOverlay() {
entry?.remove();
entry = null;
}
void resetAnimation() {
animation = Matrix4Tween(
begin: controller.value,
end: Matrix4.identity(),
).animate(
CurvedAnimation(parent: animationController, curve: Curves.easeInOut),
);
animationController.forward(from: 0);
}
void savePointerPosition(int index, Offset position) {
setState(() {
touchPositions[index] = position;
});
}
void clearPointerPosition(int index) {
setState(() {
touchPositions.remove(index);
});
}
}

How to transform a widget by switching view?

I want a smooth animation that switch between the previous Scale and position to current Scale and postion. But it look like that it's not the exact previous scale or position. Why the only first widget have a reverse scale => ZoomIn instead of ZoomOut. I add a isSet variable because of the first frame that show the widget far away from where it should start. I have try with AnimatedSlide but it's not the same way to calculate. I don't think I should add more code but if it's necessary for you I would. Am I far from the right way to to it? I'll be grateful. Thank you.
class ItemTransform extends ConsumerStatefulWidget {
final Widget child;
final String idItem;
const ItemTransform({required this.idItem, required this.child, Key? key})
: super(key: key);
#override
_ItemTransformState createState() => _ItemTransformState();
}
class _ItemTransformState extends ConsumerState<ItemTransform>
with SingleTickerProviderStateMixin {
late bool isSet;
late AnimationController _animationController;
late Animation<double> _animationOffset;
late Animation<double> _animationScale;
#override
void initState() {
super.initState();
isSet = false;
_animationController = AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
_animationOffset = Tween<double>(begin: 1.0, end: 0.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut));
_animationScale = Tween(begin: 1.0, end: 1.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut));
_animationController.forward();
WidgetsBinding.instance!.addPostFrameCallback((timeStamp) {
_afterLayout();
});
}
void _afterLayout() {
if (!mounted) {
return;
}
final RenderBox renderBoxRed = context.findRenderObject() as RenderBox;
final size = renderBoxRed.size;
ref.read(viewAnimationProvider).setSize({widget.idItem: size});
final position = renderBoxRed.localToGlobal(Offset.zero);
ref.read(viewAnimationProvider).setPosition({widget.idItem: position});
isSet = true;
}
#override
void dispose() {
_animationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationController,
builder: (context, _) {
final oldOffset =
ref.read(viewAnimationProvider).oldPositions[widget.idItem];
final newOffset =
ref.read(viewAnimationProvider).newPositions[widget.idItem];
Offset? toMove;
if (newOffset != null && oldOffset != null) {
if (isSet) {
toMove = Offset(
(oldOffset.dx - newOffset.dx) * _animationOffset.value,
(oldOffset.dy - newOffset.dy) * _animationOffset.value);
} else {
toMove = Offset(
(newOffset.dx - oldOffset.dx) * _animationOffset.value,
(newOffset.dy - oldOffset.dy) * _animationOffset.value);
}
}
final oldSizes =
ref.read(viewAnimationProvider).oldSizes[widget.idItem];
final newSizes =
ref.read(viewAnimationProvider).newSizes[widget.idItem];
if (oldSizes != null && newSizes != null) {
final oldSize = oldSizes.width / oldSizes.height;
final newSize = newSizes.width / newSizes.height;
if (isSet ) {
_animationScale = Tween(begin: newSize / oldSize, end: 1.0)
.animate(CurvedAnimation(
parent: _animationController, curve: Curves.easeInOut));
}else{
_animationScale = Tween(begin: oldSize / newSize, end: 1.0).animate(
CurvedAnimation(
parent: _animationController, curve: Curves.easeInOut));
}
}
return Transform.translate(
offset: toMove ?? Offset.zero,
child: Transform.scale(
scale: _animationScale.value,
child: widget.child,
),
);
});
}
}
final viewAnimationProvider =
ChangeNotifierProvider<ViewAnimationController>((ref) {
return ViewAnimationController();
});
class ViewAnimationController extends ChangeNotifier {
late TypeOfView oldView;
late TypeOfView newView;
late Map<String, Offset> oldPositions;
late Map<String, Offset> newPositions;
late Map<String, Size> oldSizes;
late Map<String, Size> newSizes;
ViewAnimationController() {
oldView = TypeOfView.staggered;
newView = TypeOfView.normal;
oldPositions = <String, Offset>{};
newPositions = <String, Offset>{};
oldSizes = <String, Size>{};
newSizes = <String, Size>{};
}
void setView(TypeOfView newV) {
oldView = newView;
newView = newV;
notifyListeners();
}
void setPosition(Map<String, Offset> position) {
final newDx = position.values.first.dx;
final newDy = position.values.first.dy;
final offset = newPositions[position.keys.first];
if (offset != null) {
if (newDx != newPositions[position.keys.first]!.dx ||
newDy != newPositions[position.keys.first]!.dy) {
oldPositions[position.keys.first] = newPositions[position.keys.first]!;
newPositions[position.keys.first] = position.values.first;
// newPositions.addAll(position);
}
} else {
newPositions.addAll(position);
}
}
void setSize(Map<String, Size> map) {
final width = map.values.first.width;
final height = map.values.first.height;
final size = newSizes[map.keys.first];
if (size != null) {
if (width != size.width || height != size.height) {
oldSizes[map.keys.first] = newSizes[map.keys.first]!;
newSizes[map.keys.first] = map.values.first;
}
} else {
newSizes.addAll(map);
}
}
}
switch (ref.watch(currentViewProvider)) {
case TypeOfView.normal:
return GridViewCount(
currentSize: currentSize,
listItem: listItem as List<Item>);
case TypeOfView.staggered:
return GridViewStaggered(
currentSize: currentSize,
listItem: listItem as List<Item>);
}
It looks like you want a GridView that can be zoomed?
I've made a widget in the past, called "GalleryView", to achieve a similar kind of effect, but on mobile devices with "pinch-to-zoom" gesture. Let me put it here and see if it's helpful for you. Maybe you can use it as a starting point and modify it towards your end goal.
Usage:
GalleryView.builder(
itemCount: 101, // total items
maxPerRow: 10, // the maximum zoom-out level allowed
itemBuilder: (_, index) => MyImage(),
)
Source:
class GalleryView extends StatefulWidget {
final int? itemCount;
final IndexedWidgetBuilder itemBuilder;
final int initialPerRow;
final int minPerRow;
final int maxPerRow;
final Duration duration;
final ScrollController? controller;
const GalleryView.builder({
Key? key,
this.itemCount,
required this.itemBuilder,
this.initialPerRow = 3,
this.minPerRow = 1,
this.maxPerRow = 7,
this.duration = const Duration(seconds: 1),
this.controller,
}) : super(key: key);
#override
_GalleryViewState createState() => _GalleryViewState();
}
class _GalleryViewState extends State<GalleryView> {
late final ScrollController _controller =
widget.controller ?? ScrollController();
double _maxWidth = 0.0;
late double _size; // size of each grid item
late double _prevSize;
#override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
if (constraints.maxWidth != _maxWidth) {
_maxWidth = constraints.maxWidth;
_size = _maxWidth / widget.initialPerRow;
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
_snapToGrid();
});
}
return GestureDetector(
child: _buildListView(),
onScaleStart: (_) {
_controller.jumpTo(0);
_prevSize = _size;
},
onScaleUpdate: (details) {
final maxSize = _maxWidth / widget.minPerRow;
final minSize = _maxWidth / widget.maxPerRow;
setState(() {
_size = (_prevSize * details.scale).clamp(minSize, maxSize);
});
},
onScaleEnd: (_) => _snapToGrid(),
);
},
);
}
_snapToGrid() {
final countPerRow = (_maxWidth / _size).round().clamp(
widget.minPerRow,
widget.maxPerRow,
);
setState(() => _size = _maxWidth / countPerRow);
}
ListView _buildListView() {
final countPerRow = (_maxWidth / _size).ceil();
return ListView.builder(
controller: _controller,
itemExtent: _size,
itemCount: widget.itemCount != null
? (widget.itemCount! / countPerRow).ceil()
: null,
itemBuilder: (context, int i) {
return OverflowBox(
maxWidth: double.infinity,
alignment: Alignment.centerLeft,
child: Row(
children: [
for (int j = 0; j < countPerRow; j++)
if (widget.itemCount == null ||
i * countPerRow + j < widget.itemCount!)
_buildItem(context, i * countPerRow + j),
],
),
);
},
);
}
Widget _buildItem(context, int index) {
return SizedBox(
width: _size,
height: _size,
child: AnimatedSwitcher(
duration: widget.duration,
child: KeyedSubtree(
key: ValueKey(index),
child: widget.itemBuilder(context, index),
),
),
);
}
}
I finally found a solution. I don't know if it's the best solution but it work as attended. I understand many things in my search. The "_animationScale" work badly because the with and height are same so I have to use "_animationHeight" and "_animationWidth". And I tried in a simple SizedBox and FittedBox to apply the size change but nothing change.
Concerning the offset I use the topLeft with the translate and minus to the new coordonate. It's the same resulte before but I didn't know about that translate methode. I've tried to gather all that _animation in one animation like "Animation<Rect?>" like here or here, but it's relative to a fixed size if I really understand. If someone could clearly explain me why it work as attended? Is there another way with those two previous links?
class ItemTransform extends ConsumerStatefulWidget {
final Widget child;
final String idItem;
const ItemTransform({required this.idItem, required this.child, Key? key})
: super(key: key);
#override
_ItemTransformState createState() => _ItemTransformState();
}
class _ItemTransformState extends ConsumerState<ItemTransform>
with TickerProviderStateMixin {
late bool isSet;
late AnimationController _animationController;
late Animation<Offset> _animationOffset;
late Animation<double> _animationWidth;
late Animation<double> _animationHeight;
#override
void initState() {
super.initState();
isSet = false;
_animationController = AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
_animationOffset = Tween<Offset>(begin: Offset.zero, end: Offset.zero)
.animate(CurvedAnimation(
parent: _animationController, curve: Curves.easeInOut));
_animationWidth = Tween(begin: 1.0, end: 1.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut));
_animationHeight = Tween(begin: 1.0, end: 1.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut));
WidgetsBinding.instance!.addPostFrameCallback((timeStamp) {
_afterLayout();
});
}
void _afterLayout() {
if (!mounted) {
return;
}
final RenderBox renderBox = context.findRenderObject() as RenderBox;
final size = renderBox.size;
_animationWidth = Tween(begin: size.width, end: size.width).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut));
_animationHeight = Tween(begin: size.height, end: size.height).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut));
final position = renderBox.localToGlobal(Offset.zero);
ref.read(viewAnimationProvider).setNewRect(
id: widget.idItem,
newFromWidget:
Rect.fromLTWH(position.dx, position.dy, size.width, size.height));
if (ref.read(viewAnimationProvider).newRect[widget.idItem] != null) {
_animationController.forward();
}
isSet = true;
}
#override
void dispose() {
_animationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationController,
builder: (context, _) {
final oldRect =
ref.read(viewAnimationProvider).oldRect[widget.idItem];
final newRect =
ref.read(viewAnimationProvider).newRect[widget.idItem];
if (oldRect != null && newRect != null) {
_animationOffset = Tween<Offset>(
begin: oldRect.topLeft
.translate(-newRect.topLeft.dx, -newRect.topLeft.dy),
end: Offset.zero)
.animate(CurvedAnimation(
parent: _animationController, curve: Curves.easeInOut));
//
_animationWidth =
Tween(begin: oldRect.size.width, end: newRect.size.width)
.animate(CurvedAnimation(
parent: _animationController, curve: Curves.easeInOut));
_animationHeight =
Tween(begin: oldRect.size.height, end: newRect.size.height)
.animate(CurvedAnimation(
parent: _animationController, curve: Curves.easeInOut));
}
return Offstage(
offstage: !isSet,
child: Transform.translate(
offset: _animationOffset.value,
child: Stack(
clipBehavior: Clip.none,
children: [
Positioned(
height: _animationHeight.value,
width: _animationWidth.value,
child: widget.child,
),
],
),
),
);
});
}
}
final viewAnimationProvider =
ChangeNotifierProvider<ViewAnimationController>((ref) {
return ViewAnimationController();
});
class ViewAnimationController extends ChangeNotifier {
late Map<String,Rect> oldRect;
late Map<String,Rect> newRect;
ViewAnimationController() {
oldRect = <String, Rect>{};
newRect = <String, Rect>{};
}
void setNewRect({required String id, required Rect newFromWidget}){
final rect = newRect[id];
if(rect != null && rect != newFromWidget){
oldRect[id] = rect;
newRect[id] = newFromWidget;
}else{
newRect.addAll({id:newFromWidget});
}
}
}

Flutter: Image Resize Animation is laggy

I am trying to do an animation for an AppBar and the image is shrinking laggy.
You can see in this gif what I am speaking about:
Since it is gif, the entire animation looks a little laggy, but only the image actually is.
CODE
class _AuthAppBarState extends State<AuthAppBar> with TickerProviderStateMixin {
double _leftPoint = 0.0;
double _rightPoint = 70.0;
String _lastTriggeredType = '';
late AnimationController _animationController;
late Animation<double> leftPointAnimation;
late Animation<double> rightPointAnimation;
late Animation<double> titleAnimation;
void triggerAnimation(bool trigger) {
final double prevLeft = _leftPoint;
final double prevRight = _rightPoint;
final double prevTitleHeight = _lastTriggeredType == 'equal' ? 40 : 70;
final double newLeft = leftPoints[widget.type];
final double newRight = rightPoints[widget.type];
final double newTitleHeight = widget.type == 'equal' ? 40 : 70;
setState(() {
leftPointAnimation = Tween<double>(begin: prevLeft, end: newLeft)
.animate(_animationController);
rightPointAnimation = Tween<double>(begin: prevRight, end: newRight)
.animate(_animationController);
titleAnimation =
Tween<double>(begin: prevTitleHeight, end: newTitleHeight)
.animate(_animationController);
_rightPoint = rightPoints[widget.type];
_leftPoint = leftPoints[widget.type];
_lastTriggeredType = widget.type;
});
if (trigger && _animationController != null)
_animationController.forward(from: 0);
}
#override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: Duration(milliseconds: 300),
);
triggerAnimation(false);
}
#override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final size = MediaQuery.of(context).size;
if (_animationController == null || titleAnimation == null) {
return Container();
}
if (_lastTriggeredType != widget.type) triggerAnimation(true);
// * ANIMATED CENTERED TITLE * \\
final Center centeredTitle = Center(
child: AnimatedBuilder(
animation: _animationController,
builder: (ctx, _) {
return SvgPicture.asset(
images['title'],
height: titleAnimation.value,
);
},
),
);
// * MAIN CONTAINER - defines size and color of clip path * \\
final container = Container(
color: theme.primaryColor,
child: AnimatedSize(
curve: Curves.decelerate,
duration: const Duration(seconds: 1),
child: Container(
height: widget.type == 'equal' ? 50 : size.height / 3,
child: centeredTitle,
),
vsync: this,
),
);
return AnimatedBuilder(
builder: (context, anim) {
return ClipPath(
clipper: AuthAppBarPath(
leftPoint: leftPointAnimation.value,
rightPoint: rightPointAnimation.value,
),
child: container,
);
},
animation: _animationController,
);
}
}
I tried to use AnimatedSize and AnimatedContainer. It works the same.
Thank you so much for all the time and help!!

How to add delay between each repeat of animation in flutter

I have a SlideTransition with a container in my application, and it repeats forever, but i would like a delay after each repeat. so it would be like this:
Here's my code
late final AnimationController _animationController = AnimationController(
vsync: this,
duration: const Duration(seconds: 1)
)..repeat(reverse: true); // Here there should be the 500 millisecond delay
late final Animation<Offset> _animation = Tween<Offset>(
begin: Offset.zero,
end: Offset(0, 1),
).animate(_animationController);
. . .
return Scaffold(
body: Center(
child: SlideTransition(
position: _animation,
child: Container(
height: 100,
width: 100,
color: Colors.red,
),
),
),
);
Just change the duration you want => await Future.delayed(Duration(seconds: 3));
I test it with seconds: 3 to get better idea.
late final AnimationController _animationController =
AnimationController(vsync: this, duration: const Duration(seconds: 3))
..forward();
#override
void initState() {
super.initState();
_animationController.addListener(() async {
print(_animationController.status);
if (_animationController.isCompleted) {
await Future.delayed(Duration(seconds: 3));
_animationController.reverse();
} else if (_animationController.isDismissed) {
await Future.delayed(Duration(seconds: 3));
_animationController.forward();
}
});
}
delay after each repeat :
#override
void initState() {// <-- add initstate
super.initState();
_animationController.forward(from: 0.0);
}
late final AnimationController _animationController = AnimationController(
vsync: this,
duration: const Duration(seconds: 1))
//..repeat(reverse: true); // <-- comment this line
..addStatusListener((AnimationStatus status) { // <-- add listener
if (status == AnimationStatus.completed) {
Future.delayed(
Duration(milliseconds: _animationController.value == 0 ? 500 : 0),
() {
_animationController
.animateTo(_animationController.value == 0 ? 1 : 0);
});
}
});

Slide child widget in Row layout container from its current position

I have a Row that consists x number of Button widgets. So when user clicks on one of the Button I need to fade out the other buttons and the slide the clicked button to the starting position(0,0) of the Row layout. I managed to get the fade out animation to work.
final _opacityTween = Tween<double>(begin: 1, end: 0);
_opacityAnimation = _opacityTween.animate(CurvedAnimation(
parent: _controller,
curve: new Interval(0.00, 0.50, curve: Curves.linear)
// curve: Curves.ease
));
// And using the opacityAnimation value in Opacity widget.
return Opacity(
opacity: _opacityAnimation.value,
child: child,
);
However I have been unsuccessful to using Animation classes and widgets to slide the child widget to the start position of the Row. Basically I am not sure what kind of classes could be useful to achieve such animation. I can use the Tween and set the end position to (0, 0) and use Translation to achieve it, however I am not sure how to set the begin position of the Animation when I initialise it as the position of a child is not known.
Try this solution. I thing it is almost what you need. You just have to add SizeTransition widget.
Ask if you have any question.
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
int _pressedButton;
Animation<double> _animation;
AnimationController _controller;
#override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
_animation = Tween<double>(begin: 1.0, end: 0.0)
.animate(_controller)
..addListener(() {
setState(() { });
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Test"),
),
body: Column(
children: <Widget>[
Row(
children: <Widget>[
_animatedButton(1),
_animatedButton(2),
_animatedButton(3),
_animatedButton(4)
]
),
Text(_pressedButton != null ? "$_pressedButton button pressed" : "No button pressed"),
RaisedButton(
child: Text("Reset"),
onPressed: () {
_controller.reset();
}
)
]
)
);
}
Widget _animatedButton(int button) {
if (button != _pressedButton) {
return SizeTransition(
sizeFactor: _animation,
axis: Axis.horizontal,
child: Opacity(
opacity: _animation.value,
child: _button(button)
)
);
} else {
return _button(button);
}
}
Widget _button(int button) {
return RaisedButton(
onPressed: () => onButtonClick(button),
child: Text("Button $button")
);
}
void onButtonClick(int button) {
setState(() {
_pressedButton = button;
});
_controller.forward();
}
}
UPDATE 1
Check one more way to do what you need. There are few different things in code below.
Change SingleTickerProviderStateMixin to TickerProviderStateMixin.
Use two animation controllers and animations (for fading out of unselected buttons and sliding left selected button).
Run animations sequentially (see animation tweens listeners).
Add bool _buttonSelected to track completion of animations.
Use _buttonSelected to build correct widget (Widget _buttonsWidget())
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
int _pressedButton = -1;
bool _buttonSelected = false;
Animation<double> _animationFadeOut;
AnimationController _controllerFadeOut;
Animation<double> _animationSlideLeft;
AnimationController _controllerSlideLeft;
#override
void initState() {
super.initState();
_initFadeOutAnimation();
_initSlideLeftAnimation();
}
void _initFadeOutAnimation() {
_controllerFadeOut = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
_animationFadeOut = Tween<double>(begin: 1.0, end: 0.0)
.animate(_controllerFadeOut)
..addListener(() {
setState(() {
if (_controllerFadeOut.isCompleted && !_controllerSlideLeft.isAnimating) {
_controllerSlideLeft.forward();
}
});
});
}
void _initSlideLeftAnimation() {
_controllerSlideLeft = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
_animationSlideLeft = Tween<double>(begin: 1.0, end: 0.0)
.animate(_controllerSlideLeft)
..addListener(() {
setState(() {
if (_controllerSlideLeft.isCompleted) {
_buttonSelected = true;
}
});
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Test"),
),
body: Column(
children: <Widget>[
_buttonsWidget(),
Text(_pressedButton != null ? "$_pressedButton button pressed" : "No button pressed"),
RaisedButton(
child: Text("Reset"),
onPressed: () {
_controllerFadeOut.reset();
_controllerSlideLeft.reset();
_pressedButton = -1;
_buttonSelected = false;
}
)
]
)
);
}
Widget _buttonsWidget() {
if (_buttonSelected) {
return Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[_buttonWidget(_pressedButton)]
);
} else {
return Row(
children: <Widget>[
_animatedButtonWidget(1),
_animatedButtonWidget(2),
_animatedButtonWidget(3),
_animatedButtonWidget(4)
]
);
}
}
Widget _animatedButtonWidget(int button) {
if (button == _pressedButton) {
return _buttonWidget(button);
} else {
return SizeTransition(
sizeFactor: _animationSlideLeft,
axis: Axis.horizontal,
child: Opacity(
opacity: _animationFadeOut.value,
child: _buttonWidget(button)
)
);
}
}
Widget _buttonWidget(int button) {
return RaisedButton(
onPressed: () => _onButtonClick(button),
child: Text("Button $button")
);
}
void _onButtonClick(int button) {
setState(() {
_pressedButton = button;
});
_controllerFadeOut.forward();
}
}