I have a list in the body and bottom navigation bar. I want to hide bottom navigation bar with a slide down animation when the posts list is scrolled down and visib
Working code with BottomNavigationBar
.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
ScrollController _hideBottomNavController;
bool _isVisible;
@override
initState() {
super.initState();
_isVisible = true;
_hideBottomNavController = ScrollController();
_hideBottomNavController.addListener(
() {
if (_hideBottomNavController.position.userScrollDirection ==
ScrollDirection.reverse) {
if (_isVisible)
setState(() {
_isVisible = false;
});
}
if (_hideBottomNavController.position.userScrollDirection ==
ScrollDirection.forward) {
if (!_isVisible)
setState(() {
_isVisible = true;
});
}
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: CustomScrollView(
controller: _hideBottomNavController,
shrinkWrap: true,
slivers: <Widget>[
SliverPadding(
padding: const EdgeInsets.all(10.0),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => _getItem(context),
childCount: 20,
),
),
),
],
),
),
bottomNavigationBar: AnimatedContainer(
duration: Duration(milliseconds: 500),
height: _isVisible ? 56.0 : 0.0,
child: Wrap(
children: <Widget>[
BottomNavigationBar(
type: BottomNavigationBarType.fixed,
backgroundColor: Colors.blue,
fixedColor: Colors.white,
unselectedItemColor: Colors.white,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('Home'),
),
BottomNavigationBarItem(
icon: Icon(Icons.card_giftcard),
title: Text('Offers'),
),
BottomNavigationBarItem(
icon: Icon(Icons.account_box),
title: Text('Account'),
),
],
),
],
),
),
);
}
_getItem(BuildContext context) {
return Card(
elevation: 3,
margin: EdgeInsets.all(8),
child: Row(
children: <Widget>[
Expanded(
child: Container(
padding: EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'Item',
style:
TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
)
],
),
),
),
],
),
);
}
}
Working Model
While Naveen's solution works perfectly, I didn't like the idea of using setState
to handle the visibility of the navbar. Every time the user would change scroll direction, the entire homepage including the appbar and body would rebuild which can be an expensive operation. I created a separate class to handle the visibility that uses a ValueNotifier
to track the current hidden status.
class HideNavbar {
final ScrollController controller = ScrollController();
ValueNotifier<bool> visible = ValueNotifier<bool>(true);
HideNavbar() {
visible.value = true;
controller.addListener(
() {
if (controller.position.userScrollDirection ==
ScrollDirection.reverse) {
if (visible.value) {
visible.value = false;
}
}
if (controller.position.userScrollDirection ==
ScrollDirection.forward) {
if (!visible.value) {
visible.value = true;
}
}
},
);
}
}
Now all you do is create a final instance of HideNavbar
in your HomePage widget.
final HideNavbar hiding = HideNavbar();
Now pass the instance's ScrollController
to the ListView
or CustomScrollView
body of your Scaffold
.
body: CustomScrollView(
controller: hiding.controller,
...
Then surround your bottomNavigationBar
with a ValueListenableBuilder
that takes the ValueNotifier
from the HideNavbar
instance and the set the height property of the bottomNavigationBar to be either 0 or any other value depending on the status of the ValueNotifier
.
bottomNavigationBar: ValueListenableBuilder(
valueListenable: hiding.visible,
builder: (context, bool value, child) => AnimatedContainer(
duration: Duration(milliseconds: 500),
height: value ? 56.0 : 0.0,
child: Wrap(
children: <Widget>[
BottomNavigationBar(
type: BottomNavigationBarType.fixed,
backgroundColor: Colors.blue,
fixedColor: Colors.white,
unselectedItemColor: Colors.white,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('Home'),
),
BottomNavigationBarItem(
icon: Icon(Icons.card_giftcard),
title: Text('Offers'),
),
BottomNavigationBarItem(
icon: Icon(Icons.account_box),
title: Text('Account'),
),
],
),
],
),
),
),
This approaches keeps the HomePage as a StatelessWidget, avoids countless rebuilds, and doesn't require any external libraries. You can also implement this as a stream-based approach but that would require another library such as dart:async
and would not be changing anything.
Here is the code.
void main() => runApp(MaterialApp(home: Scaffold(body: MyApp())));
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
ScrollController _scrollController;
double _containerMaxHeight = 56, _offset, _delta = 0, _oldOffset = 0;
@override
void initState() {
super.initState();
_offset = 0;
_scrollController = ScrollController()
..addListener(() {
setState(() {
double offset = _scrollController.offset;
_delta += (offset - _oldOffset);
if (_delta > _containerMaxHeight)
_delta = _containerMaxHeight;
else if (_delta < 0) _delta = 0;
_oldOffset = offset;
_offset = -_delta;
});
});
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
return Stack(
alignment: Alignment.bottomCenter,
children: <Widget>[
ListView.builder(
physics: ClampingScrollPhysics(),
controller: _scrollController,
itemCount: 20,
itemBuilder: (context, index) => ListTile(title: Text(index.toString())),
),
Positioned(
bottom: _offset,
width: constraints.maxWidth,
child: Container(
width: double.infinity,
height: _containerMaxHeight,
color: Colors.grey[300],
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
_buildItem(Icons.home, "Home"),
_buildItem(Icons.blur_circular, "Collection"),
_buildItem(Icons.supervised_user_circle, "Community"),
_buildItem(Icons.notifications, "Notifications"),
],
),
),
),
],
);
},
);
}
Widget _buildItem(IconData icon, String title) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 28),
Text(title, style: TextStyle(fontSize: 10)),
],
);
}
}
Note: I tried bottomNavigationBar
but things were not working as expected so I created kind of my own bottom navigation bar and you can modify the code further for your use.