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,日拱一卒,持续更新

相关推荐
会发光的猪。32 分钟前
css使用弹性盒,让每个子元素平均等分父元素的4/1大小
前端·javascript·vue.js
天下代码客1 小时前
【vue】vue中.sync修饰符如何使用--详细代码对比
前端·javascript·vue.js
猫爪笔记1 小时前
前端:HTML (学习笔记)【1】
前端·笔记·学习·html
前端李易安1 小时前
Webpack 热更新(HMR)详解:原理与实现
前端·webpack·node.js
红绿鲤鱼1 小时前
React-自定义Hook与逻辑共享
前端·react.js·前端框架
Domain-zhuo1 小时前
什么是JavaScript原型链?
开发语言·前端·javascript·jvm·ecmascript·原型模式
小丁爱养花2 小时前
前端三剑客(三):JavaScript
开发语言·前端·javascript
ZwaterZ2 小时前
vue el-table表格点击某行触发事件&&操作栏点击和row-click冲突问题
前端·vue.js·elementui·c#·vue
西凉河的葛三叔2 小时前
vue3+elementui-plus el-dialog全局配置点击空白处不关闭弹窗
前端·vue3·elementui-plus
周三有雨2 小时前
【面试题系列Vue07】Vuex是什么?使用Vuex的好处有哪些?
前端·vue.js·面试·typescript