光茫万丈 發表於 2022-10-29 09:01:11

Flutter之TabBarView组件项目实战示例

<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li>TabBarView</li><li>TabBar</li><li>TabBarView+项目实战</li><ul class="second_class_ul"><li>1 构建导航头部搜索框</li><li>2 构建导航头部TabBar</li><li>3 构建导航底部TabBarView容器</li><li>4 构建导航底部结构填充</li><li>5 构建导航底部结构轮播图</li><li>6 构建导航底部结构信息流</li></ul><li>总结</li><ul class="second_class_ul"></ul></ul></div><p class="maodian"></p><h2>TabBarView</h2>
<p>TabBarView 是 Material 组件库中提供了 Tab 布局组件,通常和 TabBar 配合使用。</p>
<p>TabBarView 封装了 PageView,它的构造方法:</p>
<div class="jb51code"><pre class="brush:cpp;"> TabBarView({
Key? key,
required this.children, // tab 页
this.controller, // TabController
this.physics,
this.dragStartBehavior = DragStartBehavior.start,
})
</pre></div>
<p>TabController 用于监听和控制 TabBarView 的页面切换,通常和 TabBar 联动。如果没有指定,则会在组件树中向上查找并使用最近的一个 <code>DefaultTabController</code> 。</p>
<p class="maodian"></p><h2>TabBar</h2>
<p>TabBar 为 TabBarView 的导航标题,如下图所示</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202210/2022102908404001.jpg" /></p>
<p>TabBar 有很多配置参数,通过这些参数我们可以定义 TabBar 的样式,很多属性都是在配置 indicator 和 label,拿上图来举例,Label 是每个Tab 的文本,indicator 指 &ldquo;新闻&rdquo; 下面的白色下划线。</p>
<div class="jb51code"><pre class="brush:cpp;">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,
...
})
</pre></div>
<p><code>TabBar</code> 通常位于 <code>AppBar</code> 的底部,它也可以接收一个 <code>TabController</code> ,如果需要和 <code>TabBarView</code> 联动, <code>TabBar</code> 和 <code>TabBarView</code> 使用同一个 <code>TabController</code> 即可,注意,联动时 <code>TabBar</code> 和 <code>TabBarView</code> 的孩子数量需要一致。如果没有指定 <code>controller</code>,则会在组件树中向上查找并使用最近的一个 <code>DefaultTabController</code> 。另外我们需要创建需要的 tab 并通过 tabs 传给 <code>TabBar</code>, tab 可以是任何 Widget,不过Material 组件库中已经实现了一个 Tab 组件,我们一般都会直接使用它:</p>
<div class="jb51code"><pre class="brush:cpp;">const Tab({
Key? key,
this.text, //文本
this.icon, // 图标
this.iconMargin = const EdgeInsets.only(bottom: 10.0),
this.height,
this.child, // 自定义 widget
})
</pre></div>
<p>注意,<code>text</code> 和 <code>child</code> 是互斥的,不能同时制定。</p>
<p>全部代码:</p>
<div class="jb51code"><pre class="brush:cpp;">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() =&gt; _MyTabbarView1State();
}
class _MyTabbarView1State extends State&lt;MyTabbarView1&gt;with SingleTickerProviderStateMixin {
List&lt;String&gt; 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()),)
      ],),
    );
}
}
</pre></div>
<p>运行效果:</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202210/2022102908404002.gif" /></p>
<p>滑动页面时顶部的 Tab 也会跟着动,点击顶部 Tab 时页面也会跟着切换。为了实现 TabBar 和 TabBarView 的联动,我们显式创建了一个 TabController,由于 TabController 又需要一个 TickerProvider (vsync 参数), 我们又混入了 SingleTickerProviderStateMixin;</p>
<p>由于 TabController 中会执行动画,持有一些资源,所以我们在页面销毁时必须得释放资源(dispose)。综上,我们发现创建 TabController 的过程还是比较复杂,实战中,如果需要 TabBar 和 TabBarView 联动,通常会创建一个 DefaultTabController 作为它们共同的父级组件,这样它们在执行时就会从组件树向上查找,都会使用我们指定的这个 DefaultTabController。</p>
<p>我们修改后的实现如下:</p>
<div class="jb51code"><pre class="brush:cpp;">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) =&gt; Tab(text: e)).toList(),
          ),
      ),
      body: TabBarView( //构建
          children: tabs.map((e) {
            return KeepAliveWrapper(
            child: Container(
                alignment: Alignment.center,
                child: Text(e, textScaleFactor: 5),
            ),
            );
          }).toList(),
      ),
      ),
    );
}
}
</pre></div>
<p>可以看到我们无需去手动管理 Controller 的生命周期,也不需要提供 SingleTickerProviderStateMixin,同时也没有其它的状态需要管理,也就不需要用 StatefulWidget 了,这样简单很多。</p>
<p class="maodian"></p><h2>TabBarView+项目实战</h2>
<p>实现导航信息流切换效果并缓存前面数据:</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202210/2022102908404003.gif" /></p>
<p class="maodian"></p><h3>1 构建导航头部搜索框</h3>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202210/2022102908404004.jpg" /></p>
<div class="jb51code"><pre class="brush:cpp;">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&lt;String&gt;? onChanged;
SearchBar(
      {this.searchBarType = SearchBarType.normal,
      this.hint = '搜一搜你感兴趣的内容',
      this.defaultText = '',
      this.inputBoxClick,
      this.cancelClick,
      this.onChanged});
@override
_SearchBarState createState() =&gt; _SearchBarState();
}
class _SearchBarState extends State&lt;SearchBar&gt; {
@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{
    returnContainer(
      padding: EdgeInsets.only(top: 40, left: 20, right: 40, bottom: 5),
      decoration: BoxDecoration(gradient: LinearGradient(
          colors: ,
          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))),
      ),
    );
    ;
}
}
</pre></div>
<p>通常一个应该会出现多出输入框,但是每个地方的输入框样式和按钮功能类型会有一定的区别,可以通过初始化传参的方式进行区分。如上面事例中<code>enum SearchBarType { home, normal, homeLight }</code>枚举每个功能页面出现SearchBar的样式和响应事件。</p>
<p class="maodian"></p><h3>2 构建导航头部TabBar</h3>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202210/2022102908404005.jpg" /></p>
<div class="jb51code"><pre class="brush:cpp;">//导航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&lt;Tab&gt;((HomeChannelModel model) {
      return Tab(
          text: model.name,
      );
      }).toList());
}
</pre></div>
<p>因为Tabbar需要和<code>TabBarView</code>进行联动,需要定义一个<code>TabController</code>进行绑定</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202210/2022102908404006.jpg" /></p>
<p class="maodian"></p><h3>3 构建导航底部TabBarView容器</h3>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202210/2022102908404007.jpg" /></p>
<div class="jb51code"><pre class="brush:cpp;">//TabBarView容器 信息流列表
_buildTabBarPageView() {
return KeepAliveWrapper(child:Expanded(
      flex: 1,
      child: Container(
      color: Colors.grey.withOpacity(0.3),
      child: TabBarView(
          controller: _controller,
          children: _buildItems(),
      ),
      )));
}
</pre></div>
<p class="maodian"></p><h3>4 构建导航底部结构填充</h3>
<p>底部内容结构包含轮播图左右切换,信息流上下滚动,下拉刷新,上拉加载更多、刷新组件用到<code>SmartRefresher</code>,轮播图和信息流需要拼接,需要用<code>CustomScrollView</code>。</p>
<p>代码如下:</p>
<div class="jb51code"><pre class="brush:cpp;">_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;
            return _buildChannelItems(newsModel);
          }, childCount: newsList.length),
      )
      ],
    ),
);
}
</pre></div>
<p class="maodian"></p><h3>5 构建导航底部结构轮播图</h3>
<p>轮播图单独封装SwiperView小组件</p>
<div class="jb51code"><pre class="brush:cpp;">//首页焦点轮播图数据获取
_buildFutureScroll(){
return FutureBuilder(
      future: _getHomeFocus(),
      builder: (BuildContext context, AsyncSnapshot&amp;lt;FocusDataModel&amp;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;
      });
}
</pre></div>
<p>轮播图组件封装,整体基于第三方<code>flutter_swiper_tv</code></p>
<div class="jb51code"><pre class="brush:cpp;">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&lt;FocusItemModel&gt; 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;
      return Stack(children: [
          Container(child:Image.network(focusItemModel.picUrlList!,fit: BoxFit.fitWidth,width: width,))
      ],
      );
      },
      pagination: const SwiperPagination(),
      // control: const SwiperControl(),
    );
}
}
</pre></div>
<p class="maodian"></p><h3>6 构建导航底部结构信息流</h3>
<p>信息流比较多,每条信息流样式各一,具体要根据服务端返回的数据进行判定。如本项目不至于22种样式,</p>
<div class="jb51code"><pre class="brush:cpp;">_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: ()=&gt;_jumpToPage(model),
      );
      case '3':
      return GestureDetector(
          child: ThreePicArticleView(model),
          onTap: ()=&gt;_jumpToPage(model),
      );
      case '4':
      return GestureDetector(
          child: AdBigPicView(newsModel: model,),
            onTap: ()=&gt;_jumpToPage(model),) ;
      case '9':
      return GestureDetector(
          child: Container(
          padding: const EdgeInsets.only(left: 10, right: 10),
          child: VideoBigPicView(model),
      ),
      onTap: ()=&gt;_jumpToPage(model),
      );
      case '15':
      return GestureDetector(
          child: Container(
            width: double.infinity,
            padding: const EdgeInsets.only(left: 10, right: 10),
            child: LiveItemView(model),
          ),
          onTap: ()=&gt;_jumpToPage(model),
      );
      case '16'://16、微头条无图
      return GestureDetector(
          child: Container(
            width: double.infinity,
            padding: const EdgeInsets.only(left: 10, right: 10),
            child: WTTImageView(model),
          ),
          onTap: ()=&gt;_jumpToPage(model),
      );
      case '17'://17、微头条一图
      return GestureDetector(
          child: Container(
            width: double.infinity,
            padding: const EdgeInsets.only(left: 10, right: 10),
            child: WTTImageView(model),
          ),
          onTap:()=&gt; _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: ()=&gt;_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,
      );
    }
}
</pre></div>
<p>每种样式需要单独封装Cell组件视图。</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202210/2022102908404008.jpg" /></p>
<p>通过<code>_buildChannelItems(NewsModel model)</code>方法返回的是单独的Cell视图,需要提交给对应的list进行组装:</p>
<div class="jb51code"><pre class="brush:cpp;">SliverList(
delegate: SliverChildBuilderDelegate((content, index) {
    NewsModel newsModel = newsList;
    return _buildChannelItems(newsModel);
}, childCount: newsList.length),
)
</pre></div>
<p>这样整个App首页的大体结构就完成了,包含App顶部搜索,基于Tabbar的头部频道导航。TabbarView头部导航联动。<code>CustomScrollView</code>对轮播图信息流进行拼接,等。网络数据是基于Dio进行了简单封装,具体不在这里细说。具体接口涉及隐私,不展示。</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202210/2022102908404009.jpg" /></p>
<p>至于底部<code>BottomNavigationBar</code>会在后续组件介绍的时候详细介绍到。</p>
<p class="maodian"></p><h2>总结</h2>
<p>本章主要介绍了TabBarView的基本用法以及实际复杂项目中TabBarView的组合使用场景,更多关于Flutter TabBarView组件的资料请关注琼殿技术社区其它相关文章!</p>
                           
                            <div class="art_xg">
                              <b>您可能感兴趣的文章:</b><ul><li>flutter TabBarView 动态添加删除页面的示例代码</li></ul>
                            </div>

                        </div>
                        <!--endmain-->
頁: [1]
查看完整版本: Flutter之TabBarView组件项目实战示例