如何在Flutter中用CustomPainter实现指定样式的静态BottomNavigationBar
1. 实现思路
不用依赖第三方插件,直接通过Stack+CustomPainter就能搞定:
- 用
CustomPainter绘制固定的曲线背景(静态无变化) - 上层用
Row布局导航按钮,中间按钮对齐曲线凸起位置 - 仅处理按钮选中状态的样式变化,不做动画和页面切换
2. 绘制静态曲线背景
先写一个自定义Painter类,专门绘制底部的曲线背景:
class CurvedBottomBarPainter extends CustomPainter { final Color backgroundColor; final double curveHeight; CurvedBottomBarPainter({ required this.backgroundColor, required this.curveHeight, }); @override void paint(Canvas canvas, Size size) { final paint = Paint()..color = backgroundColor; final path = Path(); // 从左下角开始绘制路径 path.moveTo(0, 0); // 左侧直线到曲线起点 path.lineTo(size.width * 0.4, 0); // 左半段二次贝塞尔曲线(控制凸起弧度) path.quadraticBezierTo( size.width * 0.45, -curveHeight, size.width * 0.5, -curveHeight, ); // 右半段二次贝塞尔曲线 path.quadraticBezierTo( size.width * 0.55, -curveHeight, size.width * 0.6, 0, ); // 右侧直线到右下角 path.lineTo(size.width, 0); // 闭合路径,绘制底部矩形 path.lineTo(size.width, size.height); path.lineTo(0, size.height); path.close(); canvas.drawPath(path, paint); } @override bool shouldRepaint(CustomPainter oldDelegate) => false; // 静态样式,无需重绘 }
3. 组装完整的导航栏组件
写一个可复用的导航栏组件,组合曲线背景和按钮:
class StaticCurvedBottomBar extends StatefulWidget { final List<BottomNavItem> items; final Color backgroundColor; final double curveHeight; final Color selectedItemColor; final Color unselectedItemColor; const StaticCurvedBottomBar({ super.key, required this.items, this.backgroundColor = Colors.white, this.curveHeight = 20, this.selectedItemColor = Colors.blue, this.unselectedItemColor = Colors.grey, }); @override State<StaticCurvedBottomBar> createState() => _StaticCurvedBottomBarState(); } class _StaticCurvedBottomBarState extends State<StaticCurvedBottomBar> { int _selectedIndex = 0; void _onItemTapped(int index) { setState(() { _selectedIndex = index; }); // 不需要页面切换逻辑,按需求留空 } @override Widget build(BuildContext context) { // 导航栏总高度=系统默认高度+曲线凸起高度 final barHeight = kBottomNavigationBarHeight + widget.curveHeight; return SizedBox( height: barHeight, child: Stack( alignment: Alignment.bottomCenter, children: [ // 底层曲线背景 CustomPaint( size: Size(MediaQuery.of(context).size.width, barHeight), painter: CurvedBottomBarPainter( backgroundColor: widget.backgroundColor, curveHeight: widget.curveHeight, ), ), // 上层导航按钮组 Padding( padding: EdgeInsets.only(bottom: widget.curveHeight / 2), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: List.generate(widget.items.length, (index) { final isSelected = index == _selectedIndex; return GestureDetector( onTap: () => _onItemTapped(index), child: Column( mainAxisSize: MainAxisSize.min, children: [ // 图标支持资源图片,直接替换成Image.asset即可 isSelected ? widget.items[index].selectedIcon : widget.items[index].icon, const SizedBox(height: 4), Text( widget.items[index].label, style: TextStyle( color: isSelected ? widget.selectedItemColor : widget.unselectedItemColor, fontSize: 12, ), ), ], ), ); }), ), ), ], ), ); } } // 导航项数据类,支持选中/未选中图标 class BottomNavItem { final Widget icon; final Widget selectedIcon; final String label; BottomNavItem({ required this.icon, required this.selectedIcon, required this.label, }); }
4. 使用示例
在页面中直接调用这个组件:
class HomePage extends StatelessWidget { const HomePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( body: const Center(child: Text('首页内容')), bottomNavigationBar: StaticCurvedBottomBar( backgroundColor: Colors.white, curveHeight: 25, selectedItemColor: Colors.deepPurple, unselectedItemColor: Colors.grey.shade400, items: [ BottomNavItem( icon: Image.asset('assets/home_unselected.png', width:24, height:24), selectedIcon: Image.asset('assets/home_selected.png', width:24, height:24), label: '首页', ), BottomNavItem( icon: Image.asset('assets/search_unselected.png', width:24, height:24), selectedIcon: Image.asset('assets/search_selected.png', width:24, height:24), label: '搜索', ), BottomNavItem( icon: Image.asset('assets/post_unselected.png', width:32, height:32), selectedIcon: Image.asset('assets/post_selected.png', width:32, height:32), label: '发布', ), BottomNavItem( icon: Image.asset('assets/message_unselected.png', width:24, height:24), selectedIcon: Image.asset('assets/message_selected.png', width:24, height:24), label: '消息', ), BottomNavItem( icon: Image.asset('assets/profile_unselected.png', width:24, height:24), selectedIcon: Image.asset('assets/profile_selected.png', width:24, height:24), label: '我的', ), ], ), ); } }
调整说明
- 曲线弧度:修改
quadraticBezierTo的控制点坐标,可以调整曲线的凸起高度和弧度,完全匹配你的设计图 - 资源图片:直接把示例中的
Image.asset换成你的本地资源路径即可 - 静态无动画:因为
CurvedBottomBarPainter的shouldRepaint返回false,曲线永远不会重绘,点击按钮仅切换图标和文字颜色
内容的提问来源于stack exchange,提问作者Mehedi Hasan
相关产品推荐
相关产品推荐

