本系列可能會伴隨大家很長時間,這裡我會從0開始搭建一個「網易雲音樂」的APP出來。
下面是該APP 功能的思維導圖:
前期回顧:
每日推薦 | 推薦歌單 |
---|---|
本篇為第三篇,在這裡我們會搭建每日推薦、推薦歌單。
UI 分析
首先還是再來看一下「每日推薦」的UI效果:
看到這個效果,有經驗的同學可能直接就會喊出:CustomScrollView
!!
沒錯,當前頁一共分為三部分:
- SliverAppBar
- SliverAppBar 的 bottom
- SliverList
整個頁面就是用 CustomScrollView
來做的,但是有一點不同:
平時我們在使用 SliverAppBar
做這種摺疊效果的時候,摺疊起來是會變成主題色的,
所以這裡我找了別人寫好的一個元件:FlexibleDetailBar
,用它以後的效果就是上面圖片那樣。
滑上去的時候「播放全部」那一行還停留在上方,是使用了 SliverAppBar 的 bottom引數。
這樣一個頁面的UI其實就分析完了。
然而!我們回過頭看一下兩個頁面的UI,是不是感覺非常相似!我們來捋一下。
- 標題,不用多說,是一樣的
- SliverAppBar 展開狀態時的內容,是不是可以由外部傳入
- 播放全部,也是一樣的,後面有個「共多少首」,也可以由呼叫者傳入
- 最下面的歌單,是不是也可以封裝出一個元件來
- 忘記標了,還有一個是SliverAppBar展開時的模糊背景,也可以由呼叫者傳入
so,我們從上往下來封裝。
先封裝SliverAppBar 的 bottom
確定一下需求,看看需要傳入哪些引數:
- count:共多少首歌
- tail:尾部控制元件
- 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
還是先確定一下需求,看看需要傳入什麼:
- 要傳入一個背景還模糊
- 傳入title
- 傳入展開時的高度
- 播放次數
- 播放全部的點選回撥
確定好就之後,程式碼如下:
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,
),
),
],
),
),
);
}
}
複製程式碼
這裡有兩個地方需要注意一下:
- 外部傳入背景圖片時,有可能是本地檔案,也有可能是網路圖片,所以我們直接在這裡判斷
startsWith('http')
- 模糊背景圖片時,加一個
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」來入群。