[02] ebook — Music Player

Wei
海大 SwiftUI iOS / Flutter App 程式設計
13 min readApr 10, 2024

(一)App操作影片

(二)GitHub連結

(三)App各分頁畫面及程式碼講解

歌手分類頁面

點擊歌手圖片會彈出該歌手的音樂

程式碼講解

1. 使用GridView建構畫面

 GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, // 每行兩列
childAspectRatio: 0.8, // 子項目的寬高比
),
itemCount: artists.length, // 歌手數量
itemBuilder: (context, index) {}
)

2. 使用onTap搭配Navigator實現,頁面導航

onTap: () {
// 根據選中的歌手過濾音樂數據
List<Music> filteredByArtist = musicData
.where((music) => music.artist == artists[index])
.toList();
String title = artists[index]; // 歌手名稱

// 導航到音樂列表頁面,傳遞過濾後的音樂列表
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => MusicListPage(
musics: filteredByArtist,
title: title,
),
),
);
},

專輯分類頁面

點擊專輯圖片會彈出該歌手的音樂

所有歌曲頁面

可以上下捲動瀏覽所有歌曲,點擊播放/暫停鍵會播放/暫停音樂

程式碼講解

1. 使用ListView呈現可捲動畫面,透過向子Widget傳入isPlaying變數來控制應呈現播放/暫停狀態

ListView.builder(
itemCount: widget.musics.length,
itemBuilder: (context, index) {
// 判斷當前項目是否正在播放
bool isPlaying = musicService.currentMusic == widget.musics[index] &&
musicService.isPlaying;

// 傳回每個 item
return MusicItemWidget(
music: widget.musics[index],
index: index,
isPlaying: isPlaying,
onPlay: updatePlaylist,
);
},
);

2. 使用ListTile作為ListViewItem

ListTile(
leading: Text(
'${index + 1}',
style: TextStyle(color: isPlaying ? selectedColor : indexColor),
),
title: Text(
music.name,
style: TextStyle(color: isPlaying ? selectedColor : titleColor),
),
subtitle: Text(
'${music.artist} - ${music.album}',
style:
TextStyle(color: isPlaying ? selectedColor : subtitleColor),
),
trailing: IconButton(
color: isPlaying ? selectedColor : iconColor,
// 依照播放狀態調整圖標,撥放/暫停
icon: Icon(isPlaying ? Icons.pause : Icons.play_arrow),
onPressed: () async {
if (isPlaying) {
// 如果正在播放,則暫停

globalPlayer.pauseMusic();
} else {
// 否則,播放目前音樂

onPlay(index); // 更新撥放清單
globalPlayer.playMusicAtIndex(index);
}
},
),
),

底部操作面板

可以開始/暫停播放歌曲,或是下一首、上一首

點擊可以展開,會新增呈現進度條,可以透過拉動進度條調整進度

程式碼講解

1. 使用setState控制StatefulWidgetisExpanded變數來控制展開

GestureDetector(
onTap: () {
// 點擊時切換展開狀態
setState(() {
isExpanded = !isExpanded;
});
},
// 根據控制面板的展開狀態,選擇展示完整或簡化的面板
child: isExpanded
? buildExpandPanel(musicService)
: buildUnExpandPanel(musicService),
);

2. 傳入musicServiceWidget可以取得、控制音樂撥放器資訊

Widget buildExpandPanel(musicService) {
// 定義文字顏色和滑桿啟動顏色
const titleColor = Color.fromARGB(255, 255, 237, 237);
const subtitleColor = Color.fromARGB(255, 151, 150, 150);
const activeColor = Color.fromARGB(255, 32, 225, 122);
const iconColor = Colors.white;

// 取得全域播放狀態和當前音樂訊息
final isPlaying = musicService.isPlaying;
final currentMusic = musicService.currentMusic;
final duration = musicService.duration.inSeconds.toDouble();
final currentPosition = musicService.currentPosition.inSeconds.toDouble();

(四)其他重要部份及程式碼講解

使用ChangeNotifier來控制全局狀態,使不同頁面可以共享資訊,並操作同一個音樂撥放器

程式碼講解

1. MusicService 繼承 ChangeNotifier,只建立一個全局的AudioPlayerMusicService 可以通過調用 notifyListeners 方法通知其所有監聽者,讓它們重新構建以反映最新的狀態

/// 使用 Provider 和 ChangeNotifier 來建立一個全域音樂播放器模型
/// 管理播放清單、目前播放音樂、播放狀態等
/// 通過將 MusicService 包裝在 ChangeNotifier 中,任何需要該數據的 Widget 都可以訪問當前的 MusicService 實例
class MusicService extends ChangeNotifier {
// 音樂播放器實例,使用 audioplayers: ^6.0.0
final AudioPlayer _audioPlayer = AudioPlayer();

// 動態傳入撥放列表
List<Music>? _playlist;
int _currentIndex = -1; // 目前播放音樂的索引
Music? _currentMusic; // 目前播放的音樂
bool _isPlaying = false; // 目前是否正在播放音樂

//音樂持續時間
Duration _duration = Duration.zero;
//目前播放進度
Duration _currentPosition = Duration.zero;

Duration get duration => _duration;
Duration get currentPosition => _currentPosition;
Music? get currentMusic => _currentMusic;
bool get isPlaying => _isPlaying;

// 使用 AudioPlayer 播放指定的音樂
Future<void> _play(Music music) async {
await _audioPlayer.play(AssetSource(music.path));
}

// 使用 AudioPlayer 暫停播放
Future<void> _pause() async {
await _audioPlayer.pause();
}

// 使用 AudioPlayer 跳到指定播放進度
Future<void> _seek(Duration duration) async {
await _audioPlayer.seek(duration);
}

// 設定播放列表
setPlaylist(List<Music> newPlaylist) {
_playlist = newPlaylist;
}

// 播放/繼續放目前選擇的音樂
void playMusic() {
if (_currentMusic == null) {
return;
}
_isPlaying = true;
notifyListeners(); // 通知監聽者狀態變更:_isPlaying 的狀態改變
_play(_currentMusic!);
}

// 根據索引播放音樂
void playMusicAtIndex(int index) {
if (_playlist == null ||
_playlist!.isEmpty ||
index < 0 ||
index >= _playlist!.length) {
return;
}
_currentIndex = index;
_currentMusic = _playlist![_currentIndex];
_isPlaying = true;
notifyListeners(); // 通知監聽者狀態變更:_isPlaying、_currentMusic、_currentIndex 的狀態改變
_play(_currentMusic!);
}

// 播放下一首音樂
void playNext() {
if (_playlist != null && _currentIndex < _playlist!.length - 1) {
playMusicAtIndex(_currentIndex + 1);
}
}

// 播放上一首音樂
void playPrevious() {
if (_currentIndex > 0) {
playMusicAtIndex(_currentIndex - 1);
}
}

// 暫停音樂
void pauseMusic() {
_isPlaying = false;
notifyListeners();
_pause();
}

// 設定目前播放進度
void setCurrentPosition(Duration position) {
_currentPosition = position;
notifyListeners();
}

// 跳到指定播放進度
void seek(Duration position) {
_seek(position);
}

// 監聽音樂播放器的狀態變化
MusicService() {
// 取得音樂總時長
_audioPlayer.onDurationChanged.listen((Duration newDuration) {
_duration = newDuration;
notifyListeners();
});

// 音樂播放完成時自動播放下一首
_audioPlayer.onPlayerComplete.listen((event) {
playNext();
});

// 更新播放進度
_audioPlayer.onPositionChanged.listen((position) {
setCurrentPosition(position);
});
}
}

2. 在MusicItemWidget中使用MusicService

// 透過 Provider.of 方法從當前上下文中取得 MusicService 的實例
final musicService = Provider.of<MusicService>(context, listen: false);
onPressed: () async {
if (isPlaying) {
// 如果正在播放,則調用 MusicService 實例的 pauseMusic 方法來暫停音樂
musicService.pauseMusic();
} else {
// 否則,調用 playMusicAtIndex 方法來播放選中的音樂。
// onPlay(index) 是一個回調函數,負責根據點擊的項目索引更新播放列表或狀態
onPlay(index);
musicService.playMusicAtIndex(index);
}
},

使用AudioPlayer 的監聽器onPlayerComplete 在音樂結束時自動撥放下一首

程式碼講解

a. 註冊當音樂結束時的監聽器

final AudioPlayer _audioPlayer = AudioPlayer();

// 音樂播放完成時自動播放下一首
_audioPlayer.onPlayerComplete.listen((event) {
playNext();
});

// 播放下一首音樂
void playNext() {
if (_playlist != null && _currentIndex < _playlist!.length - 1) {
playMusicAtIndex(_currentIndex + 1);
}
}

--

--