天天看點

Flutter實戰 | 從 0 搭建「網易雲音樂」APP(三、每日推薦、推薦歌單)

本系列可能會伴随大家很長時間,這裡我會從0開始搭建一個「網易雲音樂」的APP出來。

下面是該APP 功能的思維導圖:

Flutter實戰 | 從 0 搭建「網易雲音樂」APP(三、每日推薦、推薦歌單)

前期回顧:

•Flutter實戰 | 從 0 搭建「網易雲音樂」APP(一、建立項目、添加插件、通用代碼)•Flutter實戰 | 從 0 搭建「網易雲音樂」APP(二、Splash Page、登入頁、發現頁)

每日推薦 推薦歌單

本篇為第三篇,在這裡我們會搭建每日推薦、推薦歌單。

UI 分析

首先還是再來看一下「每日推薦」的UI效果:

Flutter實戰 | 從 0 搭建「網易雲音樂」APP(三、每日推薦、推薦歌單)

看到這個效果,有經驗的同學可能直接就會喊出:

CustomScrollView

!!

沒錯,目前頁一共分為三部分:

Flutter實戰 | 從 0 搭建「網易雲音樂」APP(三、每日推薦、推薦歌單)

1.SliverAppBar2.SliverAppBar 的 bottom3.SliverList

整個頁面就是用

CustomScrollView

來做的,但是有一點不同:

平時我們在使用

SliverAppBar

做這種折疊效果的時候,折疊起來是會變成主題色的,

是以這裡我找了别人寫好的一個元件:

FlexibleDetailBar

,用它以後的效果就是上面圖檔那樣。

滑上去的時候「播放全部」那一行還停留在上方,是使用了 SliverAppBar 的 bottom參數。

這樣一個頁面的UI其實就分析完了。

然而!我們回過頭看一下兩個頁面的UI,是不是感覺非常相似!我們來捋一下。

Flutter實戰 | 從 0 搭建「網易雲音樂」APP(三、每日推薦、推薦歌單)

1.标題,不用多說,是一樣的2.SliverAppBar 展開狀态時的内容,是不是可以由外部傳入3.播放全部,也是一樣的,後面有個「共多少首」,也可以由調用者傳入4.最下面的歌單,是不是也可以封裝出一個元件來5.忘記标了,還有一個是SliverAppBar展開時的模糊背景,也可以由調用者傳入

so,我們從上往下來封裝。

先封裝SliverAppBar 的 bottom

确定一下需求,看看需要傳入哪些參數:

1. count:共多少首歌

2. tail:尾部控件

3. onTap:點選播放全部時的回調

bottom 需要的是一個

PreferredSizeWidget

,是以我們的代碼是這樣:

class MusicListHeader extends StatelessWidget implements PreferredSizeWidget {
  MusicListHeader({this.count, this.tail, this.onTap});
  final int count;
  final Widget tail;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    return ClipRRect(
      borderRadius: BorderRadius.vertical(
          top: Radius.circular(ScreenUtil().setWidth(30))),
      child: Container(
        color: Colors.white,
        child: InkWell(
          onTap: onTap,
          child: SizedBox.fromSize(
            size: preferredSize,
            child: Row(
              children: <Widget>[
                HEmptyView(20),
                Icon(
                  Icons.play_circle_outline,
                  size: ScreenUtil().setWidth(50),
                ),
                HEmptyView(10),
                Padding(
                  padding: const EdgeInsets.only(top: 3.0),
                  child: Text(
                    "播放全部",
                    style: mCommonTextStyle,
                  ),
                ),
                HEmptyView(5),
                Padding(
                  padding: const EdgeInsets.only(top: 3.0),
                  child: count == null
                      ? Container()
                      : Text(
                    "(共$count首)",
                    style: smallGrayTextStyle,
                  ),
                ),
                Spacer(),
                tail ?? Container(),
              ],
            ),
          ),
        ),
      ),
    );
  }

  @override
  Size get preferredSize => Size.fromHeight(ScreenUtil().setWidth(100));
}           

複制

然後封裝 SliverAppBar

還是先确定一下需求,看看需要傳入什麼:

1.要傳入一個背景還模糊2.傳入title3.傳入展開時的高度4.播放次數5.播放全部的點選回調

确定好就之後,代碼如下:

class PlayListAppBarWidget extends StatelessWidget {
  final double expandedHeight;
  final Widget content;
  final String backgroundImg;
  final String title;
  final double sigma;
  final VoidCallback playOnTap;
  final int count;

  PlayListAppBarWidget({
    @required this.expandedHeight,
    @required this.content,
    @required this.title,
    @required this.backgroundImg,
    this.sigma = 5,
    this.playOnTap,
    this.count,
  });

  @override
  Widget build(BuildContext context) {
    return SliverAppBar(
      centerTitle: true,
      expandedHeight: expandedHeight,
      pinned: true,
      elevation: 0,
      brightness: Brightness.dark,
      iconTheme: IconThemeData(color: Colors.white),
      title: Text(
        title,
        style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
      ),
      bottom: MusicListHeader(
        onTap: playOnTap,
        count: count,
      ),
      flexibleSpace: FlexibleDetailBar(
        content: content,
        background: Stack(
          children: <Widget>[
            backgroundImg.startsWith('http')
                ? Image.network(
                    backgroundImg,
                    width: double.infinity,
                    height: double.infinity,
                    fit: BoxFit.cover,
                  )
                : Image.asset(backgroundImg),
            BackdropFilter(
              filter: ImageFilter.blur(
                sigmaY: sigma,
                sigmaX: sigma,
              ),
              child: Container(
                color: Colors.black38,
                width: double.infinity,
                height: double.infinity,
              ),
            ),
          ],
        ),
      ),
    );
  }
}           

複制

這裡有兩個地方需要注意一下:

1.外部傳入背景圖檔時,有可能是本地檔案,也有可能是網絡圖檔,是以我們直接在這裡判斷

startsWith('http')

2.模糊背景圖檔時,加一個

Colors.black38

,這樣省的後續有白色圖檔所導緻文字看不清。

最後封裝歌曲清單的item

這個item就比較簡單了,傳入一個實體類,根據參數來填值就好了,大緻代碼如下:

class WidgetMusicListItem extends StatelessWidget {
  final MusicData _data;

  WidgetMusicListItem(this._data);

  @override
  Widget build(BuildContext context) {
    return Container(
      width: Application.screenWidth,
      height: ScreenUtil().setWidth(120),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.center,
        children: <Widget>[
          // xxx
        ],
      ),
    );
  }
}
           

複制

總結

經過前兩次基礎頁面的搭建,我們後續再來寫頁面的時候可以說是簡單了百倍不止。

而且根本不用管網絡請求之類的邏輯,隻需管好我們的頁面就好了。

而在寫UI時,也一定要多看,多想,這個能不能封裝出來?那個能不能提取?

這樣以後再開發的話,真的是非常簡單。

該系列文章代碼已傳至 GitHub:https://github.com/wanglu1209/NeteaseClouldMusic