Basically, I have a top navigation bar (menu) and want it to expand on mouse hover. When the onHover
property becomes true
, it should expand and animate downwards, and when the onHover
property becomes false
, it should collapse and animate upwards.
However, when it expands, it's height should be constrained by the size of its child + padding. I don't want to set a specific height to the container.
For reference, this is my code:
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
bool isTopBarHovered = false;
void handleTopBarHover(bool isHovered) {
setState(() {
isTopBarHovered = isHovered;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: MediaQuery.of(context).size.width < 800
? AppBar()
: PreferredSize(
preferredSize: Size(MediaQuery.of(context).size.width, 48.0),
child: TopNavigationBar(onHover: handleTopBarHover),
),
body: Stack(
children: [
Container(),
Positioned(
top: 0,
left: 0,
right: 0,
child: AnimatedSize(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
child: isTopBarHovered
? Container(
color: Colors.black,
child: const Padding(
padding: EdgeInsets.symmetric(vertical: 88.0),
child: Center(
child: Text('Additional Widget'),
),
),
)
: const SizedBox(height: 0),
),
),
],
),
);
}
}
class TopNavigationBar extends StatefulWidget {
final Function(bool) onHover;
const TopNavigationBar({super.key, required this.onHover});
@override
State<TopNavigationBar> createState() => _TopNavigationBarState();
}
class _TopNavigationBarState extends State<TopNavigationBar> {
@override
Widget build(BuildContext context) {
bool isHover = false;
return Container(
color: Colors.black,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
InkWell(
onTap:(){},
onHover: (val) {
setState(() {
isHover = val;
});
widget.onHover(isHover);
},
child: const Padding(
padding: EdgeInsets.symmetric(vertical: 16.0, horizontal: 8.0),
child: Text(
"Item 01",
style: TextStyle(color: Colors.white),
),
),
),
],
),
);
}
}
This works partially, I can only get to make it animate when expanding, when it collapses it just disappears without animating.
This is the explanation and the demo for the @pskink answer
The code before:
Positioned(
top: 0,
left: 0,
right: 0,
child: AnimatedSize(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
child: isTopBarHovered
? Container(
color: Colors.black,
child: const Padding(
padding: EdgeInsets.symmetric(vertical: 88.0),
child: Center(
child: Text('Additional Widget'),
),
),
)
: const SizedBox(height: 0),
),
),
Step:
AnimatedSize
with AnimatedAlign
and set alignment to bottom because you will animate the widget downwards so you must stick it to bottomheightFactor
, Its like sizing the layout bounding box scale, either 1 or 0The code after:
Positioned(
top: 0,
left: 0,
right: 0,
child: AnimatedAlign(
duration: const Duration(milliseconds: 250),
alignment: Alignment.bottomCenter,
heightFactor: isTopBarHovered ? 1 : 0,
curve: Curves.easeInOut,
child: Container(
color: Colors.black,
child: const Padding(
padding: EdgeInsets.symmetric(vertical: 88.0),
child: Center(
child: Text(
'Additional Widget',
style: TextStyle(color: Colors.white),
),
),
),
),
),
),
This is the result:
Thanks to @pskink
This is the final code:
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
bool isTopBarHovered = false;
void handleTopBarHover(bool isHovered) {
setState(() {
isTopBarHovered = isHovered;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: MediaQuery.of(context).size.width < 800
? AppBar()
: PreferredSize(
preferredSize: Size(MediaQuery.of(context).size.width, 48.0),
child: TopNavigationBar(onHover: handleTopBarHover),
),
body: Stack(
children: [
Container(),
Positioned(
top: 0,
left: 0,
right: 0,
child: AnimatedAlign(
duration: const Duration(milliseconds: 250),
alignment: Alignment.bottomCenter,
heightFactor: isTopBarHovered ? 1 : 0,
curve: Curves.easeInOut,
child: Container(
color: Colors.black,
child: const Padding(
padding: EdgeInsets.symmetric(vertical: 88.0),
child: Center(
child: Text(
'Additional Widget',
style: TextStyle(color: Colors.white),
),
),
),
),
),
),
],
),
);
}
}
class TopNavigationBar extends StatefulWidget {
final Function(bool) onHover;
const TopNavigationBar({super.key, required this.onHover});
@override
State<TopNavigationBar> createState() => _TopNavigationBarState();
}
class _TopNavigationBarState extends State<TopNavigationBar> {
@override
Widget build(BuildContext context) {
bool isHover = false;
return Container(
color: Colors.black,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
InkWell(
onTap: () {},
onHover: (val) {
setState(() {
isHover = val;
});
widget.onHover(isHover);
},
child: const Padding(
padding: EdgeInsets.symmetric(vertical: 16.0, horizontal: 8.0),
child: Text(
"Item 01",
style: TextStyle(color: Colors.white),
),
),
),
],
),
);
}
}