如何实现Flutter自定义底部导航栏滚动显隐(仿X/Twitter)
实现X(原Twitter)风格的滚动触发底部导航栏
当前问题
现有自定义底部导航栏CustomBottomNavbar无法实现目标效果,当前表现:
- 应用启动后自动执行弹出动画
- 向上或向下滚动都会隐藏导航栏
- 隐藏后约3秒自动弹出
期望效果
导航栏默认保持隐藏,仅在用户向上滚动时带动画弹出;向下滚动时立即隐藏,完全匹配X(原Twitter)的底部导航栏行为。
解决方案
通过添加滚动监听、区分滚动方向,并调整状态控制逻辑来实现:
修改要点
- 初始状态设为隐藏,移除启动时自动显示的延迟逻辑
- 为父级滚动组件添加滚动监听,记录滚动位置变化判断方向
- 仅在向上滚动时设置导航栏可见,向下滚动时设置隐藏
- 优化动画参数,确保过渡流畅自然
修改后的完整代码
import 'package:flutter/material.dart'; class CustomBottomNavbar extends StatefulWidget { final int currentIndex; final void Function(int index) onTap; // 添加滚动控制器参数,用于监听滚动事件 final ScrollController scrollController; const CustomBottomNavbar({ Key? key, required this.currentIndex, required this.onTap, required this.scrollController, }) : super(key: key); @override _CustomBottomNavbarState createState() => _CustomBottomNavbarState(); } class _CustomBottomNavbarState extends State<CustomBottomNavbar> { bool _isVisible = false; int _tappedIndex = -1; double _previousScrollOffset = 0; @override void initState() { super.initState(); // 初始隐藏导航栏 _isVisible = false; // 添加滚动监听 widget.scrollController.addListener(_handleScroll); } @override void dispose() { // 移除监听,避免内存泄漏 widget.scrollController.removeListener(_handleScroll); super.dispose(); } void _handleScroll() { final currentOffset = widget.scrollController.offset; // 判断滚动方向:向上滚动(currentOffset < _previousScrollOffset) // 同时排除滚动到顶部的情况 if (currentOffset < _previousScrollOffset && currentOffset > 0) { // 向上滚动,显示导航栏 if (!_isVisible) { setState(() { _isVisible = true; }); } } else if (currentOffset > _previousScrollOffset) { // 向下滚动,隐藏导航栏 if (_isVisible) { setState(() { _isVisible = false; }); } } // 更新上一次滚动位置 _previousScrollOffset = currentOffset; } @override Widget build(BuildContext context) { return AnimatedPositioned( duration: const Duration(milliseconds: 300), curve: Curves.easeOut, bottom: _isVisible ? 0 : -80, // 调整隐藏距离为导航栏实际高度,避免过度偏移 left: 0, right: 0, child: Padding( padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 48.0), child: Container( padding: const EdgeInsets.symmetric(horizontal: 8.0), width: double.infinity, decoration: BoxDecoration( borderRadius: BorderRadius.circular(50), color: Colors.grey[200]!.withOpacity(0.95), ), height: 60.0, child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ buildNavItem(Icons.home, 0), buildNavItem(Icons.explore, 1), buildNavItem(Icons.notifications, 2), buildNavItem(Icons.account_circle, 3), ], ), ), ), ); } Widget buildNavItem(IconData icon, int index) { final isSelected = index == widget.currentIndex; double iconSize = _tappedIndex == index ? 40.0 : 32.0; Color iconColor = isSelected ? Colors.grey[900]! : (_tappedIndex == index ? Colors.black : Colors.grey[500]!); return GestureDetector( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( width: 60.0, height: 60.0, child: AnimatedSwitcher( duration: const Duration(milliseconds: 1200), child: Icon( icon, key: ValueKey<IconData>(icon), color: iconColor, size: iconSize, ), ), ), ], ), onTapDown: (_) { setState(() { _tappedIndex = index; }); }, onTapUp: (_) { setState(() { _tappedIndex = -1; }); widget.onTap(index); }, onTapCancel: () { setState(() { _tappedIndex = -1; }); }, ); } }
使用说明
在使用该导航栏的页面中,需要创建一个ScrollController并传递给导航栏,同时将其绑定到页面的滚动组件(如ListView、SingleChildScrollView等):
class HomePage extends StatelessWidget { final ScrollController _scrollController = ScrollController(); @override Widget build(BuildContext context) { return Scaffold( body: ListView( controller: _scrollController, // 页面内容 ), bottomNavigationBar: CustomBottomNavbar( currentIndex: 0, onTap: (index) { // 导航切换逻辑 }, scrollController: _scrollController, ), ); } }
内容的提问来源于stack exchange,提问作者Nakul Bisht
相关产品推荐
相关产品推荐

