美文网首页Flutter
Flutter实战--美食App(四)

Flutter实战--美食App(四)

作者: SamBrother | 来源:发表于2020-08-21 00:46 被阅读0次
镇楼

好久没更新了,主要是最近比较忙,后面我会尽量把这个系列尽快完成吧!写的不好的地方还请各位看官包涵,指出斧正!谢谢!

今天要实现的是食集界面,下面先分析下这个界面的构成。这个页面的构成比较复杂,包含多层视图嵌套,如果通过原生Android实现的话估计需要花点时间,但是Flutter就相对来说比较快速了。布局如下图所示:

食集界面
从上图可以看出:
  • 顶部是一个AppBar,这个AppBar的背景是张自定义图片,同时含有三个功能区,分别是"+"搜索框email。点击"+"按钮弹出ModalBottomSheet(后期实现),点击搜索区跳转搜索界面(已实现),email是一个actions按钮。
  • AppBar下方是一个Container,暂称"推荐区",其中包含四个功能区,分别是 Banner推荐展示区Channel渠道展示区一日三餐展示区广告位展示区。其中Banner是一个ListViewChannel是一个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的高度了(小技巧)。


注意事项:

  1. 在上述两种实现方式的代码中数据未做数据类单独处理,主要是因为后端返回的数据类型杂乱无章,如果每个功能区的数据都封装成一个数据类则适配起来比不做处理更复杂。
  2. 这里要提醒注意的是下方的瀑布流区,如果单独的一个瀑布流可以直接使用ListView作为其一个itemView,然后将瀑布流封装成一个Widget再将其滚动属性禁掉设置成physics: NeverScrollableScrollPhysics()即可实现,但是如果像上述由TabBar+TabBarView+StaggeredGridView构成的复杂视图则ListView无法满足需要。之前还有个同事跟我争论说直接使用ListView就可以实现该效果,结果是他没能通过使用ListView实现想要的效果,我只想说句在指正别人的时候先自己去实现下,否则就是在瞎扯淡!
  3. 上述两种方式实现滚动吸顶效果后有个Bug,就是下方的TabBar在吸顶后瀑布流区域可以实现滚动,但是当瀑布流滚动到顶部时scrollController却无法自动切换到最外层的滚动视图上去,必须触及TabBar区域才能切换到最外层的滚动视图,暂时还没想到有什么好的方式解决这个问题,但目前来看不影响使用。
    具体的效果可以安装apk试试: APK地址

下篇要实现的是分别通过自定义和使用自带的搜索控件实现搜索界面。

☞完整Demo请移步 iCooker 喜欢的请给个Star ☆!!

相关文章

网友评论

    本文标题:Flutter实战--美食App(四)

    本文链接:https://www.haomeiwen.com/subject/hztyjktx.html