I have a simple Card with an "Expand" button that toggles visibility of an _expandedCardBody method that returns a Column.
bool expandedCard = false;
Widget _expandedCardBody() {
return Column(
children: <Widget>[
Row(...),
Row(...),
Row(...),
]);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
padding: EdgeInsets.all(10),
child: ListView(
children: <Widget>[
Text("Setup my budget", style: kTitleStyle),
Card(
color: Colors.white,
child: Column(
children: <Widget>[
ListTile(
leading: SvgPicture.asset(
"assets/images/icon-eating-out.svg",
width: 65),
title: Text('Eating out', style: kNormalStyle),
subtitle:
Text('AED 1,245 this month', style: kSmallStyle),
trailing: SizedBox(
width: 75,
child: TextFormField(
keyboardType: TextInputType.number,
decoration: InputDecoration(hintText: "AED.."),
style: kNormalStyle,
),
),
),
expandedCard ? _expandedCardBody() : SizedBox(),
Divider(),
Container(
alignment: Alignment.topLeft,
child: FlatButton(
child: Text(expandedCard ? 'COLLAPSE' : 'EXPAND'),
onPressed: () {
setState(() {
expandedCard = !expandedCard;
});
},
),
),
],
),
)
],
)));
}
It works, but this is what it looks like:
Instead of simply showing/hiding _expandedCardBody, I'd like to animate it's height.
I've tried using AnimatedContainer like so, but it requires knowing the height of the _expandedCardBody (which I do not).
AnimatedContainer(
child: _expandedCardBody(),
height: expandedCard ? 200 : 0, // 🤔 don't know what the height is.
duration: Duration(milliseconds: 250),
),
How can I animate the height of the Card body?
Use SingleTickerProvider with your state class
for eg like this :
class YourClass extends State<YourClass>
with SingleTickerProviderStateMixin {
AnimationController _animationController;
Animation<double> animation;
var isDetailOpened = false;
#override
void initState() {
_animationController =
AnimationController(duration: Duration(milliseconds: 200), vsync: this);
_animationController.addListener(() {
setState(() {});
});
seeMoreEnabled = false;
animation =
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut);
super.initState();
}
#override
dispose() {
_animationController.dispose();
super.dispose();
}
then Replace your Text (Expand or collapse) with flatButton or CupertinoButton
after doing that onPress or onClick method
onPressed: () {
if (isDetailOpened) {
_animationController.reverse();
} else {
_animationController.forward();
}
isDetailOpened = !isDetailOpened;
},
after that Put your widgets in SizeTransition
SizeTransition(
axisAlignment: 1.0,
sizeFactor: animation,
(your other widgets)
This is an example for an ideal purpose (in simply way)
Related
I'm trying to add a custom dropdown menu whose items are just links to other pages
I tried using DropdownButton
But I failed to make its elements as a link and it requires a value, and I do not have a value to pass to it
thank you
You can use OverlayEntry for this case. Below is a simple working example of a dropdown using OverlayEntry:
class TestDropdownWidget extends StatefulWidget {
TestDropdownWidget({Key? key}) : super(key: key);
#override
_TestDropdownWidgetState createState() => _TestDropdownWidgetState();
}
class _TestDropdownWidgetState extends State<TestDropdownWidget>
with TickerProviderStateMixin {
final LayerLink _layerLink = LayerLink();
late OverlayEntry _overlayEntry;
bool _isOpen = false;
//Controller Animation
late AnimationController _animationController;
late Animation<double> _expandAnimation;
#override
void dispose() {
super.dispose();
_animationController.dispose();
}
#override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 200),
);
_expandAnimation = CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
);
}
#override
Widget build(BuildContext context) {
return CompositedTransformTarget(
link: _layerLink,
child: InkWell(
onTap: _toggleDropdown,
child: Text('Click Me'), //Define your child here
),
);
}
OverlayEntry _createOverlayEntry() {
return OverlayEntry(
builder: (context) => GestureDetector(
onTap: () => _toggleDropdown(close: true),
behavior: HitTestBehavior.translucent,
// full screen container to register taps anywhere and close drop down
child: SizedBox(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: Stack(
children: [
Positioned(
left: 100,
top: 100.0,
width: 250,
child: CompositedTransformFollower(
//use offset to control where your dropdown appears
offset: Offset(0, 20),
link: _layerLink,
showWhenUnlinked: false,
child: Material(
elevation: 2,
borderRadius: BorderRadius.circular(6),
borderOnForeground: true,
color: Colors.white,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6),
border: Border.all(color: Colors.grey),
),
child: SizeTransition(
axisAlignment: 1,
sizeFactor: _expandAnimation,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
//These are the options that appear in the dropdown
Text('Option 1'),
Text('Option 2'),
Text('Option 3'),
Text('Option 4'),
Text('Option 5'),
],
),
),
),
),
),
),
],
),
),
),
);
}
void _toggleDropdown({
bool close = false,
}) async {
if (_isOpen || close) {
_animationController.reverse().then((value) {
_overlayEntry.remove();
if (mounted) {
setState(() {
_isOpen = false;
});
}
});
} else {
_overlayEntry = _createOverlayEntry();
Overlay.of(context)!.insert(_overlayEntry);
setState(() => _isOpen = true);
_animationController.forward();
}
}
}
Here's a gif to show the ui:
Imagine Facebook mobile app, where you tap on the notification about someone like your comment. The app will open the appropriate screen, scroll you down to the comment, and after you arrive there, the comment row will flash yellow for a while, rapidly turn transparent, and then it's done.
I just want to make the same flashing animation to a ListView/Column element to let users know that something is happening there as a result of their action. But from what I gathered, to create just a simple animation like that needs a complex elaborate contraption with Animation widgets.
There's a widget that does a much appreciated fade animation called FadeInImage. I just need to provide destination URL, placeholder image asset, and the widget will handle the rest. I'm wondering if there's such alternative where I can just provide a key to a widget, and then call from anywhere: rowKey.currentState.flash(color: Colors.yellow). Or perhaps a way to let me tell the ListView or Column to flash certain row like listViewKey.currentState.items[5].flash(color: Colors.yellow).
There is no a Widget like you are looking for, but you can create a custom widget if you know the Flutter basics. You will be able to build from simple animations to the most advanced ones.
I made a simple example, a list of elements where you can select any element from the list (index).
When you open the screen, you will see the scroll animation, after that, the blink animation will start.
class FlashingHome extends StatelessWidget {
const FlashingHome({Key? key}) : super(key: key);
void _goToWidget(BuildContext context, int index) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => FlashingList(index: index),
),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
MaterialButton(
color: Colors.greenAccent,
child: const Text('Go to element 5'),
onPressed: () => _goToWidget(context, 5),
),
MaterialButton(
color: Colors.greenAccent,
child: const Text('Go to element 10'),
onPressed: () => _goToWidget(context, 10),
),
],
),
),
);
}
}
class FlashingList extends StatefulWidget {
const FlashingList({required this.index, Key? key}) : super(key: key);
final int index;
#override
State<FlashingList> createState() => _FlashingListState();
}
class _FlashingListState extends State<FlashingList>
with SingleTickerProviderStateMixin {
final _scrollController = ScrollController();
late final AnimationController _animationController;
final _itemSize = 150.0;
Timer? _timer;
Future<void> _startScrolling() async {
await _scrollController.animateTo(
_itemSize * widget.index,
duration: const Duration(seconds: 1),
curve: Curves.easeOut,
);
// after the scroll animation finishes, start the blinking
_animationController.repeat(reverse: true);
// the duration of the blinking
_timer = Timer(const Duration(seconds: 3), () {
setState(() {
_animationController.stop();
_timer?.cancel();
});
});
}
#override
void initState() {
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
);
WidgetsBinding.instance!.addPostFrameCallback((_) => _startScrolling());
super.initState();
}
#override
void dispose() {
_timer?.cancel();
_animationController.dispose();
_scrollController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flashing List'),
),
body: ListView.builder(
controller: _scrollController,
itemCount: 15,
itemExtent: 150,
itemBuilder: (context, index) {
final item = Padding(
padding: const EdgeInsets.all(20.0),
child: Text('My Item :$index'),
);
return Padding(
padding: const EdgeInsets.all(4.0),
child: FittedBox(
child: index == widget.index && _animationController.isDismissed
? FadeTransition(
opacity: _animationController,
child: Container(
color: Colors.yellow,
child: item,
),
)
: Container(
color: Colors.grey[200],
child: item,
),
),
);
},
),
);
}
}
Result:
Now that you know how to create an automatic scrolling list, animated item, you can customize this with a more complex animation and extract into a custom widget that you can reuse in your projects.
Reference: https://docs.flutter.dev/development/ui/animations
Try shimmer
While the data is being fetched, you can create a simple animation which will give the feel that something's loading. Here's a simple example.
I am using FAB onPress to reflect the changes.
bool isApiCallProcess = false;
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
isApiCallProcess = !isApiCallProcess;
});
},
),
backgroundColor: Colors.white,
body: (isApiCallProcess == false)
? CircleAvatar(
backgroundColor:
Colors.black12,
radius: 40,
backgroundImage: AssetImage(
'images/dosa.jpg',
),
):
Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Wrap(
children: [
Column(
children: [
const CircleAvatar(
radius: 40,
backgroundImage: AssetImage(
'',
),
),
const SizedBox(
height: 10,
),
Container(
decoration: ShapeDecoration(
color: Colors.grey[400]!,
shape: const
RoundedRectangleBorder(),
),
height: 12,
width: 60,
),
],
),
],
),
),
),
);
Here's the screenshots :
Am working on a button that animates like Gmail Compose Button. The behavior is such that on scrolling up it shrinks to a circle with icon at center while on scrolling downwards it expands to show icon and text. My implantation works well, but the issue now is that I want a fade like effect for the text such that after the FAB button expands, the text fades in smoothly rather than appearing abruptly.
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
bool isLoaded = false;
bool upDirection = true, flag = true;
ScrollController _scrollController = ScrollController();
#override
void initState() {
// TODO: implement initState
super.initState();
_scrollController
..addListener(() {
upDirection = _scrollController.position.userScrollDirection ==
ScrollDirection.forward;
// makes sure we don't call setState too much, but only when it is needed
if (upDirection != flag) setState(() {});
flag = upDirection;
});
}
#override
Widget build(BuildContext context) {
return new Scaffold(
body: Stack(
children: [
SingleChildScrollView(
controller: _scrollController,
child: Stack(
children: [
Positioned(
bottom: MediaQuery.of(context).size.height * 0.1,
right: 20.0,
child: AnimatedContainer(
width: flag ? 170 : 56,
height: 56,
duration: Duration(milliseconds: 300),
child: FloatingActionButton.extended(
backgroundColor: AppColors.customFabRed,
heroTag: null,
onPressed: () {
},
icon: flag
? Icon(
Icons.call,
color: Colors.white,
)
: null,
label: flag
? AnimatedOpacity( //trying to get the text to fade in after the fab is expanded but nothing happens
opacity: flag ? 1.0 : 0.0,
duration: const Duration(milliseconds: 9000),
child: Text(
'Call a doctor',
textAlign: TextAlign.start,
style: TextStyle(
fontSize: 13.5,
height: 1.5,
fontWeight: FontWeight.w400,
fontFamily: 'Euclid',
color: Colors.white,
),
),
)
: Icon(
Icons.call,
color: Colors.white,
))),
),
],
),
);
}
}
Try below code hope it helps to you :
Declare one Boolean variable
bool isFABExtended = false;
Create function for button action change:
void _switchButton() {
setState(
() {
isFABExtended = !isFABExtended;
},
);
}
Declare your Widget:
floatingActionButton: FloatingActionButton.extended(
onPressed: _switchButton,
label: AnimatedSwitcher(
duration: Duration(seconds: 1),
transitionBuilder: (Widget child, Animation<double> animation) =>
FadeTransition(
opacity: animation,
child: SizeTransition(
child: child,
sizeFactor: animation,
axis: Axis.horizontal,
),
),
child: isFABExtended
? Icon(Icons.check)
: Row(
children: [
Padding(
padding: const EdgeInsets.only(right: 4.0),
child: Icon(Icons.add),
),
Text("Add Button")
],
),
),
),
Your Button Look like and
In my app, I have a container that I want to start rotating with a slow-curve on a click, then keep rotation, and then the next click will make it stop with a slow-curve.
How do I make a curve-animation in flutter?
Somthing like this:
https://miro.medium.com/max/960/1*ZLekwO4QthfAWlBgM-9vpA.gif
Make an animation controller and an animation.
AnimationController _animController;
Animation<double> _animation;
#override
void initState() {
super.initState();
_animController = AnimationController(
duration: Duration(seconds: 2),
vsync: this,
);
_animation =
Tween<double>(begin: 0, end: 2 * math.pi).animate(_animController)
..addListener(() {
setState(() {});
});
}
#override
void dispose() {
_animController.dispose();
super.dispose();
}
Define a boolean variable. This variable indicates whether the object is animating or not.
var _animating = false;
End the animation on Stop and repeat the animation on Start.
Scaffold(
backgroundColor: Colors.blueGrey,
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Transform.rotate(
angle: _animation.value,
child: Container(
color: Colors.green,
height: 80,
width: 80,
padding: EdgeInsets.all(30),
),
),
Padding(
padding: const EdgeInsets.only(top: 20.0),
child: RaisedButton(
color: Colors.white,
child: Text(_animating ? "Stop" : "Start"),
onPressed: () {
if (_animating) {
_animController.animateTo(1,
duration: Duration(seconds: 3), curve: Curves.ease);
} else {
_animController.repeat();
}
setState(() => _animating = !_animating);
},
),
),
],
),
),
)
Result:
in this class as an widget which i use animation to show and hide, when i multiple show or hiding by click, sometimes i get this error:
BoxConstraints has a negative minimum width.The offending constraints were: BoxConstraints(w=-0.8, 0.0<=h<=Infinity; NOT NORMALIZED)
this part of code has problem
child: Row( //<--- problem cause on this line of code
children: <Widget>[
...
],
),
My class code:
class FollowingsStoryList extends StatefulWidget {
final List<Stories> _stories;
FollowingsStoryList({#required List<Stories> stories}) : _stories = stories;
#override
_FollowingsStoryListState createState() => _FollowingsStoryListState();
}
class _FollowingsStoryListState extends State<FollowingsStoryList> with TickerProviderStateMixin {
List<Stories> get stories => widget._stories;
bool _isExpanded = false;
AnimationController _barrierController;
#override
void initState() {
super.initState();
_barrierController = AnimationController(duration: const Duration(milliseconds: 400), vsync: this);
}
#override
Widget build(BuildContext context) {
return AnimatedContainer(
curve: _isExpanded ? Curves.elasticOut : Curves.elasticIn,
duration: Duration(milliseconds: 800),
child: Row(
children: <Widget>[
AnimatedContainer(
curve: _isExpanded ? Curves.elasticOut : Curves.elasticIn,
duration: Duration(milliseconds: 800),
width: _isExpanded ? 90 : 1,
color: Colors.white,
padding: EdgeInsets.only(bottom: 65.0),
child: Column(
children: <Widget>[
],
),
),
GestureDetector(
onTap: _toggleExpanded,
child: StoriesShapeBorder(
alignment: Alignment(1, 0.60),
icon: Icons.adjust,
color: Colors.white,
barWidth: 4,
),
),
],
),
);
}
_toggleExpanded() {
setState(() {
_isExpanded = !_isExpanded;
if (_isExpanded) {
_barrierController.forward();
} else {
_barrierController.reverse();
}
});
}
}
Because Curves.elasticOut and Curves.elasticIn are
oscillating curves that grows/shrinks in magnitude while overshooting
its bounds.
When _isExpanded is false, you are expecting the width to go from 90 to 1 however Curves.elasticOut overshoots and becomes much less than 1 (hence a negative width) for a short period of time.
This is what is causing the error imho. Plus for what reason do you have nested AnimatedContainers? Top AnimatedContainer seems unnecessary.
Try getting rid of the top AnimatedContainer and changing all 4 of the Curves to Curves.linear and I think your error will be gone. Actually you can use any Curves which don't overshoot.
If you really need Elastic Curve, then maybe this would work too:
#override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
AnimatedContainer(
curve: _isExpanded ? Curves.elasticOut : Curves.easeInSine,
duration: Duration(milliseconds: 800),
width: _isExpanded ? 90 : 1,
color: Colors.white,
padding: EdgeInsets.only(bottom: 65.0),
child: Column(
children: <Widget>[
],
),
),
GestureDetector(
onTap: _toggleExpanded,
child: StoriesShapeBorder(
alignment: Alignment(1, 0.60),
icon: Icons.adjust,
color: Colors.white,
barWidth: 4,
),
),
],
);
}
Try it and let me know.
For example, you can use AnimatedController, to (oddly enough) control your animation value and set minimum value "0".
import 'dart:math' as math;
double getValue(...) {
// some logic / curves
// ....
return math.max(0, value);
}