Flutter實(shí)戰(zhàn)--美食App(四)

鎮(zhèn)樓

好久沒更新了,主要是最近比較忙,后面我會(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è)ListViewChannel是一個(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):

  1. 在上述兩種實(shí)現(xiàn)方式的代碼中數(shù)據(jù)未做數(shù)據(jù)類單獨(dú)處理,主要是因?yàn)楹蠖朔祷氐臄?shù)據(jù)類型雜亂無章,如果每個(gè)功能區(qū)的數(shù)據(jù)都封裝成一個(gè)數(shù)據(jù)類則適配起來比不做處理更復(fù)雜。
  2. 這里要提醒注意的是下方的瀑布流區(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)下,否則就是在瞎扯淡!
  3. 上述兩種方式實(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)搜索界面。

?完整Demo請(qǐng)移步 iCooker 喜歡的請(qǐng)給個(gè)Star ☆??!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容