本系列可能會伴隨你們很長時間,這裏我會從0開始搭建一個「網易雲音樂」的APP出來。git
下面是該APP 功能的思惟導圖:github
前期回顧:微信
本篇爲第五篇,在這裏咱們會搭建播放歌曲和播放歌曲一系列的邏輯。markdown
咱們整個APP的重中之重就是「播放歌曲」。(廢話,一個音樂播放軟件不放歌幹嗎?)網絡
在上一篇文章當中已經對UI有了一個大致的認識,接下來就是要對「播放歌曲」全部的功能有一個瞭解:ide
對於播放來講,我選擇的是 luanpotter - audioplayers,這個庫提供了咱們對播放音樂須要的全部功能。ui
有了它之後咱們就能夠根據需求來封裝一個控制歌曲播放的類 PlaySongsModel
。this
老套路,看文檔。lua
一共有兩種初始化的方法:url
第一種是適合咱們的方法,只能同時播放一個音頻。
第二種文檔上說是適合遊戲聲音,這種模式下不會觸發總時間和當前時間的更新,也不能把音頻跳轉到特定的位置,有點像廣播的意思。
光初始化還不夠,咱們還要在 Android 上設置參數:
Android 若是想要長時間播放音頻,則須要添加權限:WAKE_LOCK
另外兩個平臺想要播放非 https的音頻,也要設置一些參數,具體就請百度。
咱們的播放器全局確定就只須要一個,並且須要共享。
這個時候咱們就會想到了 Provider
。
並且只須要在首頁初始化就行了,因此初始化代碼以下:
void init() {
_audioPlayer.setReleaseMode(ReleaseMode.STOP);
// 播放狀態監聽
_audioPlayer.onPlayerStateChanged.listen((state) {
_curState = state;
/// 先作順序播放
if(state == AudioPlayerState.COMPLETED){
nextPlay();
}
// 其實也只有在播放狀態更新時才須要通知。
notifyListeners();
});
_audioPlayer.onDurationChanged.listen((d) {
curSongDuration = d;
});
// 當前播放進度監聽
_audioPlayer.onAudioPositionChanged.listen((Duration p) {
sinkProgress(p.inMilliseconds > curSongDuration.inMilliseconds ? curSongDuration.inMilliseconds : p.inMilliseconds);
});
}
複製代碼
這裏也添加了一些監聽:
在 main.dart
中:
runApp(MultiProvider(
providers: [
ChangeNotifierProvider<UserModel>.value(
value: UserModel(),
),
ChangeNotifierProvider<PlaySongsModel>(
builder: (_) => PlaySongsModel()..init(),
),
],
child: MyApp(),
));
}
複製代碼
這樣就完成了咱們播放器的初始化工做。
當咱們點擊某一個音樂、或者「播放所有」的時候,這個時候該怎麼操做?
首先排除一點:播放歌曲等操做絕對是不可能在播放歌曲頁面操做的。
由於後續要在每個頁面中都會有一個播放的控制器。
並且「每日推薦」和「歌單」列表返回的數據有些不一樣。
那咱們就自定義一個類,來接收你所想要播放的音樂:
class Song{
int id; // 歌曲id
String name; // 歌曲名稱
String artists; // 演唱者
String picUrl; // 歌曲圖片
Song(this.id, {this.name, this.artists, this.picUrl});
@override
String toString() {
return 'Song{id: $id, name: $name, artists: $artists}';
}
}
複製代碼
而後在 Provider 的 model 當中定義方法:
List<Song> _songs = [];
// 播放一首歌
void playSong(Song song) {
_songs.insert(curIndex, song);
play();
}
// 播放不少歌
void playSongs(List<Song> songs, {int index}) {
this._songs = songs;
if (index != null) curIndex = index;
play();
}
// 添加歌曲
void addSongs(List<Song> songs) {
this._songs.addAll(songs);
}
// 播放
void play() {
_audioPlayer.play(
"https://music.163.com/song/media/outer/url?id=${this._songs[curIndex].id}.mp3");
}
複製代碼
最後當咱們點擊某一個歌曲或者「播放所有」的時候,只須要調用 model 當中的方法,把整首歌單、或者單一的曲子加入到當前播放的隊列當中就行了。
當咱們開始播放的時候須要跳轉到「播放頁面」,而這個時候咱們的播放頁面的邏輯就很是簡單了。
只須要在頁面套上 Consumer
獲取到當前所播放的歌曲就行了。
根本不須要咱們傳入任何東西:
// 大體代碼
var curSong = model.curSong;
if (model.curState == AudioPlayerState.PLAYING) {
// 若是當前狀態是在播放當中,則唱片一直旋轉,
// 而且唱針是移除狀態
_controller.forward();
_stylusController.reverse();
} else {
_controller.stop();
_stylusController.forward();
}
Utils.showNetImage(
curSong.picUrl, // 當前圖片
),
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
model.curSong.name, // 當前歌曲名字
style: commonWhiteTextStyle,
),
Text(
model.curSong.artists, // 當前藝術家
style: smallWhite70TextStyle,
),
],
),
複製代碼
這樣就直接能把咱們的頁面畫出來了。
在咱們進入播放頁面的時候,判斷一下當前歌曲的狀態,就能來控制唱針和碟片的狀態:
var curSong = model.curSong;
if (model.curState == AudioPlayerState.PLAYING) {
// 若是當前狀態是在播放當中,則唱片一直旋轉,
// 而且唱針是移除狀態
_controller.forward();
_stylusController.reverse();
} else {
_controller.stop();
_stylusController.forward();
}
複製代碼
若是當前狀態是在播放當中,則唱片一直旋轉,而且唱針是移除狀態。
在初始化的時候咱們監聽了當前歌曲的狀態,每當咱們作出操做的時候,都會對應着一個狀態、而且進行 notifyListeners
:
/// 暫停、恢復
void togglePlay(){
if (_audioPlayer.state == AudioPlayerState.PAUSED) {
resumePlay();
} else {
pausePlay();
}
}
// 暫停
void pausePlay() {
_audioPlayer.pause();
}
/// 跳轉到固定時間
void seekPlay(int milliseconds){
_audioPlayer.seek(Duration(milliseconds: milliseconds));
resumePlay();
}
/// 恢復播放
void resumePlay() {
_audioPlayer.resume();
}
/// 下一首
void nextPlay(){
if(curIndex >= _songs.length){
curIndex = 0;
}else{
curIndex++;
}
play();
}
void prePlay(){
if(curIndex <= 0){
curIndex = _songs.length - 1;
}else{
curIndex--;
}
play();
}
複製代碼
因此在咱們作出一系列的操做後,都會通知到頁面上,而後根據相對應的狀態進行處理就行了。
當咱們的歌曲能播放後,接下來的重點就是在播放進度上了。
首先看一下監聽返回的值是什麼:
// 總進度
_audioPlayer.onDurationChanged.listen((d) {
curSongDuration = d;
});
// 當前播放進度監聽
_audioPlayer.onAudioPositionChanged.listen((Duration p) {
// xxx
});
複製代碼
emm..是一個Duration,那咱們可使用 duration.inMilliseconds
來獲取時間戳。
並且這個監聽事件間隔很是短,因此咱們確定是不可能用 setState() 來更新頁面,這樣確定會形成很是嚴重的後果。
那這裏咱們就可使用 StreamBuilder
來進行操做,每有一次當前進度的更新,就用stream 的方式發送出去,而後更新頁面。
// 歌曲進度
void sinkProgress(int m){
_curPositionController.sink.add('$m-${curSongDuration.inMilliseconds}');
}
複製代碼
這裏有個坑,就是 當前播放時間有可能會大於總時長!
因此咱們在調用方法的時候要進行一次判斷:
sinkProgress(p.inMilliseconds > curSongDuration.inMilliseconds ? curSongDuration.inMilliseconds : p.inMilliseconds);
複製代碼
若是當前播放時間大於了總時間,那麼就傳入總時間。
而後在播放頁面:
Widget build(BuildContext context) {
return StreamBuilder<String>(
stream: model.curPositionStream,
builder: (context, snapshot) {
if (snapshot.hasData) {
var totalTime =
snapshot.data.substring(snapshot.data.indexOf('-') + 1);
var curTime = double.parse(snapshot.data.substring(0, snapshot.data.indexOf('-')));
var curTimeStr = DateUtil.formatDateMs(curTime.toInt(), format: "mm:ss");
return buildProgress(curTime, totalTime, curTimeStr);
} else {
return buildProgress(0, "0", "00:00");
}
},
);
}
複製代碼
歌曲狀態是播放中,可是頗有多是還沒開始播放(網絡問題),因此咱們要判斷是否有有數據。
數據是咱們本身傳入的,獲取到數據後傳入方法中就ok了。
buildProgress
方法:
return Container(
height: ScreenUtil().setWidth(120),
child: Row(
children: <Widget>[
Text(
curTimeStr,
style: smallWhiteTextStyle,
),
Expanded(
child: SliderTheme(
data: SliderThemeData(
trackHeight: ScreenUtil().setWidth(2),
thumbShape: RoundSliderThumbShape(
enabledThumbRadius: ScreenUtil().setWidth(10),
),
),
child: Slider(
value: curTime,
onChanged: (data) {
model.sinkProgress(data.toInt());
},
onChangeStart: (data) {
model.pausePlay();
},
onChangeEnd: (data) {
model.seekPlay(data.toInt());
},
activeColor: Colors.white,
inactiveColor: Colors.white30,
min: 0,
max: double.parse(totalTime),
),
),
),
Text(
DateUtil.formatDateMs(int.parse(totalTime), format: "mm:ss"),
style: smallWhite30TextStyle,
),
],
),
);
複製代碼
這裏進度條使用的控件是Slider
,設置幾個參數:
這裏最大值咱們直接設置成當前歌曲的Duration,這樣作好處有不少:
並且在 onChangeStart
onChangeEnd
這兩個回調中調用播放器的 暫停 和 播放就行了。
(總感受這個控件就是爲了這個需求創造的,23333)
這樣就完成了咱們的進度條的邏輯。
最後再來看一下效果:
歌詞功能、正在播放功能這週會跟上,你們敬請期待!
該系列文章代碼已傳至 GitHub:github.com/wanglu1209/…
另我我的建立了一個「Flutter 交流羣」,能夠添加我我的微信 「17610912320」來入羣。