Bootstrap

Flutter 实战开发-导航栏

本文主要通过flutter实现底部导航栏与顶部卡片切换效果,本文demo地址

一、底部导航栏

底部导航栏的应用场景非常多,基本每个app都要用到,常用于app登录后的顶级页面托管

在flutter中,底部导航栏的创建主要用到了

1,State状态管理

2,BottomNavigationBar底部导航组件

3,IndexedStack缓存组件

1.1、新建入口页面

app入口文件(main.dart)

import 'package:flutter/material.dart';

import 'BottomTabBar.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: BottomTabBar(),
    );
  }
}

1.2、底部导航栏组件

首先我们需要定义状态变量与页面绑定

定义currentIndex:当前页面index,
定义pageList:具体业务页面列表

int currentIndex = 0;
List pageList = [FirstPage(), SecondPage(), ThirdPage()];

底部导航栏的组件ui样式,我们使用系统提供的BottomNavigationBar,BottomNavigationBar组件api如下

image-20210831092217950

具体实现如下(BottomTabBar.dart)

import 'package:flutter/material.dart';

import 'tabs/category/CategoryPage.dart';
import 'tabs/home/HomePage.dart';
import 'tabs/mine/MinePage.dart';
import 'tabs/setting/SettingPage.dart';

class BottomTabBar extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _BottomTabBarState();
  }
}

class _BottomTabBarState extends State<BottomTabBar> {
  int currentIndex = 0;
  List pageList = [HomePage(), CategoryPage(), SettingPage(), MinePage()];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('导航栏'),
      ),
      body: this.pageList[this.currentIndex],
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: this.currentIndex,
        onTap: (int index) {
          setState(() {
            this.currentIndex = index;
          });
        },
        type: BottomNavigationBarType.fixed, //配置底部tabs可以有
        items: [
          BottomNavigationBarItem(icon: Icon(Icons.home), title: Text('首页')),
          BottomNavigationBarItem(icon: Icon(Icons.category), title: Text('分类')),
          BottomNavigationBarItem(icon: Icon(Icons.settings), title: Text('设置')),
          BottomNavigationBarItem(icon: Icon(Icons.people), title: Text('我的')),
        ],
      ),
    );
  }
}

1.3、其他页面处理

对于加载的其他业务页面(CategoryPage.dart、HomePage.dart、MinePage.dart、SettingPage.dart),处理如下

import 'package:flutter/material.dart';

class MinePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text('我的'),
    );
  }
}

1.5、底部TabBar gif演示

这样我们就可以运行出一个简单的底部切换demo

效果如下

底部tabbar

1.4、页面缓存

但是这样存在一个问题

底部Tabbar切换时currentPage会被销毁,重新加载页面。底部Tabbar每切换一次,就请求一次网络,造成了资源浪费,用户体验也比较差。

这里我们使用IndexedStack做缓存,将body的页面调整如下

//调整前
body: this.pageList[this.currentIndex],
//加缓存
body: IndexedStack(
        children: <Widget>[
          ...this.pageList,
        ],
        index: currentIndex,
      ),

1.5、底部导航凸起效果

image-20210906150954468

在Scaffold容器下添加悬浮按钮,覆盖到tabBar上面

floatingActionButton: Container(
  height: 70,
  width: 70,
  padding: EdgeInsets.all(8),
  //设置:内边距8
  margin: EdgeInsets.only(top: 2),
  //设置:外边距2, 这样就可以让浮动按钮下来,贴近分类Tab文字
  decoration: BoxDecoration(
      borderRadius: BorderRadius.circular(40), color: Colors.white),
  child: FloatingActionButton(
      child: Icon(Icons.add),
      onPressed: () {
        setState(() {
          //可以实现重新渲染页面,因为_currentIndex变成了1,所以页面会跳转到分类页面
          this.currentIndex = 1; //点击浮动按钮时,切换到分类页面
        });
      },
      backgroundColor: this.currentIndex == 1
          ? Colors.red
          : Colors.yellow //利用三目运算符,实现选中时,浮动按钮背景颜色变化
      ),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,

二、顶部导航栏(卡片切换)

顶部导航栏使用场景也非常多,常用于单页面里面的卡片切换

2.1、新建入口文件

首先顶部导航栏我们写到首页中(HomePage.dart)

import 'package:flutter/material.dart';

import 'widget/TopTabBar.dart';

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: TopTabBar()
    );
  }
}

2.2、顶部导航栏组件

我们使用TabBarView做缓存,相当于ScrollTabView,组件api如下

image-20210831153419008

具体代码(TopTabBar.dart)

import 'package:flutter/material.dart';

class TopTabBar extends StatefulWidget {
  @override
  TopTabBarState createState() => TopTabBarState();
}

class TopTabBarState extends State<TopTabBar>
    with SingleTickerProviderStateMixin {
  late TabController tabController;
  var tabs = <Text>[];

  @override
  void initState() {
    super.initState();
    tabs = <Text>[
      Text('儿童'),
      Text('女装'),
      Text('百货'),
      Text('美食'),
      Text('美妆'),
    ];

    tabController = TabController(length: tabs.length, vsync: this);
    //.addListenter 可以对 TabController 增加监听,每次发生切换,都能够走到方法中
    this.tabController.addListener(() {
      print(this.tabController.toString());
      print(this.tabController.index);
      print(this.tabController.length);
      print(this.tabController.previousIndex);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: new Column(
        children: <Widget>[
          ConstrainedBox(
            constraints: BoxConstraints(
              minWidth: double.infinity,
              minHeight: Size.fromHeight(kMinInteractiveDimension).height,
            ),
            child: Container(
              color: Colors.blue,
              child: TabBar(
                tabs: tabs,
                controller: tabController,
                onTap: (int index) {
                  print('Selected......$index');
                },
                unselectedLabelColor: Colors.white,
                //设置未选中时的字体颜色,tabs里面的字体样式优先级最高
                unselectedLabelStyle: TextStyle(fontSize: 16),
                //设置未选中时的字体样式,tabs里面的字体样式优先级最高
                labelColor: Colors.red,
                //设置选中时的字体颜色,tabs里面的字体样式优先级最高
                labelStyle: TextStyle(fontSize: 16.0),
                //设置选中时的字体样式,tabs里面的字体样式优先级最高
                isScrollable: false,
                //isScrollable 默认为false 里面标题平分显示 ;true 可以滚动不平分显示
                indicatorColor: Colors.red,
                //选中下划线的颜色
                indicatorSize: TabBarIndicatorSize.label,
                //选中下划线的长度,label时跟文字内容长度一样,tab时跟一个Tab的长度一样
                indicatorWeight: 4.0, //选中下划线的高度,值越大高度越高,默认为2。0
              ),
            ),
          ),
          Expanded(
              child: new TabBarView(
            controller: tabController,
            children: <Widget>[
              ListViewContnet(),
              ListViewContnet(),
              ListViewContnet(),
              ListViewContnet(),
              ListViewContnet(),
            ],
          )),
        ],
      ),
    );
  }
}


class ListViewContnet extends StatelessWidget {
  const ListViewContnet({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    const TITLE = '标题标题标题标题标题标题标题';

    return ListView(
      children: <Widget>[
        ListTile(title: Text(TITLE)),
        ListTile(title: Text(TITLE)),
        ListTile(title: Text(TITLE)),
        ListTile(title: Text(TITLE)),
        ListTile(title: Text(TITLE)),
        ListTile(title: Text(TITLE)),
        ListTile(title: Text(TITLE)),
        ListTile(title: Text(TITLE)),
        ListTile(title: Text(TITLE)),
        ListTile(title: Text(TITLE)),
      ],
    );
  }
}

2.3、顶部固定卡片切换 gif演示

这样就实现了卡片切换效果(可滑动不平均大小),效果如下

不滚动平均

2.4、顶部可滑动卡片 gif演示

新闻中卡片可能有多个动态加载,我们需要顶部可以滑动不平均大小的效果

这里我们更改TabBar的isScrollable属性为true即可。效果如下

滑动不平均
;