继续创造,加快成长!这是我参与「日新计划 10 月更文挑战」的第30天,点击查看活动概况
TabBarView 是 Material 组件库中提供了 Tab 布局组件,一般和 TabBar 配合运用。
TabBarView
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
联动, TabBar
和 TabBarView
运用同一个 TabController
即可,留意,联动时 TabBar
和 TabBarView
的孩子数量需求一起。如果没有指定 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
})
留意,text
和 child
是互斥的,不能一起拟定。
全部代码:
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<FocusDataModel> 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的组合运用场景。