Flutter AnimatedSwitcher jumps between children - flutter

I am trying to implement some custom design in an expasion panel list. Therefore, I wanted to create some kind of animation that animates smoothly from one view (e.g. header) to another view (e.g. full info of the tile) that has other dimensions (obviously, full info will be higher than just the header). This is quite easy to implement with an AnimatedContainer. However, I would need the height of the header widget and the full info widget in order to animate between these two heigths. As these values differ between tiles (other info -> maybe other height) and tracking height via global keys is not my preferred solution, I decided to use the much simpler AnimatedSwitcher instead. However, the behavior of my AnimatedSwitcher is quite strange. At first, the other tiles in the ListView (in my example the button) move down instantly and subsequently the tile expands. Has anyone an idea of how I could implement some code in order to achieve the same animation that I would get from AnimatedContainer(button/other tiles moving down simultaniously with the tile expanding)? Thanks in advance for any advice. Here is my code:
class MyPage extends State {
List _items;
int pos;
#override
void initState() {
pos = 0;
_items = [
Container(
color: Colors.white,
width: 30,
key: UniqueKey(),
child: Column(
children: <Widget>[Text('1'), Text('2')], //example that should visualise different heights
),
),
Container(
width: 30,
color: Colors.white,
key: UniqueKey(),
child: Column(
children: <Widget>[Text('1'), Text('2'), Text('44534'), Text('534534')],
),
)
];
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
padding: EdgeInsets.only(top: 100),
children: <Widget>[
AnimatedSwitcher(
duration: Duration(seconds: 1),
transitionBuilder: (child, animation) => ScaleTransition(
child: child,
scale: animation,
),
child: _items[pos],
),
RaisedButton(
child: Text('change'),
onPressed: pos == 0
? () {
setState(() => pos = 1);
}
: () {
setState(() => pos = 0);
})
],
),
);
}
}
The solution was quite simple. Just found out that there exists an AnimatedSize Widget that finds out the size of its children automatically.

I stumbled on this post and since I had a similar problem I decided to create a tutorial here on how to mix AnimatedSwitcher and AnimatedSize to solve this issue. Animations do not happen at the same time but the advantage is that you have full control on the animation provided to the switcher.
I ended up doing this in the end (please note that I'm using BlocBuilder and that AnimatedSizeWidget is a basic implementation of AnimatedSize:
AnimatedSizeWidget(
duration: const Duration(milliseconds: 250),
child: BlocBuilder<SwapCubit, bool>(
builder: (context, state) {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 1000),
child: state
? Icon(Icons.face, size: 80, key: Key("80"))
: Icon(Icons.face, size: 160, key: Key("160")),
);
},
),
),

var isWidgetA = true;
final Widget widgetA = Container(
key: const ValueKey(1),
color: Colors.red,
width: 100,
height: 100,
);
final Widget widgetB = Container(
key: const ValueKey(2),
color: Colors.green,
width: 50,
height: 50,
);
...
AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
transitionBuilder: (Widget child, Animation<double> animation) {
return SizeTransition(
sizeFactor: animation,
child: ScaleTransition(
child: child,
scale: animation,
alignment: Alignment.center,
),
);
},
child: isWidgetA
? widgetA
: widgetB,
),

Related

Flutter - Detect when finger enter in a container

In my interface I have a row of containers like this
.
The idea is that when I pass my finger on these containers, the one under my finger gets bigger (and other changes but that's not the point).
I know how to use GestureDetector and get it bigger when I tap on the container with "onTap". But if you keep your finger down and drag it to another container nothing change. Idealy I'd like to be able to detect when the user pass his finger hover a container while touching the screen.
Appreciate if someone can advise. Thank you in advance!
You can use onVerticalDragUpdate on GestureDetector.
class DraUILien extends StatefulWidget {
const DraUILien({super.key});
#override
State<DraUILien> createState() => _DraUILienState();
}
class _DraUILienState extends State<DraUILien> {
int? activeIndex;
final double containerWidth = 30;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: GestureDetector(
onVerticalDragUpdate: (details) {
activeIndex =
details.localPosition.dx ~/ (containerWidth + 16); //16 padding
setState(() {});
},
child: SizedBox(
height: 200,
child: Row(
children: List.generate(
10,
(index) => Padding(
padding: const EdgeInsets.all(8.0),
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
color: index == activeIndex ? Colors.blue : Colors.grey,
width: containerWidth,
height: index == activeIndex ? 200 : 100,
),
),
),
),
),
)),
);
}
}
Play with the logic for more customization. If you need onTap functionality try including onPanDown

Why isn't my Flutter animated checkmark working?

I'm trying to replicate the following animation from dribble.com:
I've gotten the ScaleTransition to work but the SizeTransition does not. What am I doing wrong or what don't I understand?
When I only swap out the SizeTransition with a FadeTransition (and keep the same controllers & animations), the animations both run. When I move the Center widget from being a child of the SizeTransition to the parent the animation runs.
However it is not properly centered.
import 'package:flutter/material.dart';
class AnimatedCheck extends StatefulWidget {
#override
_AnimatedCheckState createState() => _AnimatedCheckState();
}
class _AnimatedCheckState extends State<AnimatedCheck> with TickerProviderStateMixin {
late AnimationController scaleController = AnimationController(duration: const Duration(milliseconds: 800), vsync: this);
late Animation<double> scaleAnimation = CurvedAnimation(parent: scaleController, curve: Curves.elasticOut);
late AnimationController checkController = AnimationController(duration: const Duration(milliseconds: 400), vsync: this);
late Animation<double> checkAnimation = CurvedAnimation(parent: checkController, curve: Curves.linear);
#override
void initState() {
super.initState();
scaleController.addStatusListener((status) {
if (status == AnimationStatus.completed) {
checkController.forward();
}
});
scaleController.forward();
}
#override
void dispose() {
scaleController.dispose();
checkController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
double circleSize = 140;
double iconSize = 108;
return ScaleTransition(
scale: scaleAnimation,
child: Container(
height: circleSize,
width: circleSize,
decoration: BoxDecoration(
color: Colors.green,
shape: BoxShape.circle,
),
child: SizeTransition(
sizeFactor: checkAnimation,
axis: Axis.horizontal,
axisAlignment: -1,
child: Center(
child: Icon(Icons.check, color: Colors.white, size: iconSize)
)
),
),
);
}
}
When you move the Center to the outside, it doesn't center the children of it's children, just puts it's child in the center of the area it occupies. Try setting the Container alignment using alignment: Alignment.center, inside of the Container. Please let me know if it doesn't work and I'll replicate the code to figure out the problem is.
Edit: This is because there is space around the visual check you see, the SizeTransition doesn't know this so it animates from the edge of the Icon which causes the visual bug you see. I'd recommend you use a tool like Rive (rive.app) to accomplish what you want, alternatively, you can use another animation or use some clunky workaround like animating the position while the size transition occurs so that it appears to be centered.
Hi if this is an issue for you, I figured out how both animations will be triggerd.
See code below.
Stack(
children: [
Center(
child: ScaleTransition(
scale: scaleAnimation,
child: Container(
height: circleSize,
width: circleSize,
decoration: BoxDecoration(
color: kApprovedColor,
shape: BoxShape.circle,
),
),
),
),
SizeTransition(
sizeFactor: checkAnimation,
axis: Axis.horizontal,
axisAlignment: -1,
child: Center(
child: Icon(Icons.check, color: Colors.white, size: iconSize),
),
),
],
),
Flutter's documentation has an explanation:
Like most widgets, SizeTransition will conform to the constraints it
is given, so be sure to put it in a context where it can change size.
For instance, if you place it into a Container with a fixed size, then
the SizeTransition will not be able to change size, and will appear to
do nothing.
Thank you. Now I know what I'm doing wrong.

Flutter "Catch up" animation on drag

I'm currently building a flutter app where a user can drag a circle around the screen. I use a Listener widget to get the current pointer position, then use a transform widget to position it accordingly.
class _DraggingExample extends State<VelocityAnimations>{
var offset = Offset(0, 0);
#override
Widget build(BuildContext context) {
return Scaffold(
// I get pointer-events here.
body: Listener(
onPointerMove: (e) {
setState(() {
// - 50 to center the container
offset = e.localPosition - Offset(50, 50);
});
},
behavior: HitTestBehavior.opaque,
child: SizedBox.expand(
child: Stack(
children: [
Center(child: Text('Hello, World!')),
// I transform the container here.
Transform.translate(
offset: offset,
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(50),
),
),
)
],
),
),
),
);
}
}
It works perfectly. But it's very bland. It would be much nicer if when I move my pointer/finger, the container gradually catches up to the finger. The effect I'm looking for can be seen in this codepen. Here, the black circle gradually catches up with the mouse as you move it along.
I've tried using a Physics Simulation and plain animations, but I can't figure out how to do it.
You can try to use AnimatedPositioned which animates its position implicitly.
The animation can be configured with duration and Curves as you want.
AnimatedPositioned(
left: offset.dx,
top: offset.dy,
curve: Curves.easeOutCirc,
duration: Duration(milliseconds: 400),
child: Container(
// ...
),
),

Resizing parent widget to fit child post 'Transform' in Flutter

I'm using Transforms in Flutter to create a scrolling carousel for selecting from various options.
This uses standard elements such as ListView.builder, which all works fine, aside from the fact that the parent widget of the Transform doesn't scale down to fit the content as seen here:
Here's the code used to generate the 'card' (there was actually a Card in there, but I've stripped it out in an attempt to get everything to scale correctly):
return Align(
child: Transform(
alignment: Alignment.center,
transform: mat,
child: Container(
height: 220,
color: color,
width: MediaQuery.of(context).size.width * 0.7,
child: Text(
offset.toString(),
style: TextStyle(color: Colors.white, fontSize: 12.0),
),
),
),
);
}
Even if I remove the 'height' parameter of the Container (so everything scales to fit the 'Text' widget), the boxes containing the Transform widgets still have the gaps around them.
Flutter doesn't seem to have any documentation to show how to re-scale the parent if the object within is transformed - anyone here knows or has any idea of a workaround?
EDIT: The widget returned from this is used within a build widget in a Stateful widget. The stack is Column > Container > ListView.builder.
If I remove the Transform, the Containers fit together as I'd like - it seems that performing a perspective transform on the Container 'shrinks' it's content (in this case, the color - check the linked screen grab), but doesn't re-scale the Container itself, which is what I'm trying to achieve.
I have a tricky solution for this: addPostFrameCallback + overlay.
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
// ignore: must_be_immutable
class ChildSizeWidget extends HookWidget {
final Widget Function(BuildContext context, Widget child, Size size) builder;
final Widget child;
final GlobalKey _key = GlobalKey();
OverlayEntry _overlay;
ChildSizeWidget({ this.child, this.builder });
#override
Widget build(BuildContext context) {
final size = useState<Size>(null);
useEffect(() {
WidgetsBinding.instance.addPostFrameCallback((timestamp) {
_overlay = OverlayEntry(
builder: (context) => Opacity(
child: SingleChildScrollView(
child: Container(
child: child,
key: _key,
),
),
opacity: 0.0,
),
);
Overlay.of(context).insert(_overlay);
WidgetsBinding.instance.addPostFrameCallback((timestamp) {
size.value = _key.currentContext.size;
_overlay.remove();
});
});
return () => null;
}, [child]);
if (size == null || size.value == null) {
return child;
} else {
return builder(context, child, size.value);
}
}
}
Usage:
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
class HomeView extends HookWidget {
#override
Widget build(BuildContext context) {
final change = useState<bool>(false);
final normal = Container(
color: Colors.blueAccent,
height: 200.0,
width: 200.0,
);
final big = Container(
color: Colors.redAccent,
height: 300.0,
width: 200.0,
);
return Column(
children: [
Container(
alignment: Alignment.center,
child: ChildSizeWidget(
child: change.value ? big : normal,
builder: (context, child, size) => AnimatedContainer(
alignment: Alignment.center,
child: SingleChildScrollView(child: child),
duration: Duration(milliseconds: 250),
height: size.height,
),
),
color: Colors.grey,
),
FlatButton(
child: Text('Toggle child'),
onPressed: () => change.value = !change.value,
color: Colors.green,
),
],
);
}
}
I have a menu with several options, they have different height and with the help of the animations this is ok, it's working really nice for me.
Why are you using Align, as much as I can see in your code, there is no property set or used, to align anything. So try removing Align widget around Transform.
Because according to the documentation, Transform is such a widget that tries to be the same size as their children. So that would satisfy your requirement.
For more info check out this documentation: https://flutter.dev/docs/development/ui/layout/box-constraints
I hope it helps!

Why can't I center a SizeTransition horizontally in a Column in Flutter?

Please check the following code. Regardless what I tried, the SizeTransition is not centered horizontally in the Column. I tried to wrap Column in a Container and then provide infinite width. I tried to wrap SizeTransition in a Center. I tried to wrap SizeTransition in a Container which has center alignment property. I tried to wrap it in a Stack. I tried to give the container child with alignment center property etc... But none of them works...
class MyWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: AnimatedBox(),
);
}
}
class AnimatedBox extends StatefulWidget {
#override
createState() => _AnimatedBoxState();
}
class _AnimatedBoxState extends State<AnimatedBox> with SingleTickerProviderStateMixin {
AnimationController _controller;
#override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(milliseconds: 400),
);
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
RaisedButton(
child: Text('animate forward'),
onPressed: () {_controller.forward();},
),
RaisedButton(
child: Text('animate reverse'),
onPressed: () {_controller.reverse();},
),
const SizedBox(height: 100.0,),
SizeTransition(
child: Container(
width: 200.0,
height: 200.0,
color: Colors.blue,
),
sizeFactor: CurvedAnimation(
curve: Curves.fastOutSlowIn,
parent: _controller,
),
),
],
);
}
}
For instance, the following code does not work for SizeTransition, but works for ScaleTransition. I have no idea what's wrong with SizeTransition.
return Container(
width: double.infinity,
child: Column(
Despite the fact that my previous answer solves the problem to some extent, I also wanted to address how limited SizeTransition widget is and how to solve this.
SizeTransition provides the effect of "unfolding" its content, running the animation either in horizontal or in vertical axis by rewriting alignment settings.
To achieve the same effect without breaking alignment rules, but also avoid using ScaleTransition widget as we need the "unfold/reveal" animation and not "scale up" - here is what I propose:
#override
Widget build(BuildContext context) {
final _animation = CurvedAnimation(parent: _controller, curve: Curves.fastOutSlowIn);
return Column(
children: <Widget>[
// ...,
AnimatedBuilder(
animation: _animation,
builder: (_, child) => ClipRect(
child: Align(
alignment: Alignment.center,
heightFactor: _animation.value,
widthFactor: null,
child: child,
),
),
child: Container(
width: 200.0,
height: 200.0,
color: Colors.blue,
child: Text("test"),
),
)
]
);
}
This is basically an AnimatedBuilder widget with the same ClipRect & Align used as in SizeTransition, except that it does limit alignment to one axis only.
If you'd like the animation to run in both horizontal & vertical axes - assign the same _animation.value to widthFactor property:
Align(
alignment: Alignment.center,
heightFactor: _animation.value,
widthFactor: _animation.value,
child: child,
),
This will help you achieve "reveal from center" effect without scaling up & down the content of your widget.
I can see that you tried many things already, here is some ideas I have:
#1
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: // ...
)
or
#2
Column(
crossAxisAlignment: CrossAxisAlignment.stretch, // critical
children: <Widget>[
// ...,
Container(
alignment: Alignment.center, // critical
child: SizeTransition(
child: Container(
width: 200.0,
height: 200.0,
color: Colors.blue,
),
sizeFactor: CurvedAnimation(
curve: Curves.fastOutSlowIn,
parent: _controller,
),
),
),
]
)
Update
There is indeed a peculiar aspect of SizeTransition widget.
It has axis property that is set to Axis.vetical by default, which overrides the widget's horizontal alignment to -1.0 (start) and vertical alignment to 0.0 (center).
Changing that property to Axis.horizontal makes things work the other way around - aligning the widget horizontally to 0.0 (center) and vertically to -1.0 (start).
Solution:
SizeTransition(
axis: Axis.horizontal,
// ...,
)
Please let me know if this helped.
If you are using animated list view, you can use slide transition instead, just wrap your column around a slide transition widget and you should be good to go
SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, -1), //The animated item will move upwards, use +1 to move downwards
end: Offset.zero,
).animate(
CurvedAnimation(parent: widget.animation, curve: Curves.fastOutSlowIn),
),
child: Column()
)
widget.animation is your Animation<double> variable (if inside stateful widget)