Flutter实现滚动时隐藏AppBar与底部导航栏的方法
Flutter滚动时自动隐藏AppBar和底部导航栏的实现方案
问题描述
我正在开发一款同时包含AppBar和底部导航栏的Flutter应用,希望实现用户滚动时自动隐藏这两个组件:向上滚动列表时,AppBar滑出顶部视野、底部导航栏滑出底部视野;向下滚动回顶部时,两者重新显示。
当前项目包含两个页面:
- 导航栏页面:编写了底部导航栏和AppBar的代码,其body为用户展示的UI
- ListView页面:包含数据列表的UI
期望效果:
现有代码
导航栏页面(MyNavBarr)
class MyNavBarr extends StatefulWidget { const MyNavBarr({super.key}); @override State<MyNavBarr> createState() => _MyNavBarrState(); } class _MyNavBarrState extends State<MyNavBarr> { int _selectedIndex = 0; static const List<Widget> _widgetOptions = <Widget>[ LiistView(), LiistView(), LiistView(), LiistView(), ]; void _onItemTapped(int index) { setState(() { _selectedIndex = index; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("AppBar"), centerTitle: true, ), body: _widgetOptions.elementAt(_selectedIndex), bottomNavigationBar: SizedBox( height: 65.0, child: BottomNavigationBar( items: const <BottomNavigationBarItem>[ BottomNavigationBarItem( icon: Icon(IconlyLight.image), label: '', ), BottomNavigationBarItem( icon: Icon(IconlyLight.video), label: '', ), BottomNavigationBarItem( icon: Icons.library_music_outlined, label: '', ), BottomNavigationBarItem( icon: Icon(IconlyLight.profile), label: '', ), ], currentIndex: _selectedIndex, showSelectedLabels: true, showUnselectedLabels: false, selectedFontSize: 0.0, type: BottomNavigationBarType.fixed, onTap: _onItemTapped, ), ), ); } }
ListView页面(LiistView)
class LiistView extends StatefulWidget { const LiistView({super.key}); @override State<LiistView> createState() => _LiistViewState(); } class _LiistViewState extends State<LiistView> { final controller = ScrollController(); @override Widget build(BuildContext context) { return Scaffold( body: ListView.builder( itemCount: 80, itemBuilder: (context, index){ return const ListTile( title: Text("Abcd"), ); }, ), ); } }
解决方案代码
导航栏页面(修改后)
class MyNavBarr extends StatefulWidget { const MyNavBarr({super.key}); @override State<MyNavBarr> createState() => _MyNavBarrState(); } class _MyNavBarrState extends State<MyNavBarr> { int _selectedIndex = 0; bool _isVisible = true; late final List<ScrollController> _scrollControllers; @override void initState() { super.initState(); // 为每个页面创建独立的滚动控制器并添加监听 _scrollControllers = List.generate(4, (index) { final controller = ScrollController(); controller.addListener(_handleScroll); return controller; }); } @override void dispose() { // 销毁所有滚动控制器,避免内存泄漏 for (var controller in _scrollControllers) { controller.dispose(); } super.dispose(); } void _handleScroll() { final currentController = _scrollControllers[_selectedIndex]; // 根据滚动方向切换导航栏显示状态 if (currentController.position.userScrollDirection == ScrollDirection.reverse) { if (_isVisible) setState(() => _isVisible = false); } else if (currentController.position.userScrollDirection == ScrollDirection.forward) { if (!_isVisible) setState(() => _isVisible = true); } } void _onItemTapped(int index) { setState(() => _selectedIndex = index); } @override Widget build(BuildContext context) { final appBarHeight = AppBar().preferredSize.height; const bottomNavHeight = 65.0; return Scaffold( body: Stack( children: [ // 内容区域:根据导航栏状态调整上下内边距,避免内容被遮挡 Padding( padding: EdgeInsets.only( top: _isVisible ? appBarHeight : 0, bottom: _isVisible ? bottomNavHeight : 0, ), child: IndexedStack( index: _selectedIndex, children: [ LiistView(controller: _scrollControllers[0]), LiistView(controller: _scrollControllers[1]), LiistView(controller: _scrollControllers[2]), LiistView(controller: _scrollControllers[3]), ], ), ), // 带动画的AppBar AnimatedContainer( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, height: _isVisible ? appBarHeight : 0, child: AppBar( title: const Text("AppBar"), centerTitle: true, elevation: _isVisible ? 4 : 0, ), ), // 带动画的底部导航栏 AnimatedPositioned( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, bottom: _isVisible ? 0 : -bottomNavHeight, left: 0, right: 0, child: SizedBox( height: bottomNavHeight, child: BottomNavigationBar( items: const <BottomNavigationBarItem>[ BottomNavigationBarItem( icon: Icon(IconlyLight.image), label: '', ), BottomNavigationBarItem( icon: Icon(IconlyLight.video), label: '', ), BottomNavigationBarItem( icon: Icons.library_music_outlined, label: '', ), BottomNavigationBarItem( icon: Icon(IconlyLight.profile), label: '', ), ], currentIndex: _selectedIndex, showSelectedLabels: true, showUnselectedLabels: false, selectedFontSize: 0.0, type: BottomNavigationBarType.fixed, onTap: _onItemTapped, ), ), ), ], ), ); } }
ListView页面(修改后)
class LiistView extends StatelessWidget { final ScrollController controller; const LiistView({super.key, required this.controller}); @override Widget build(BuildContext context) { return ListView.builder( controller: controller, itemCount: 80, itemBuilder: (context, index){ return const ListTile( title: Text("Abcd"), ); }, ); } }
关键实现要点
- 移除嵌套Scaffold:ListView页面作为导航栏页面body的一部分,无需嵌套Scaffold,避免布局层级混乱
- 统一管理滚动控制器:为每个页面创建独立的
ScrollController,切换页面时监听当前活跃页面的滚动事件 - 平滑动画过渡:使用
AnimatedContainer和AnimatedPositioned实现导航栏的显示/隐藏动画,300ms过渡时长保证流畅性 - 内容适配:通过
Padding动态调整内容区域的上下间距,确保导航栏隐藏时内容全屏显示,显示时不被遮挡
内容的提问来源于stack exchange,提问作者Jeevan Crasta
相关产品推荐
相关产品推荐

