Flutter之TabBarView組件項(xiàng)目實(shí)戰(zhàn)示例
TabBarView
TabBarView 是 Material 組件庫(kù)中提供了 Tab 布局組件,通常和 TabBar 配合使用。
TabBarView 封裝了 PageView,它的構(gòu)造方法:
TabBarView({ Key? key, required this.children, // tab 頁(yè) this.controller, // TabController this.physics, this.dragStartBehavior = DragStartBehavior.start, })
TabController 用于監(jiān)聽(tīng)和控制 TabBarView 的頁(yè)面切換,通常和 TabBar 聯(lián)動(dòng)。如果沒(méi)有指定,則會(huì)在組件樹(shù)中向上查找并使用最近的一個(gè) DefaultTabController
。
TabBar
TabBar 為 TabBarView 的導(dǎo)航標(biāo)題,如下圖所示
TabBar 有很多配置參數(shù),通過(guò)這些參數(shù)我們可以定義 TabBar 的樣式,很多屬性都是在配置 indicator 和 label,拿上圖來(lái)舉例,Label 是每個(gè)Tab 的文本,indicator 指 “新聞” 下面的白色下劃線(xiàn)。
const TabBar({ Key? key, required this.tabs, // 具體的 Tabs,需要我們創(chuàng)建 this.controller, this.isScrollable = false, // 是否可以滑動(dòng) this.padding, this.indicatorColor,// 指示器顏色,默認(rèn)是高度為2的一條下劃線(xiàn) this.automaticIndicatorColorAdjustment = true, this.indicatorWeight = 2.0,// 指示器高度 this.indicatorPadding = EdgeInsets.zero, //指示器padding this.indicator, // 指示器 this.indicatorSize, // 指示器長(zhǎng)度,有兩個(gè)可選值,一個(gè)tab的長(zhǎng)度,一個(gè)是label長(zhǎng)度 this.labelColor, this.labelStyle, this.labelPadding, this.unselectedLabelColor, this.unselectedLabelStyle, this.mouseCursor, this.onTap, ... })
TabBar
通常位于 AppBar
的底部,它也可以接收一個(gè) TabController
,如果需要和 TabBarView
聯(lián)動(dòng), TabBar
和 TabBarView
使用同一個(gè) TabController
即可,注意,聯(lián)動(dòng)時(shí) TabBar
和 TabBarView
的孩子數(shù)量需要一致。如果沒(méi)有指定 controller
,則會(huì)在組件樹(shù)中向上查找并使用最近的一個(gè) DefaultTabController
。另外我們需要?jiǎng)?chuàng)建需要的 tab 并通過(guò) tabs 傳給 TabBar
, tab 可以是任何 Widget,不過(guò)Material 組件庫(kù)中已經(jīng)實(shí)現(xiàn)了一個(gè) Tab 組件,我們一般都會(huì)直接使用它:
const Tab({ Key? key, this.text, //文本 this.icon, // 圖標(biāo) this.iconMargin = const EdgeInsets.only(bottom: 10.0), this.height, this.child, // 自定義 widget })
注意,text
和 child
是互斥的,不能同時(shí)制定。
全部代碼:
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 =['頭條','新車(chē)','導(dǎo)購(gòu)','小視頻','改裝賽事']; 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()),) ],), ); } }
運(yùn)行效果:
滑動(dòng)頁(yè)面時(shí)頂部的 Tab 也會(huì)跟著動(dòng),點(diǎn)擊頂部 Tab 時(shí)頁(yè)面也會(huì)跟著切換。為了實(shí)現(xiàn) TabBar 和 TabBarView 的聯(lián)動(dòng),我們顯式創(chuàng)建了一個(gè) TabController,由于 TabController 又需要一個(gè) TickerProvider (vsync 參數(shù)), 我們又混入了 SingleTickerProviderStateMixin;
由于 TabController 中會(huì)執(zhí)行動(dòng)畫(huà),持有一些資源,所以我們?cè)陧?yè)面銷(xiāo)毀時(shí)必須得釋放資源(dispose)。綜上,我們發(fā)現(xiàn)創(chuàng)建 TabController 的過(guò)程還是比較復(fù)雜,實(shí)戰(zhàn)中,如果需要 TabBar 和 TabBarView 聯(lián)動(dòng),通常會(huì)創(chuàng)建一個(gè) DefaultTabController 作為它們共同的父級(jí)組件,這樣它們?cè)趫?zhí)行時(shí)就會(huì)從組件樹(shù)向上查找,都會(huì)使用我們指定的這個(gè) DefaultTabController。
我們修改后的實(shí)現(xiàn)如下:
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( //構(gòu)建 children: tabs.map((e) { return KeepAliveWrapper( child: Container( alignment: Alignment.center, child: Text(e, textScaleFactor: 5), ), ); }).toList(), ), ), ); } }
可以看到我們無(wú)需去手動(dòng)管理 Controller 的生命周期,也不需要提供 SingleTickerProviderStateMixin,同時(shí)也沒(méi)有其它的狀態(tài)需要管理,也就不需要用 StatefulWidget 了,這樣簡(jiǎn)單很多。
TabBarView+項(xiàng)目實(shí)戰(zhàn)
實(shí)現(xiàn)導(dǎo)航信息流切換效果并緩存前面數(shù)據(jù):
1 構(gòu)建導(dǎo)航頭部搜索框
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 = '搜一搜你感興趣的內(nèi)容', 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), ], ), ),); } //構(gòu)建文本輸入框 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))), ), ); ; } }
通常一個(gè)應(yīng)該會(huì)出現(xiàn)多出輸入框,但是每個(gè)地方的輸入框樣式和按鈕功能類(lèi)型會(huì)有一定的區(qū)別,可以通過(guò)初始化傳參的方式進(jìn)行區(qū)分。如上面事例中enum SearchBarType { home, normal, homeLight }
枚舉每個(gè)功能頁(yè)面出現(xiàn)SearchBar的樣式和響應(yīng)事件。
2 構(gòu)建導(dǎo)航頭部TabBar
//導(dǎo)航tabar 關(guān)注 頭條 新車(chē) ,,。 _buildTabBar() { return TabBar( controller: _controller, isScrollable: true,//是否可滾動(dòng) labelColor: Colors.black,//文字顏色 labelPadding: const EdgeInsets.fromLTRB(20, 0, 10, 5), //下劃線(xiàn)樣式設(shè)置 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()); }
因?yàn)門(mén)abbar需要和TabBarView
進(jìn)行聯(lián)動(dòng),需要定義一個(gè)TabController
進(jìn)行綁定
3 構(gòu)建導(dǎo)航底部TabBarView容器
//TabBarView容器 信息流列表 _buildTabBarPageView() { return KeepAliveWrapper(child:Expanded( flex: 1, child: Container( color: Colors.grey.withOpacity(0.3), child: TabBarView( controller: _controller, children: _buildItems(), ), ))); }
4 構(gòu)建導(dǎo)航底部結(jié)構(gòu)填充
底部?jī)?nèi)容結(jié)構(gòu)包含輪播圖左右切換,信息流上下滾動(dòng),下拉刷新,上拉加載更多、刷新組件用到SmartRefresher
,輪播圖和信息流需要拼接,需要用CustomScrollView
。
代碼如下:
_buildRefreshView() { //刷新組件 return SmartRefresher( controller: _refreshController, enablePullDown: true, enablePullUp: true, onLoading: () async { page++; print('onLoading $page'); //加載頻道數(shù)據(jù) widget.homeChannelModel.termId == 0 ? _getTTHomeNews() : _getHomeNews(); }, onRefresh: () async { page = 1; print('onRefresh $page'); //加載頻道數(shù)據(jù) 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 構(gòu)建導(dǎo)航底部結(jié)構(gòu)輪播圖
輪播圖單獨(dú)封裝SwiperView小組件
//首頁(yè)焦點(diǎn)輪播圖數(shù)據(jù)獲取 _buildFutureScroll(){ return FutureBuilder( future: _getHomeFocus(), builder: (BuildContext context, AsyncSnapshot<FocusDataModel> snapshot){ print('輪播圖數(shù)據(jù)加載 ${snapshot.connectionState} 對(duì)應(yīng)數(shù)據(jù):${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 構(gòu)建導(dǎo)航底部結(jié)構(gòu)信息流
信息流比較多,每條信息流樣式各一,具體要根據(jù)服務(wù)端返回的數(shù)據(jù)進(jìn)行判定。如本項(xiàng)目不至于22種樣式,
_buildChannelItems(NewsModel model) { //0,無(wú)圖,1單張小圖 3、三張小圖 4.大圖推廣 5.小圖推廣 6.專(zhuān)題(統(tǒng)一大圖) // 8.視頻小圖,9.視頻大圖 ,,11.banner廣告,12.車(chē)展, // 14、視頻直播 15、直播回放 16、微頭條無(wú)圖 17、微頭條一圖 // 18、微頭條二圖以上 19分組小視頻 20單個(gè)小視頻 22 文章折疊卡片(關(guān)注頻道) 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、微頭條無(wú)圖 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小視頻 左上方帶有藍(lán)色小視頻標(biāo)記 return Container( padding: const EdgeInsets.only(left: 10, right: 10), child: VideoBigPicView(model), ); default: return Container( height: 20, color: Colors.blue, ); } }
每種樣式需要單獨(dú)封裝Cell組件視圖。
通過(guò)_buildChannelItems(NewsModel model)
方法返回的是單獨(dú)的Cell視圖,需要提交給對(duì)應(yīng)的list進(jìn)行組裝:
SliverList( delegate: SliverChildBuilderDelegate((content, index) { NewsModel newsModel = newsList[index]; return _buildChannelItems(newsModel); }, childCount: newsList.length), )
這樣整個(gè)App首頁(yè)的大體結(jié)構(gòu)就完成了,包含App頂部搜索,基于Tabbar的頭部頻道導(dǎo)航。TabbarView頭部導(dǎo)航聯(lián)動(dòng)。CustomScrollView
對(duì)輪播圖信息流進(jìn)行拼接,等。網(wǎng)絡(luò)數(shù)據(jù)是基于Dio進(jìn)行了簡(jiǎn)單封裝,具體不在這里細(xì)說(shuō)。具體接口涉及隱私,不展示。
至于底部BottomNavigationBar
會(huì)在后續(xù)組件介紹的時(shí)候詳細(xì)介紹到。
總結(jié)
本章主要介紹了TabBarView的基本用法以及實(shí)際復(fù)雜項(xiàng)目中TabBarView的組合使用場(chǎng)景,更多關(guān)于Flutter TabBarView組件的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
iOS10適配之權(quán)限Crash問(wèn)題的完美解決方案
這篇文章主要為大家詳細(xì)介紹了iOS10適配之權(quán)限Crash問(wèn)題的完美解決方案,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-09-09iOS開(kāi)發(fā)之UIKeyboardTypeNumberPad數(shù)字鍵盤(pán)自定義按鍵
這篇文章主要介紹了iOS開(kāi)發(fā)之UIKeyboardTypeNumberPad數(shù)字鍵盤(pán)自定義按鍵 的相關(guān)資料,需要的朋友可以參考下2016-08-08IOS登錄頁(yè)面動(dòng)畫(huà)、轉(zhuǎn)場(chǎng)動(dòng)畫(huà)開(kāi)發(fā)詳解
本篇文章通過(guò)詳細(xì)的步驟給大家詳細(xì)講述了IOS登錄頁(yè)面動(dòng)畫(huà)、轉(zhuǎn)場(chǎng)動(dòng)畫(huà)開(kāi)發(fā)的詳細(xì)教程,有興趣的朋友參考學(xué)習(xí)下。2018-01-01將多個(gè)字符串高亮顯示之TTTAttributedLabel
本文介紹了將多個(gè)字符串高亮顯示之TTTAttributedLabel。在此需要對(duì)每個(gè)字符串進(jìn)行匹配,可以研究下kmp和bm算法,在這里應(yīng)用了oc自帶的NSRegularExpression 來(lái)進(jìn)行正則表達(dá)式匹配,算是比較簡(jiǎn)單的方法,需要的朋友可以參考下2015-07-07iOS如何獲取設(shè)備型號(hào)的最新方法總結(jié)
在開(kāi)發(fā)中,我們經(jīng)常需要獲取設(shè)備的型號(hào)以進(jìn)行數(shù)據(jù)統(tǒng)計(jì)或者做不同的適配。這篇文章主要給大家介紹了關(guān)于iOS如何獲取設(shè)備型號(hào)的最新方法,需要的朋友可以參考下2018-11-11iOS評(píng)分(評(píng)價(jià))星星圖打分功能
這篇文章主要介紹了iOS評(píng)分(評(píng)價(jià))星星圖打分功能,評(píng)分視圖分為展示和評(píng)分兩種,具體詳情大家可以通過(guò)本文詳細(xì)學(xué)習(xí)2016-11-11解決iOS11圖片下拉放大出現(xiàn)信號(hào)欄白條的bug問(wèn)題
這篇文章主要介紹了iOS11圖片下拉放大出現(xiàn)信號(hào)欄白條的bug問(wèn)題,需要的朋友參考下吧2017-09-09