Flutter TabBar与TabBarView联动及获取当前点击栏目索引

TabBar还有TabBarView都是谷歌flutter官方组件库------Material组件库提供的组件,其中TabBar用于导航切换,TabBarView则是配合其切换显示的对应的视图,官网参考地址:TabBarView class - material library - Dart API

实现一体联动有两种实现方式:使用默认控制器(DefaultTabController)和自定义控制器。使用自定义控制器灵活性更高,但是需要指定TabController的length属性,但有些情况下栏目的实际数据是从网络上异步加载读取的,这个TabController的length无法动态更新或后面重新指定,非常扯淡,具体实现参考:flutter 之 TabBar、TabBarView的使用 - 简书

本文主要介绍在使用DefaultTabController下,实现获取点击当前栏目的索引,包括点击TabBar和滑动TabBarView以及视图状态保持,示例如下:

Kotlin 复制代码
  late int _selectIndex = 0;
  late final ScrollController _scrollController;
  late final NewsPageViewModel _vm = NewsPageViewModel();
  late final _scaffoldKey = GlobalKey<_NewsPageViewState>();

  @override
  void initState() {
    // TODO: implement initState
    super.initState();

    //栏目滑动索引改变
    _scrollController = ScrollController(onDetach: (ScrollPosition position) {
      if (_scaffoldKey.currentContext != null) {
        final controller =
            DefaultTabController.of(_scaffoldKey.currentContext!);
        controller.addListener(() {
          if (controller.index.toDouble() == controller.animation?.value) {
            //loadListDataFor(controller.index);
            debugPrint(controller.index);
          }
        });
      }
    });
  }

@override
  Widget build(BuildContext context) {
    // TODO: implement build
    return BaseView<NewsPageViewModel>(
        viewModel: _vm,
        build: (context, viewModel, child) {
          if (viewModel.state == ViewState.Busy) {
            return BaseView.loadingWidget();
          } else {
            return DefaultTabController(
                initialIndex: _selectIndex,
                length: viewModel.arrCategory.length,
                child: NestedScrollView(
                  controller: _scrollController,
                  headerSliverBuilder:
                      (BuildContext context, bool innerBoxIsScrolled) {
                    return <Widget>[
                      SliverOverlapAbsorber(
                        handle: NestedScrollView.sliverOverlapAbsorberHandleFor(
                            context),
                        sliver: SliverAppBar(
                          title: Utils.shareInstance.customPageTitle('资讯'),
                          //标题横向间距
                          titleSpacing: 15,
                          //左侧的图标或文字,多为返回箭头
                          leading: null,
                          //左边按钮的宽度
                          leadingWidth: 15,
                          //居中
                          centerTitle: false,
                          //标题栏是否固定
                          pinned: true,
                          //滑动时是否悬浮
                          floating: false,
                          //配合floating使用
                          snap: false,
                          //是否显示在状态栏的下面,false就会占领状态栏的高度
                          primary: true,
                          //设置分栏区域上面的高度
                          // expandedHeight: 0,
                          elevation: 0,
                          //是否显示阴影,直接取值innerBoxIsScrolled,展开不显示阴影,合并后会显示
                          forceElevated: innerBoxIsScrolled,
                          //自定义导航和中间内容的展示
                          flexibleSpace: null,
                          //导航栏背景色
                          backgroundColor: K_APP_NAVIGATION_BACKGROUND_COLOR,
                          //TabBar 分栏标题
                          bottom: _setCategoryTabBar(viewModel),
                        ),
                      )
                    ];
                  },
                  //分栏展示的页面信息
                  body: _setCategoryTabBarView(context, viewModel),
                ));
          }
        },
        onModelReady: (viewModel) {
          //加载栏目
          viewModel.categoryLoad(context, isLoading: false);
        });
  }

@override
  void dispose() {
    _scrollController.dispose();

    // TODO: implement dispose
    super.dispose();
  }

//MARK: - 扩展
extension on _NewsPageViewState {
  //分栏菜单
  TabBar _setCategoryTabBar(NewsPageViewModel viewModel) {
    return TabBar(
      key: _scaffoldKey,
      //设置对齐方式(否则左边有空白)
      tabAlignment: TabAlignment.start,
      //是否允许滚动
      isScrollable: true,
      //未选中的颜色
      unselectedLabelColor: Utils.shareInstance.hexToInt(0x505050),
      //未选中的样式
      unselectedLabelStyle: const TextStyle(fontSize: 15),
      //选中的颜色
      labelColor: K_APP_TINT_COLOR,
      //选中的样式
      labelStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
      //底部滑动条样式
      indicatorSize: TabBarIndicatorSize.label,
      //底部滑动条颜色
      indicatorColor: K_APP_TINT_COLOR,
      //菜单项目
      tabs: viewModel.arrCategory.map((item) {
        return Tab(text: item.name);
      }).toList(),
      //点击事件
      onTap: (index) {
        debugPrint(index);
        //loadListDataFor(index);
        setState(() {
          _selectIndex = index;
        });
      },
    );
  }

  // tabBar 分栏菜单各个页面
  Widget _setCategoryTabBarView(
      BuildContext context, NewsPageViewModel viewModel) {
    return TabBarView(
        children: viewModel.arrCategory.map((item) {
      //设置UI
      return SafeArea(
          top: false,
          bottom: false,
          child: Builder(
            builder: (BuildContext context) {
              return CustomScrollView(
                slivers: [
                  SliverOverlapInjector(
                    handle: NestedScrollView.sliverOverlapAbsorberHandleFor(
                        context),
                  ),
                  SliverPadding(
                    padding: EdgeInsets.zero,
                    sliver: SliverFixedExtentList(
                        delegate: SliverChildBuilderDelegate(
                            (BuildContext context, int index) {
                          final m = viewModel.arrList[index];
                          
                          if (m.className == '快讯') {
                            //7x24快讯
                            return NewsPageViewInformationCell(
                                index, m, context, viewModel);
                          }

                          return NewsPageViewCell(index, m, context, viewModel);
                        }, childCount: viewModel.arrList.length),
                        itemExtent: 50.0 //item高度或宽度,取决于滑动方向
                        ),
                  )
                ],
              );
            },
          ));
    }).toList());
  }

}

上面方法进过实际测试,可以实现点击和滑动时获取当前栏目的索引,以便根据索引执行加载当前页面的栏目数据的业务逻辑。随之会产生新的问题就是栏目来回切换没有保持页面的状态,会重复加载数据,解决思路是借助 AutomaticKeepAliveClientMixin 并设置 wantKeepAlive 为true,在PageView和PageController组合中同样适用,效果如下:

86_1720350605

参考Demo,日拱一卒,持续更新

相关推荐
xiao-xiang15 分钟前
jenkins-通过api获取所有job及最新build信息
前端·servlet·jenkins
C语言魔术师32 分钟前
【小游戏篇】三子棋游戏
前端·算法·游戏
匹马夕阳2 小时前
Vue 3中导航守卫(Navigation Guard)结合Axios实现token认证机制
前端·javascript·vue.js
你熬夜了吗?2 小时前
日历热力图,月度数据可视化图表(日活跃图、格子图)vue组件
前端·vue.js·信息可视化
桂月二二8 小时前
探索前端开发中的 Web Vitals —— 提升用户体验的关键技术
前端·ux
hunter2062069 小时前
ubuntu向一个pc主机通过web发送数据,pc端通过工具直接查看收到的数据
linux·前端·ubuntu
qzhqbb9 小时前
web服务器 网站部署的架构
服务器·前端·架构
刻刻帝的海角9 小时前
CSS 颜色
前端·css
九酒10 小时前
从UI稿到代码优化,看Trae AI 编辑器如何帮助开发者提效
前端·trae
浪浪山小白兔10 小时前
HTML5 新表单属性详解
前端·html·html5