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

Flutter筆記 發表於 2019-10-16

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

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

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

前期回顧:

每日推薦 推薦歌單
每日推薦
推薦歌單

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

UI 分析

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

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

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

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

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

  1. SliverAppBar
  2. SliverAppBar 的 bottom
  3. 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. 傳入title
  3. 傳入展開時的高度
  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:github.com/wanglu1209/…

另我個人建立了一個「Flutter 交流群」,可以新增我個人微信 「17610912320」來入群。

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