欢迎访问移动开发之家(rcyd.net),关注移动开发教程。移动开发之家  移动开发问答|  每日更新
页面位置 : > > > 内容正文

Flutter之TabBarView组件项目实战示例,

来源: 开发者 投稿于  被查看 2704 次 评论:276

Flutter之TabBarView组件项目实战示例,


目录
  • TabBarView
  • TabBar
  • TabBarView+项目实战
    • 1 构建导航头部搜索框
    • 2 构建导航头部TabBar
    • 3 构建导航底部TabBarView容器
    • 4 构建导航底部结构填充
    • 5 构建导航底部结构轮播图
    • 6 构建导航底部结构信息流
  • 总结

    TabBarView

    TabBarView 是 Material 组件库中提供了 Tab 布局组件,通常和 TabBar 配合使用。

    TabBarView 封装了 PageView,它的构造方法:

     TabBarView({
      Key? key,
      required this.children, // tab 页
      this.controller, // TabController
      this.physics,
      this.dragStartBehavior = DragStartBehavior.start,
    }) 
    

    TabController 用于监听和控制 TabBarView 的页面切换,通常和 TabBar 联动。如果没有指定,则会在组件树中向上查找并使用最近的一个 DefaultTabController

    TabBar

    TabBar 为 TabBarView 的导航标题,如下图所示

    TabBar 有很多配置参数,通过这些参数我们可以定义 TabBar 的样式,很多属性都是在配置 indicator 和 label,拿上图来举例,Label 是每个Tab 的文本,indicator 指 “新闻” 下面的白色下划线。

    const TabBar({
      Key? key,
      required this.tabs, // 具体的 Tabs,需要我们创建
      this.controller,
      this.isScrollable = false, // 是否可以滑动
      this.padding,
      this.indicatorColor,// 指示器颜色,默认是高度为2的一条下划线
      this.automaticIndicatorColorAdjustment = true,
      this.indicatorWeight = 2.0,// 指示器高度
      this.indicatorPadding = EdgeInsets.zero, //指示器padding
      this.indicator, // 指示器
      this.indicatorSize, // 指示器长度,有两个可选值,一个tab的长度,一个是label长度
      this.labelColor, 
      this.labelStyle,
      this.labelPadding,
      this.unselectedLabelColor,
      this.unselectedLabelStyle,
      this.mouseCursor,
      this.onTap,
      ...
    }) 
    

    TabBar 通常位于 AppBar 的底部,它也可以接收一个 TabController ,如果需要和 TabBarView 联动, TabBarTabBarView 使用同一个 TabController 即可,注意,联动时 TabBarTabBarView 的孩子数量需要一致。如果没有指定 controller,则会在组件树中向上查找并使用最近的一个 DefaultTabController 。另外我们需要创建需要的 tab 并通过 tabs 传给 TabBar, tab 可以是任何 Widget,不过Material 组件库中已经实现了一个 Tab 组件,我们一般都会直接使用它:

    const Tab({
      Key? key,
      this.text, //文本
      this.icon, // 图标
      this.iconMargin = const EdgeInsets.only(bottom: 10.0),
      this.height,
      this.child, // 自定义 widget
    })
    

    注意,textchild 是互斥的,不能同时制定。

    全部代码:

    import 'package:flutter/material.dart';
    /// @Author wywinstonwy
    /// @Date 2022/1/18 9:09 上午
    /// @Description: 
    class MyTabbarView1 extends StatefulWidget {
      const MyTabbarView1({Key? key}) : super(key: key);
      @override
      _MyTabbarView1State createState() => _MyTabbarView1State();
    }
    class _MyTabbarView1State extends State<MyTabbarView1>with SingleTickerProviderStateMixin {
      List<String> tabs =['头条','新车','导购','小视频','改装赛事'];
      late TabController tabController;
      @override
      void initState() {
        // TODO: implement initState
        super.initState();
        tabController = TabController(length: tabs.length, vsync: this);
      }
      @override
      void dispose() {
        tabController.dispose();
        super.dispose();
      }
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('TabbarView',textAlign: TextAlign.center,),
            bottom:TabBar(
                unselectedLabelColor: Colors.white.withOpacity(0.5),
                labelColor: Colors.white,
                // indicatorSize:TabBarIndicatorSize.label,
                indicator:const UnderlineTabIndicator(),
                controller: tabController,
                tabs: tabs.map((e){
                  return Tab(text: e,);
                }).toList()) ,
          ),
          body: Column(
          children: [
            Expanded(
              flex: 1,
              child:  TabBarView(
                controller: tabController,
                children: tabs.map((e){
                  return Center(child: Text(e,style: TextStyle(fontSize: 50),),);
                }).toList()),)
          ],),
        );
      }
    }
    

    运行效果:

    滑动页面时顶部的 Tab 也会跟着动,点击顶部 Tab 时页面也会跟着切换。为了实现 TabBar 和 TabBarView 的联动,我们显式创建了一个 TabController,由于 TabController 又需要一个 TickerProvider (vsync 参数), 我们又混入了 SingleTickerProviderStateMixin;

    由于 TabController 中会执行动画,持有一些资源,所以我们在页面销毁时必须得释放资源(dispose)。综上,我们发现创建 TabController 的过程还是比较复杂,实战中,如果需要 TabBar 和 TabBarView 联动,通常会创建一个 DefaultTabController 作为它们共同的父级组件,这样它们在执行时就会从组件树向上查找,都会使用我们指定的这个 DefaultTabController。

    我们修改后的实现如下:

    class TabViewRoute2 extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        List tabs = ["新闻", "历史", "图片"];
        return DefaultTabController(
          length: tabs.length,
          child: Scaffold(
            appBar: AppBar(
              title: Text("App Name"),
              bottom: TabBar(
                tabs: tabs.map((e) => Tab(text: e)).toList(),
              ),
            ),
            body: TabBarView( //构建
              children: tabs.map((e) {
                return KeepAliveWrapper(
                  child: Container(
                    alignment: Alignment.center,
                    child: Text(e, textScaleFactor: 5),
                  ),
                );
              }).toList(),
            ),
          ),
        );
      }
    }
    

    可以看到我们无需去手动管理 Controller 的生命周期,也不需要提供 SingleTickerProviderStateMixin,同时也没有其它的状态需要管理,也就不需要用 StatefulWidget 了,这样简单很多。

    TabBarView+项目实战

    实现导航信息流切换效果并缓存前面数据:

    1 构建导航头部搜索框

    import 'package:flutter/material.dart';
    import 'package:qctt_flutter/constant/colors_definition.dart';
    enum SearchBarType { home, normal, homeLight }
    class SearchBar extends StatefulWidget {
      final SearchBarType searchBarType;
      final String hint;
      final String defaultText;
      final void Function()? inputBoxClick;
      final void Function()? cancelClick;
      final ValueChanged<String>? onChanged;
      SearchBar(
          {this.searchBarType = SearchBarType.normal,
          this.hint = '搜一搜你感兴趣的内容',
          this.defaultText = '',
          this.inputBoxClick,
          this.cancelClick,
          this.onChanged});
      @override
      _SearchBarState createState() => _SearchBarState();
    }
    class _SearchBarState extends State<SearchBar> {
      @override
      Widget build(BuildContext context) {
        return Container(
          color: Colors.white,
          height: 74,
          child: searchBarView,
        );
      }
      Widget get searchBarView {
        if (widget.searchBarType == SearchBarType.normal) {
          return _genNormalSearch;
        }
        return _homeSearchBar;
      }
      Widget get _genNormalSearch {
        return Container(
            color: Colors.white,
            padding: EdgeInsets.only(top: 40, left: 20, right: 60, bottom: 5),
            child: Container(
              height: 30,
              decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(6),
                  color: Colors.grey.withOpacity(0.5)),
              padding: EdgeInsets.only(left: 5, right: 5),
              child: Row(
                children: [
                  const Icon(
                    Icons.search,
                    color: Colors.grey,
                    size: 24,
                  ),
                  Container(child: _inputBox),
                  const Icon(
                    Icons.clear,
                    color: Colors.grey,
                    size: 24,
                  )
                ],
              ),
            ),);
      }
      //可编辑输入框
      Widget get _homeSearchBar{
        return  Container(
          padding: EdgeInsets.only(top: 40, left: 20, right: 40, bottom: 5),
          decoration: BoxDecoration(gradient: LinearGradient(
              colors: [mainColor,mainColor.withOpacity(0.2)],
              begin:Alignment.topCenter,
              end: Alignment.bottomCenter
          )),
          child: Container(
            height: 30,
            decoration: BoxDecoration(
                borderRadius: BorderRadius.circular(6),
                color: Colors.grey.withOpacity(0.5)),
            padding: EdgeInsets.only(left: 5, right: 5),
            child: Row(
              children: [
                const Icon(
                  Icons.search,
                  color: Colors.grey,
                  size: 24,
                ),
                Container(child: _inputBox),
              ],
            ),
          ),);
      }
     //构建文本输入框
      Widget get _inputBox {
        return Expanded(
          child: TextField(
            style: const TextStyle(
                fontSize: 18.0, color: Colors.black, fontWeight: FontWeight.w300),
            decoration: InputDecoration(
    //                   contentPadding: EdgeInsets.fromLTRB(1, 3, 1, 3),
    //                   contentPadding: EdgeInsets.only(bottom: 0),
                contentPadding:
                    const EdgeInsets.symmetric(vertical: 0, horizontal: 12),
                border: InputBorder.none,
                hintText: widget.hint,
                hintStyle: TextStyle(fontSize: 15),
                enabledBorder: const OutlineInputBorder(
                  // borderSide: BorderSide(color: Color(0xFFDCDFE6)),
                  borderSide: BorderSide(color: Colors.transparent),
                  borderRadius: BorderRadius.all(Radius.circular(4.0)),
                ),
                focusedBorder: const OutlineInputBorder(
                    borderRadius: BorderRadius.all(Radius.circular(8)),
                    borderSide: BorderSide(color: Colors.transparent))),
          ),
        );
        ;
      }
    }
    

    通常一个应该会出现多出输入框,但是每个地方的输入框样式和按钮功能类型会有一定的区别,可以通过初始化传参的方式进行区分。如上面事例中enum SearchBarType { home, normal, homeLight }枚举每个功能页面出现SearchBar的样式和响应事件。

    2 构建导航头部TabBar

    //导航tabar 关注 头条 新车 ,,。
    _buildTabBar() {
      return TabBar(
          controller: _controller,
          isScrollable: true,//是否可滚动
          labelColor: Colors.black,//文字颜色
          labelPadding: const EdgeInsets.fromLTRB(20, 0, 10, 5),
          //下划线样式设置
          indicator: const UnderlineTabIndicator(
            borderSide: BorderSide(color: Color(0xff2fcfbb), width: 3),
            insets: EdgeInsets.fromLTRB(0, 0, 0, 10),
          ),
          tabs: tabs.map<Tab>((HomeChannelModel model) {
            return Tab(
              text: model.name,
            );
          }).toList());
    }
    

    因为Tabbar需要和TabBarView进行联动,需要定义一个TabController进行绑定

    3 构建导航底部TabBarView容器

    //TabBarView容器 信息流列表
    _buildTabBarPageView() {
      return KeepAliveWrapper(child:Expanded(
          flex: 1,
          child: Container(
            color: Colors.grey.withOpacity(0.3),
            child: TabBarView(
              controller: _controller,
              children: _buildItems(),
            ),
          )));
    }
    

    4 构建导航底部结构填充

    底部内容结构包含轮播图左右切换,信息流上下滚动,下拉刷新,上拉加载更多、刷新组件用到SmartRefresher,轮播图和信息流需要拼接,需要用CustomScrollView

    代码如下:

    _buildRefreshView() {
      //刷新组件
      return SmartRefresher(
        controller: _refreshController,
        enablePullDown: true,
        enablePullUp: true,
        onLoading: () async {
          page++;
          print('onLoading $page');
          //加载频道数据
          widget.homeChannelModel.termId == 0 ? _getTTHomeNews() : _getHomeNews();
        },
        onRefresh: () async {
          page = 1;
          print('onRefresh $page');
          //加载频道数据
          widget.homeChannelModel.termId == 0 ? _getTTHomeNews() : _getHomeNews();
        },
        //下拉头部UI样式
        header: const WaterDropHeader(
          idleIcon: Icon(
            Icons.car_repair,
            color: Colors.blue,
            size: 30,
          ),
        ),
        //上拉底部UI样式
        footer: CustomFooter(
          builder: (BuildContext context, LoadStatus? mode) {
            Widget body;
            if (mode == LoadStatus.idle) {
              body = const Text("pull up load");
            } else if (mode == LoadStatus.loading) {
              body = const CupertinoActivityIndicator();
            } else if (mode == LoadStatus.failed) {
              body = const Text("Load Failed!Click retry!");
            } else if (mode == LoadStatus.canLoading) {
              body = const Text("release to load more");
            } else {
              body = const Text("No more Data");
            }
            return Container(
              height: 55.0,
              child: Center(child: body),
            );
          },
        ),
        //customScrollview拼接轮播图和信息流。
        child: CustomScrollView(
          slivers: [
            SliverToBoxAdapter(
                    child: _buildFutureScroll()
                  ),
            SliverList(
              delegate: SliverChildBuilderDelegate((content, index) {
                NewsModel newsModel = newsList[index];
                return _buildChannelItems(newsModel);
              }, childCount: newsList.length),
            )
          ],
        ),
      );
    }
    

    5 构建导航底部结构轮播图

    轮播图单独封装SwiperView小组件

    //首页焦点轮播图数据获取
    _buildFutureScroll(){
      return FutureBuilder(
          future: _getHomeFocus(),
          builder: (BuildContext context, AsyncSnapshot&lt;FocusDataModel&gt; snapshot){
            print('轮播图数据加载 ${snapshot.connectionState} 对应数据:${snapshot.data}');
            Container widget;
            switch(snapshot.connectionState){
              case ConnectionState.done:
                if(snapshot.data != null){
                  widget = snapshot.data!.focusList!.isNotEmpty?Container(
                    height: 200,
                    width: MediaQuery.of(context).size.width,
                    child: SwiperView(snapshot.data!.focusList!,
                        MediaQuery.of(context).size.width),
                  ):Container();
                }else{
                  widget = Container();
                }
                break;
              case ConnectionState.waiting:
                widget = Container();
                break;
              case ConnectionState.none:
                widget = Container();
                break;
              default :
                widget = Container();
                break;
            }
            return widget;
          });
    }
    

    轮播图组件封装,整体基于第三方flutter_swiper_tv

    import "package:flutter/material.dart";
    import 'package:flutter_swiper_tv/flutter_swiper.dart';
    import 'package:qctt_flutter/http/api.dart';
    import 'package:qctt_flutter/models/home_channel.dart';
    import 'package:qctt_flutter/models/home_focus_model.dart';
    class SwiperView extends StatelessWidget {
      // const SwiperView({Key? key}) : super(key: key);
      final double width;
      final List<FocusItemModel> items;
      const SwiperView(this.items,this.width,{Key? key}) : super(key: key);
      @override
      Widget build(BuildContext context) {
        return Swiper(
          itemCount: items.length,
          itemWidth: width,
          containerWidth: width,
          itemBuilder: (BuildContext context,int index){
            FocusItemModel focusItemModel = items[index];
            return Stack(children: [
              Container(child:Image.network(focusItemModel.picUrlList![0],fit: BoxFit.fitWidth,width: width,))
            ],
            );
          },
          pagination: const SwiperPagination(),
          // control: const SwiperControl(),
        );
      }
    }
    

    6 构建导航底部结构信息流

    信息流比较多,每条信息流样式各一,具体要根据服务端返回的数据进行判定。如本项目不至于22种样式,

      _buildChannelItems(NewsModel model) {
        //0,无图,1单张小图 3、三张小图 4.大图推广 5.小图推广 6.专题(统一大图)
    // 8.视频小图,9.视频大图 ,,11.banner广告,12.车展,
    // 14、视频直播 15、直播回放 16、微头条无图 17、微头条一图
    // 18、微头条二图以上 19分组小视频 20单个小视频 22 文章折叠卡片(关注频道)
        switch (model.style) {
          case '1':
            return GestureDetector(
              child: OnePicArticleView(model),
              onTap: ()=>_jumpToPage(model),
            );
          case '3':
            return GestureDetector(
              child: ThreePicArticleView(model),
              onTap: ()=>_jumpToPage(model),
            );
          case '4':
            return GestureDetector(
              child: AdBigPicView(newsModel: model,),
                onTap: ()=>_jumpToPage(model),) ;
          case '9':
            return GestureDetector(
              child: Container(
              padding: const EdgeInsets.only(left: 10, right: 10),
              child: VideoBigPicView(model),
            ),
            onTap: ()=>_jumpToPage(model),
            );
          case '15':
            return GestureDetector(
              child: Container(
                width: double.infinity,
                padding: const EdgeInsets.only(left: 10, right: 10),
                child: LiveItemView(model),
              ),
              onTap: ()=>_jumpToPage(model),
            );
          case '16'://16、微头条无图
            return GestureDetector(
              child: Container(
                width: double.infinity,
                padding: const EdgeInsets.only(left: 10, right: 10),
                child: WTTImageView(model),
              ),
              onTap: ()=>_jumpToPage(model),
            );
          case '17'://17、微头条一图
            return GestureDetector(
              child: Container(
                width: double.infinity,
                padding: const EdgeInsets.only(left: 10, right: 10),
                child: WTTImageView(model),
              ),
              onTap:()=> _jumpToPage(model),
            );
          case '18'://18、微头条二图以上
            //18、微头条二图以上
            return GestureDetector(
              child: Container(
                width: double.infinity,
                padding: const EdgeInsets.only(left: 10, right: 10),
                child: WTTImageView(model),
              ),
              onTap: ()=>_jumpToPage(model),
            );
          case '19': //19分组小视频
            return Container(
              width: double.infinity,
              padding: const EdgeInsets.only(left: 10, right: 10),
              child: SmallVideoGroupView(model.videoList),
            );
          case '20':
          //20小视频 左上方带有蓝色小视频标记
            return Container(
              padding: const EdgeInsets.only(left: 10, right: 10),
              child: VideoBigPicView(model),
            );
          default:
            return Container(
              height: 20,
              color: Colors.blue,
            );
        }
      }
    

    每种样式需要单独封装Cell组件视图。

    通过_buildChannelItems(NewsModel model)方法返回的是单独的Cell视图,需要提交给对应的list进行组装:

    SliverList(
      delegate: SliverChildBuilderDelegate((content, index) {
        NewsModel newsModel = newsList[index];
        return _buildChannelItems(newsModel);
      }, childCount: newsList.length),
    )
    

    这样整个App首页的大体结构就完成了,包含App顶部搜索,基于Tabbar的头部频道导航。TabbarView头部导航联动。CustomScrollView对轮播图信息流进行拼接,等。网络数据是基于Dio进行了简单封装,具体不在这里细说。具体接口涉及隐私,不展示。

    至于底部BottomNavigationBar会在后续组件介绍的时候详细介绍到。

    总结

    本章主要介绍了TabBarView的基本用法以及实际复杂项目中TabBarView的组合使用场景,更多关于Flutter TabBarView组件的资料请关注3672js教程其它相关文章!

    您可能感兴趣的文章:
    • Flutter之可滚动组件实例详解
    • Flutter之 ListView组件使用示例详解
    • Flutter使用 input chip 标签组件示例详解
    • Drawer Builder组件实现flutter侧边抽屉效果示例分析
    • UI 开源组件Flutter图表范围选择器使用详解
    • Flutter控制组件显示和隐藏三种方式详解

    用户评论