Flutter底部导航栏的实现

1,171 阅读2分钟

简介

本文介绍实现底部导航栏的几种方式和注意事项。

1、使用BottomNavigationBar

1.1 效果图

1.2 代码示例

class MainPage extends StatefulWidget {
  @override
  _MainPageState createState() => _MainPageState();
}

class _MainPageState extends State<MainPage> {
  int _currentPage = 0;//当前页面
  //导航栏每个图标对应的页面
  List<Widget> _pages = <Widget>[    HomePage(),    SocialPage(),    KnowledgePage(),    ShoppingPage()  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(
        children: _pages,
        index: _currentPage,
      ),
      bottomNavigationBar: BottomNavigationBar(
        items: const <BottomNavigationBarItem>[
          //设置导航栏的图标及文字
          BottomNavigationBarItem(icon: Icon(Icons.home), title: Text('首页')),
          BottomNavigationBarItem(icon: Icon(Icons.chat), title: Text('好友')),
          BottomNavigationBarItem(icon: Icon(Icons.description), title: Text('文章')),
          BottomNavigationBarItem(icon: Icon(Icons.shop), title: Text('商城')),
        ],
        currentIndex: _currentPage,
        selectedItemColor: Colors.redAccent,//被选中的图标和文字颜色
        showUnselectedLabels: true,
        type: BottomNavigationBarType.fixed,
        unselectedItemColor: Colors.black45,//未选中的图标和文字颜色
        onTap: _onPageChanged,//点击事件
      ),
    );
  }

  void _onPageChanged(int index) {
    setState(() {
      //通过setState切换页面
      _currentPage = index;
    });
  }

1.3 实现像闲鱼中间按钮凸起的效果

  • 实现方式:使用floatingActionButton组件,位置放在底部中间,_pages的第三个位置加一个Container()占位,同时BottomNavigationBar的items第三个位置不添加icon等,代码如下:
List<Widget> _pages = <Widget>[
    HomePage(),
    SocialPage(),
    Container(), //占位
    KnowledgePage(),
    ShoppingPage()
  ];
//设置floatingActionButton加号
floatingActionButton: GestureDetector(
    behavior: HitTestBehavior.opaque,
    child: Container(
      margin: EdgeInsets.only(top: 25.0),
      child: Image(
        image: AssetImage('images/add.png'),
        height: 67.0,
        width: 67.0,
        fit: BoxFit.cover,
      ),
    ),
    onTap: () {
      //点击事件
    },
  ),
///BottomNavigationBar
items: const <BottomNavigationBarItem>[
//设置导航栏的图标及文字
BottomNavigationBarItem(icon: Icon(Icons.home), title: Text('首页')),
BottomNavigationBarItem(icon: Icon(Icons.chat), title: Text('好友')),
//占位
BottomNavigationBarItem(
   icon: Container(height: 30.0,width: 80.0,),
   activeIcon: Container(height: 30.0,width: 80.0,),
   label: "",
)
BottomNavigationBarItem(icon: Icon(Icons.description), title: Text('文章')),
BottomNavigationBarItem(icon: Icon(Icons.shop), title: Text('商城')),
]

1.4 注意事项

  • body用IndexedStack,避免切换底部标签界面重绘;
  • BottomNavigationBar的showUnselectedLabels属性要设置为true,否则未选中项只显示图片,不显示文字;
  • BottomNavigationBarType有两个值,shifting:切换时对应标签会有偏移动画,fixed:切换时无动画效果,一般用fixed;

1.5 使用PageView代替IndexedStack

  • 使用IndexedStack的弊端:每次打开app,对应children的各个页面都会加载,就会造成不必要的资源浪费,可以使用PageView实现点击某个导航栏再加载对应页面。
//使用PageView和AutomaticKeepAliveClientMixin避免切换底部tab页面重绘
body: PageView.builder(
    controller: _pageController,
    itemCount: _pages.length,
    physics: NeverScrollableScrollPhysics(),
    itemBuilder: (context, index) {
      return _pages[index];
    })
//导航栏对应页面要混入AutomaticKeepAliveClientMixin
class HomePageState extends State<HomePage> with AutomaticKeepAliveClientMixin {}
//build方法要加super.build(context);
@override
  Widget build(BuildContext context) {
    super.build(context);
    return Scaffold();
  }
//实现AutomaticKeepAliveClientMixin的方法并返回true
@override
bool get wantKeepAlive => true;

2、使用BottomAppBar

  • BottomAppBar可以实现不规则底部导航栏

2.1 示例图

实现中间按钮凸起效果:

image.png

2.2 示例代码

  • bottomNavigationBar使用BottomAppBar
  • CustomShape定义了底部导航栏的形状
  • _bottomBar()为两侧按钮布局
bottomNavigationBar: BottomAppBar(
  color: Colors.white,
  elevation: 10,
  shape: CustomShape(),
  child: SizedBox(height: 50, child: _bottomBar()),
)),

image.png

  • 如上图,按钮上方弧线是使用3个二阶贝塞尔曲线连接起来的
class CustomShape extends NotchedShape {
  @override
  Path getOuterPath(Rect host, Rect? guest) {
    Path path = Path();
    var pLeft1 = Offset(host.width / 2 - 45, host.top);
    var pLeft2 = Offset(host.width / 2 - 32, host.top);
    var pLeft3 = Offset(host.width / 2 - 24, host.top - 8);
    var pTop = Offset(host.width / 2, host.top - 28);
    var pRight1 = Offset(host.width / 2 + 24, host.top - 8);
    var pRight2 = Offset(host.width / 2 + 32, host.top);
    var pRight3 = Offset(host.width / 2 + 45, host.top);
    path
      ..moveTo(host.left, host.top)
      ..lineTo(pLeft1.dx, pLeft1.dy)
      ..quadraticBezierTo(pLeft2.dx, pLeft2.dy, pLeft3.dx, pLeft3.dy)
      ..quadraticBezierTo(pTop.dx, pTop.dy, pRight1.dx, pRight1.dy)
      ..quadraticBezierTo(pRight2.dx, pRight2.dy, pRight3.dx, pRight3.dy)
      ..lineTo(host.right, host.top)
      ..lineTo(host.right, host.bottom)
      ..lineTo(host.left, host.bottom)
      ..close();
    return path;
  }
}