好久没更新了,主要是最近比较忙,后面我会尽量把这个系列尽快完成吧!写的不好的地方还请各位看官包涵,指出斧正!谢谢!
今天要实现的是食集界面,下面先分析下这个界面的构成。这个页面的构成比较复杂,包含多层视图嵌套,如果通过原生Android实现的话估计需要花点时间,但是Flutter就相对来说比较快速了。布局如下图所示:
从上图可以看出:
- 顶部是一个
AppBar
,这个AppBar
的背景是张自定义图片,同时含有三个功能区,分别是"+"
、搜索框
和email
。点击"+"
按钮弹出ModalBottomSheet
(后期实现),点击搜索区跳转搜索界面(已实现),email
是一个actions
按钮。 -
AppBar
下方是一个Container
,暂称"推荐区"
,其中包含四个功能区,分别是Banner推荐展示区
、Channel渠道展示区
、一日三餐展示区
和广告位展示区
。其中Banner
是一个ListView
,Channel
是一个Wrap
,三餐区
是由TabBar
+PageView
构成的,广告位
是一个Swiper
。 - 在
推荐区
下方是一个菜谱推荐区
,主要是由TabBar
+TabBarView
+StaggeredGridView
构成。
我们需要实现的效果时最外层是一个可滚动的视图widget
,然后菜谱推荐区
的TabBar
在滚动到顶部的时候需要有吸顶效果,其次菜谱推荐区的瀑布流也需要可以滚动,上面的其他可滚动Widget
也需要联动滚动而不发生冲突。具体效果可以
这里提供两种可滚动的视图Widget
来实现上述需要的效果,一种使用NestedScrollView
,另一种是使用CustomScrollView
,这两种可滚动Widget
均作为最外层的Widget
。
-
使用NestedScrollView实现
使用NestedScrollView
作为外层Widget
时可以将上述的推荐区
整个视图包括菜谱推荐区的TabBar
一道封装到其headerSliverBuilder
中作为其头部视图,然后下方的菜谱推荐区的TabBarView
单独作为其body
部分。大概的结构如下:
@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), //展开高度,必选项
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
是一个大小跟SliverAppBar
相同但位于其下方且是一个可伸缩的区域,这里可以自由添加需要的其他Widget
,就像在Container
中添加widget
一样,flexibleSpace
具体的用法可自行百度或谷歌搜索学习。
这种方法有个弊端,就是expandedHeight
的高度必填,否则显示会有问题,这就要求出原型图的时候把推荐区
的高度必须给确定写死,否则无法适配达到预期效果。
上述具体的源码请参见food_set_page.dart
-
使用CustomScrollView实现
使用CustonScrollView
实现上述效果时需要将推荐区
单独封装成一个视图,然后下方的TabBar
使用SliverPersistentHeader
封装,这样其就可以滚动到顶部时实现吸顶效果了,参考代码如下:
@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(),
],
);
},
),
),
);
}
这种方式避免了前一种方式必须设置expandedHeight
的高度的问题,但是 CustonScrollView
包裹的子Widget
必须是slivers
类型,否则会报错,故_buildRecommend()
需要SliverToBoxAdapter
包装下,TabBarView
需要通过SliverFillRemaining
包装后作为一个单独的展示视图。
同时上述代码对推荐区
的数据方式采用了provider
获取,这可以避免使用setState
刷新局部数据从而导致整个页面刷新造成UI
卡顿的问题。
上述具体的源码请参见food_set_page2.dart
-
顶部AppBar的实现
上面两种方式是实现了AppBar
下方可滚动视图,接下来简单的介绍下顶部AppBar
的实现,主要就是自定义搜索框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("点击+按钮.."),
),
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(
"搜索百万免费菜谱",
style: TextStyle(
color: Colors.black54,
fontSize: 14,
),
),
],
),
),
),
actions: <Widget>[
IconButton(
icon: Icon(Icons.mail_outline, color: Colors.black87),
onPressed: () => Fluttertoast.showToast(msg: '点击Email按钮'),
),
],
),
preferredSize: Size.fromHeight(50),
);
}
背景图在flexibleSpace: Image.asset('assets/images/bar.png', fit: BoxFit.cover)
这里设置,是不是时曾相识,对,又是flexibleSpace
,这个东西是个好东西,大家可以好好了解下。PreferredSize
包装AppBar
后就可以自由调整AppBar
的高度了(小技巧)。
注意事项:
- 在上述两种实现方式的代码中数据未做数据类单独处理,主要是因为后端返回的数据类型杂乱无章,如果每个功能区的数据都封装成一个数据类则适配起来比不做处理更复杂。
- 这里要提醒注意的是下方的瀑布流区,如果单独的一个瀑布流可以直接使用
ListView
作为其一个itemView
,然后将瀑布流封装成一个Widget
再将其滚动属性禁掉设置成physics: NeverScrollableScrollPhysics()
即可实现,但是如果像上述由TabBar
+TabBarView
+StaggeredGridView
构成的复杂视图则ListView
无法满足需要。之前还有个同事跟我争论说直接使用ListView
就可以实现该效果,结果是他没能通过使用ListView
实现想要的效果,我只想说句在指正别人的时候先自己去实现下,否则就是在瞎扯淡! - 上述两种方式实现滚动吸顶效果后有个
Bug
,就是下方的TabBar
在吸顶后瀑布流区域可以实现滚动,但是当瀑布流滚动到顶部时scrollController
却无法自动切换到最外层的滚动视图上去,必须触及TabBar
区域才能切换到最外层的滚动视图,暂时还没想到有什么好的方式解决这个问题,但目前来看不影响使用。
具体的效果可以安装apk试试: APK地址
下篇要实现的是分别通过自定义和使用自带的搜索控件实现搜索界面。
网友评论