
好久沒更新了,主要是最近比較忙,后面我會(huì)盡量把這個(gè)系列盡快完成吧!寫的不好的地方還請(qǐng)各位看官包涵,指出斧正!謝謝!
今天要實(shí)現(xiàn)的是食集界面,下面先分析下這個(gè)界面的構(gòu)成。這個(gè)頁面的構(gòu)成比較復(fù)雜,包含多層視圖嵌套,如果通過原生Android實(shí)現(xiàn)的話估計(jì)需要花點(diǎn)時(shí)間,但是Flutter就相對(duì)來說比較快速了。布局如下圖所示:

從上圖可以看出:
- 頂部是一個(gè)
AppBar,這個(gè)AppBar的背景是張自定義圖片,同時(shí)含有三個(gè)功能區(qū),分別是"+"、搜索框和email。點(diǎn)擊"+"按鈕彈出ModalBottomSheet(后期實(shí)現(xiàn)),點(diǎn)擊搜索區(qū)跳轉(zhuǎn)搜索界面(已實(shí)現(xiàn)),email是一個(gè)actions按鈕。 -
AppBar下方是一個(gè)Container,暫稱"推薦區(qū)",其中包含四個(gè)功能區(qū),分別是Banner推薦展示區(qū)、Channel渠道展示區(qū)、一日三餐展示區(qū)和廣告位展示區(qū)。其中Banner是一個(gè)ListView,Channel是一個(gè)Wrap,三餐區(qū)是由TabBar+PageView構(gòu)成的,廣告位是一個(gè)Swiper。 - 在
推薦區(qū)下方是一個(gè)菜譜推薦區(qū),主要是由TabBar+TabBarView+StaggeredGridView構(gòu)成。
我們需要實(shí)現(xiàn)的效果時(shí)最外層是一個(gè)可滾動(dòng)的視圖widget,然后菜譜推薦區(qū)的TabBar在滾動(dòng)到頂部的時(shí)候需要有吸頂效果,其次菜譜推薦區(qū)的瀑布流也需要可以滾動(dòng),上面的其他可滾動(dòng)Widget也需要聯(lián)動(dòng)滾動(dòng)而不發(fā)生沖突。具體效果可以
這里提供兩種可滾動(dòng)的視圖Widget來實(shí)現(xiàn)上述需要的效果,一種使用NestedScrollView,另一種是使用CustomScrollView,這兩種可滾動(dòng)Widget均作為最外層的Widget。
-
使用NestedScrollView實(shí)現(xiàn)
使用NestedScrollView作為外層Widget時(shí)可以將上述的推薦區(qū)整個(gè)視圖包括菜譜推薦區(qū)的TabBar一道封裝到其headerSliverBuilder中作為其頭部視圖,然后下方的菜譜推薦區(qū)的TabBarView單獨(dú)作為其body部分。大概的結(jié)構(gòu)如下:
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[200],
appBar: _buildAppBar(),
body: _recommendData == null
? LoadingWidget()
: NestedScrollView(
// controller: _scrollController,
headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
_buildSliverAppBar(),
];
},
body: TabBarView(
controller: _tabController,
children: _recipeList,
),
),
);
}
其中_buildSliverAppBar()代碼如下:
Widget _buildSliverAppBar() {
return SliverAppBar(
automaticallyImplyLeading: false, //返回按鈕,不需要
elevation: 0,
pinned: true, //吸頂
floating: true,
expandedHeight: ScreenUtil().setHeight(2600), //展開高度,必選項(xiàng)
flexibleSpace: FlexibleSpaceBar(
collapseMode: CollapseMode.pin,
background: Container(
height: double.infinity,
color: Colors.white,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// _slogan(),
RecommendData(data: _recommendData[1]['video_info']),
Channel(data: _recommendData[2]['channel']),
Meals(data: _recommendData[3]['sancan']),
AdBanner(data: _recommendData[4]['zhuanti']),
],
),
),
),
bottom: PreferredSize(
//修改TabBar吸頂后的背景顏色和高度
child: Material(
color: Colors.white,
child: TabBar(
controller: _tabController,
tabs: _tabTitles,
labelColor: Colors.red,
labelPadding: EdgeInsets.symmetric(horizontal: 2.0),
labelStyle: TextStyle(
fontSize: ScreenUtil().setSp(42),
// color: Colors.black,
fontWeight: FontWeight.bold,
),
unselectedLabelColor: Colors.black54,
unselectedLabelStyle: TextStyle(
fontSize: ScreenUtil().setSp(36),
// color: Colors.black54,
),
indicatorColor: Colors.red,
indicatorSize: TabBarIndicatorSize.label,
indicatorWeight: 4.0,
),
),
preferredSize: Size.fromHeight(50),
),
);
}
flexibleSpace是一個(gè)大小跟SliverAppBar相同但位于其下方且是一個(gè)可伸縮的區(qū)域,這里可以自由添加需要的其他Widget,就像在Container中添加widget一樣,flexibleSpace具體的用法可自行百度或谷歌搜索學(xué)習(xí)。
這種方法有個(gè)弊端,就是expandedHeight的高度必填,否則顯示會(huì)有問題,這就要求出原型圖的時(shí)候把推薦區(qū)的高度必須給確定寫死,否則無法適配達(dá)到預(yù)期效果。
上述具體的源碼請(qǐng)參見food_set_page.dart
-
使用CustomScrollView實(shí)現(xiàn)
使用CustonScrollView實(shí)現(xiàn)上述效果時(shí)需要將推薦區(qū)單獨(dú)封裝成一個(gè)視圖,然后下方的TabBar使用SliverPersistentHeader封裝,這樣其就可以滾動(dòng)到頂部時(shí)實(shí)現(xiàn)吸頂效果了,參考代碼如下:
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[200],
appBar: _buildAppBar(),
body: ChangeNotifierProvider<RecommendModel>(
create: (_) => model,
child: Consumer<RecommendModel>(
builder: (_, model, child) {
return model.recommendData == null
? LoadingWidget()
: CustomScrollView(
controller: _scrollController,
slivers: <Widget>[
_buildRecommend(),
_buildPersistentHeader(),
_buildSliverBody(),
],
);
},
),
),
);
}
這種方式避免了前一種方式必須設(shè)置expandedHeight的高度的問題,但是 CustonScrollView包裹的子Widget必須是slivers類型,否則會(huì)報(bào)錯(cuò),故_buildRecommend()需要SliverToBoxAdapter包裝下,TabBarView需要通過SliverFillRemaining包裝后作為一個(gè)單獨(dú)的展示視圖。
同時(shí)上述代碼對(duì)推薦區(qū)的數(shù)據(jù)方式采用了provider獲取,這可以避免使用setState刷新局部數(shù)據(jù)從而導(dǎo)致整個(gè)頁面刷新造成UI卡頓的問題。
上述具體的源碼請(qǐng)參見food_set_page2.dart
-
頂部AppBar的實(shí)現(xiàn)
上面兩種方式是實(shí)現(xiàn)了AppBar下方可滾動(dòng)視圖,接下來簡單的介紹下頂部AppBar的實(shí)現(xiàn),主要就是自定義搜索框widget和背景圖片,其他的都很簡單。具體參見如下代碼:
Widget _buildAppBar() {
return PreferredSize(
child: AppBar(
// elevation: 0,
brightness: Brightness.light,
backgroundColor: Colors.transparent,
flexibleSpace: Image.asset('assets/images/bar.png', fit: BoxFit.cover),
leading: IconButton(
icon: Icon(Icons.add, color: Colors.black87),
onPressed: () => debugPrint("點(diǎn)擊+按鈕.."),
),
centerTitle: true,
titleSpacing: 8.0,
title: Container(
padding: EdgeInsets.all(6.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.0),
border: Border.all(width: 0.5, color: Colors.grey),
color: Colors.grey[100]),
child: InkWell(
onTap: () {
// showSearch(context: context, delegate: SearchPage());
getHotWords(Config.SEARCH_HOT_WORDS_URL).then((val) {
Routes.navigateTo(context, '/search',
params: {'data': json.encode(val)});
});
},
child: Row(
children: <Widget>[
Icon(
EvilIcons.search,
color: Colors.grey[700],
size: 20,
),
SizedBox(width: 8.0),
Text(
"搜索百萬免費(fèi)菜譜",
style: TextStyle(
color: Colors.black54,
fontSize: 14,
),
),
],
),
),
),
actions: <Widget>[
IconButton(
icon: Icon(Icons.mail_outline, color: Colors.black87),
onPressed: () => Fluttertoast.showToast(msg: '點(diǎn)擊Email按鈕'),
),
],
),
preferredSize: Size.fromHeight(50),
);
}
背景圖在flexibleSpace: Image.asset('assets/images/bar.png', fit: BoxFit.cover)這里設(shè)置,是不是時(shí)曾相識(shí),對(duì),又是flexibleSpace,這個(gè)東西是個(gè)好東西,大家可以好好了解下。PreferredSize包裝AppBar后就可以自由調(diào)整AppBar的高度了(小技巧)。
注意事項(xiàng):
- 在上述兩種實(shí)現(xiàn)方式的代碼中數(shù)據(jù)未做數(shù)據(jù)類單獨(dú)處理,主要是因?yàn)楹蠖朔祷氐臄?shù)據(jù)類型雜亂無章,如果每個(gè)功能區(qū)的數(shù)據(jù)都封裝成一個(gè)數(shù)據(jù)類則適配起來比不做處理更復(fù)雜。
- 這里要提醒注意的是下方的瀑布流區(qū),如果單獨(dú)的一個(gè)瀑布流可以直接使用
ListView作為其一個(gè)itemView,然后將瀑布流封裝成一個(gè)Widget再將其滾動(dòng)屬性禁掉設(shè)置成physics: NeverScrollableScrollPhysics()即可實(shí)現(xiàn),但是如果像上述由TabBar+TabBarView+StaggeredGridView構(gòu)成的復(fù)雜視圖則ListView無法滿足需要。之前還有個(gè)同事跟我爭論說直接使用ListView就可以實(shí)現(xiàn)該效果,結(jié)果是他沒能通過使用ListView實(shí)現(xiàn)想要的效果,我只想說句在指正別人的時(shí)候先自己去實(shí)現(xiàn)下,否則就是在瞎扯淡! - 上述兩種方式實(shí)現(xiàn)滾動(dòng)吸頂效果后有個(gè)
Bug,就是下方的TabBar在吸頂后瀑布流區(qū)域可以實(shí)現(xiàn)滾動(dòng),但是當(dāng)瀑布流滾動(dòng)到頂部時(shí)scrollController卻無法自動(dòng)切換到最外層的滾動(dòng)視圖上去,必須觸及TabBar區(qū)域才能切換到最外層的滾動(dòng)視圖,暫時(shí)還沒想到有什么好的方式解決這個(gè)問題,但目前來看不影響使用。
具體的效果可以安裝apk試試: APK地址
下篇要實(shí)現(xiàn)的是分別通過自定義和使用自帶的搜索控件實(shí)現(xiàn)搜索界面。