[02] ebook — Music Player
(一)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
作為ListView
的Item
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
控制StatefulWidget
的isExpanded
變數來控制展開
GestureDetector(
onTap: () {
// 點擊時切換展開狀態
setState(() {
isExpanded = !isExpanded;
});
},
// 根據控制面板的展開狀態,選擇展示完整或簡化的面板
child: isExpanded
? buildExpandPanel(musicService)
: buildUnExpandPanel(musicService),
);
2. 傳入musicService
讓Widget
可以取得、控制音樂撥放器資訊
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
,只建立一個全局的AudioPlayer
,MusicService
可以通過調用 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);
}
}