基于第三方库的Flutter HarmonyOS 音乐播放器
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net一款功能完整的本地音乐播放器应用,基于 Flutter 框架开发,支持 HarmonyOS 平台。
·
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
一款功能完整的本地音乐播放器应用,基于 Flutter 框架开发,支持 HarmonyOS 平台。
创建项目
1. 创建 Flutter 项目
# 创建新项目
flutter create -t app text3
cd text3
2. 添加 HarmonyOS 支持
# 在项目中添加 HarmonyOS 平台
flutter build app --release
—## 项目结构
创建以下目录结构:
text3/
├── lib/
│ ├── main.dart
│ ├── models/
│ │ └── song_model.dart
│ ├── providers/
│ │ ├── audio_provider.dart
│ │ └── playlist_provider.dart
│ ├── screens/
│ │ ├── home_screen.dart
│ │ ├── player_screen.dart
│ │ └── playlist_screen.dart
│ ├── services/
│ │ ├── audio_service.dart
│ │ ├── database_helper.dart
│ │ └── media_scanner_service.dart
│ └── widgets/
│ ├── mini_player_bar.dart
│ ├── seek_slider.dart
│ └── circular_album_art.dart
├── assets/
│ ├── icons/
│ └── animations/
├── ohos/
│ └── entry/
│ └── src/
│ └── main/
│ ├── module.json5
│ └── ets/
└── pubspec.yaml
配置文件
1. pubspec.yaml
name: text3
description: Flutter HarmonyOS 音乐播放器
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: '>=3.5.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
# UI 组件
cupertino_icons: ^1.0.8
provider: ^6.1.2
# 音频播放
audioplayers: ^6.1.0
# 本地音乐查询
on_audio_query: ^2.9.0
# 数据库
sqflite: ^2.4.1
path_provider: ^2.1.4
path: ^1.9.0
# 权限处理
permission_handler: ^11.4.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^4.0.0
flutter:
uses-material-design: true
assets:
- assets/
- assets/icons/
- assets/animations/
2. 安装依赖
flutter pub get
核心代码实现
1. 数据模型 - lib/models/song_model.dart
import 'package:on_audio_query/on_audio_query.dart';
/// 歌曲数据模型
class Song {
final int id;
final String title;
final String artist;
final String album;
final String data;
final int duration;
final int albumId;
bool isFavorite;
Song({
required this.id,
required this.title,
required this.artist,
required this.album,
required this.data,
required this.duration,
required this.albumId,
this.isFavorite = false,
});
/// 从 AudioQuery 结果创建 Song 对象
factory Song.fromAudioQuery(dynamic audio) {
return Song(
id: audio.id ?? 0,
title: audio.displayName ?? '未知标题',
artist: audio.artist ?? '未知艺术家',
album: audio.album ?? '未知专辑',
data: audio.data ?? '',
duration: audio.duration ?? 0,
albumId: audio.albumId ?? 0,
isFavorite: false,
);
}
/// 转换为 Map
Map<String, dynamic> toMap() {
return {
'id': id,
'title': title,
'artist': artist,
'album': album,
'data': data,
'duration': duration,
'albumId': albumId,
'isFavorite': isFavorite ? 1 : 0,
};
}
/// 从 Map 创建 Song
factory Song.fromMap(Map<String, dynamic> map) {
return Song(
id: map['id'] ?? 0,
title: map['title'] ?? '',
artist: map['artist'] ?? '',
album: map['album'] ?? '',
data: map['data'] ?? '',
duration: map['duration'] ?? 0,
albumId: map['albumId'] ?? 0,
isFavorite: (map['isFavorite'] ?? 0) == 1,
);
}
}
2. 音频播放服务 - lib/services/audio_service.dart
import 'package:audioplayers/audioplayers.dart';
import 'dart:async';
import '../models/song_model.dart';
/// 播放模式枚举
enum PlayMode {
linear, // 顺序播放
loop, // 循环播放
shuffle, // 随机播放
single, // 单曲循环
}
/// 音频播放服务(单例模式)
class AudioPlayerService {
static final AudioPlayerService _instance = AudioPlayerService._internal();
factory AudioPlayerService() => _instance;
AudioPlayerService._internal();
final AudioPlayer _audioPlayer = AudioPlayer();
PlayMode _playMode = PlayMode.linear;
List<Song> _playQueue = [];
int _currentIndex = -1;
bool _isPlaying = false;
// 状态流
final _stateController = StreamController<bool>.broadcast();
Stream<bool> get stateStream => _stateController.stream;
final _positionController = StreamController<Duration>.broadcast();
Stream<Duration> get positionStream => _positionController.stream;
final _durationController = StreamController<Duration>.broadcast();
Stream<Duration> get durationStream => _durationController.stream;
// Getter
Song? get currentSong => _currentIndex >= 0 && _currentIndex < _playQueue.length
? _playQueue[_currentIndex]
: null;
bool get isPlaying => _isPlaying;
List<Song> get playQueue => _playQueue;
int get currentIndex => _currentIndex;
/// 初始化播放器监听
void init() {
// 监听播放状态
_audioPlayer.onPlayerStateChanged.listen((state) {
_isPlaying = state == PlayerState.playing;
_stateController.add(_isPlaying);
});
// 监听播放位置
_audioPlayer.onPositionChanged.listen((position) {
_positionController.add(position);
});
// 监听总时长
_audioPlayer.onDurationChanged.listen((duration) {
_durationController.add(duration);
});
// 监听播放完成
_audioPlayer.onPlayerComplete.listen((event) {
_handlePlayComplete();
});
}
/// 播放完成处理
void _handlePlayComplete() {
switch (_playMode) {
case PlayMode.linear:
playNext();
break;
case PlayMode.loop:
playNext();
break;
case PlayMode.shuffle:
playNext();
break;
case PlayMode.single:
playCurrent();
break;
}
}
/// 播放歌曲
Future<void> playSong(Song song) async {
try {
final index = _playQueue.indexWhere((s) => s.id == song.id);
if (index != -1) {
_currentIndex = index;
} else {
_playQueue.add(song);
_currentIndex = _playQueue.length - 1;
}
await _playCurrent();
} catch (e) {
print('播放失败:$e');
}
}
/// 播放当前歌曲
Future<void> _playCurrent() async {
if (_currentIndex < 0 || _currentIndex >= _playQueue.length) return;
final song = _playQueue[_currentIndex];
try {
await _audioPlayer.play(DeviceFileSource(song.data));
_isPlaying = true;
_stateController.add(_isPlaying);
} catch (e) {
print('播放当前歌曲失败:$e');
}
}
/// 播放/暂停
Future<void> playPause() async {
if (_isPlaying) {
await _audioPlayer.pause();
_isPlaying = false;
} else {
await _audioPlayer.resume();
_isPlaying = true;
}
_stateController.add(_isPlaying);
}
/// 播放上一首
Future<void> playPrevious() async {
if (_playQueue.isEmpty) return;
if (_currentIndex > 0) {
_currentIndex--;
await _playCurrent();
} else if (_playMode == PlayMode.loop) {
_currentIndex = _playQueue.length - 1;
await _playCurrent();
}
}
/// 播放下一首
Future<void> playNext() async {
if (_playQueue.isEmpty) return;
switch (_playMode) {
case PlayMode.shuffle:
_shuffleQueue();
await _playCurrent();
break;
case PlayMode.linear:
case PlayMode.loop:
if (_currentIndex < _playQueue.length - 1) {
_currentIndex++;
await _playCurrent();
} else if (_playMode == PlayMode.loop) {
_currentIndex = 0;
await _playCurrent();
}
break;
case PlayMode.single:
await _playCurrent();
break;
}
}
/// 随机打乱队列
void _shuffleQueue() {
if (_playQueue.isEmpty) return;
_playQueue.shuffle();
}
/// 跳转到指定位置
Future<void> seekTo(Duration position) async {
await _audioPlayer.seek(position);
}
/// 设置播放进度(别名)
Future<void> seek(Duration position) async {
await _audioPlayer.seek(position);
}
/// 设置播放模式
void setPlayMode(PlayMode mode) {
_playMode = mode;
switch (mode) {
case PlayMode.loop:
_audioPlayer.setReleaseMode(ReleaseMode.loop);
break;
default:
_audioPlayer.setReleaseMode(ReleaseMode.release);
break;
}
}
/// 设置音量 (0.0 - 1.0)
Future<void> setVolume(double volume) async {
await _audioPlayer.setVolume(volume.clamp(0.0, 1.0));
}
/// 切换循环模式
void toggleLoop() {
if (_playMode == PlayMode.loop) {
setPlayMode(PlayMode.linear);
} else {
setPlayMode(PlayMode.loop);
}
}
/// 切换随机播放
void toggleShuffle() {
if (_playMode == PlayMode.shuffle) {
setPlayMode(PlayMode.linear);
} else {
setPlayMode(PlayMode.shuffle);
}
}
/// 停止播放
Future<void> stop() async {
await _audioPlayer.stop();
_isPlaying = false;
_stateController.add(_isPlaying);
}
/// 释放资源
void dispose() {
_audioPlayer.dispose();
_stateController.close();
_positionController.close();
_durationController.close();
}
}
3. 媒体扫描服务 - lib/services/media_scanner_service.dart
import 'package:on_audio_query/on_audio_query.dart';
import '../models/song_model.dart';
/// 媒体扫描服务
class MediaScannerService {
final OnAudioQuery _audioQuery = OnAudioQuery();
/// 扫描所有歌曲
Future<List<Song>> scanAllSongs() async {
try {
final songs = await _audioQuery.querySongs(
sortType: SongSortType.DATE_ADDED,
orderType: OrderType.DESC_OR_GREATER,
uriType: UriType.EXTERNAL,
);
return songs.map((song) => Song.fromAudioQuery(song)).toList();
} catch (e) {
print('扫描歌曲失败:$e');
return [];
}
}
/// 获取所有专辑
Future<List<dynamic>> getAllAlbums() async {
try {
return await _audioQuery.queryAlbums(
sortType: AlbumSortType.ALBUM,
orderType: OrderType.ASC_OR_SMALLER,
);
} catch (e) {
print('获取专辑失败:$e');
return [];
}
}
/// 获取专辑封面
Future<List<int>?> getAlbumArt(int albumId) async {
try {
return await _audioQuery.queryArtwork(
albumId,
ArtworkType.ALBUM,
size: 300,
);
} catch (e) {
return null;
}
}
}
4. 数据库帮助类 - lib/services/database_helper.dart
import 'package:sqflite/sqflite.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart';
class DatabaseHelper {
static final DatabaseHelper _instance = DatabaseHelper._internal();
factory DatabaseHelper() => _instance;
DatabaseHelper._internal();
static Database? _database;
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDatabase();
return _database!;
}
Future<Database> _initDatabase() async {
final directory = await getApplicationDocumentsDirectory();
final path = join(directory.path, 'audio_player.db');
return await openDatabase(
path,
version: 1,
onCreate: _onCreate,
);
}
Future<void> _onCreate(Database db, int version) async {
// 收藏表
await db.execute('''
CREATE TABLE favorites (
song_id INTEGER PRIMARY KEY,
added_at TEXT NOT NULL
)
''');
// 播放列表表
await db.execute('''
CREATE TABLE playlists (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
cover_path TEXT,
created_at TEXT NOT NULL
)
''');
// 播放列表 - 歌曲关联表
await db.execute('''
CREATE TABLE playlist_songs (
playlist_id INTEGER NOT NULL,
song_id INTEGER NOT NULL,
position INTEGER NOT NULL,
added_at TEXT NOT NULL,
FOREIGN KEY (playlist_id) REFERENCES playlists (id) ON DELETE CASCADE,
PRIMARY KEY (playlist_id, song_id)
)
''');
// 最近播放表
await db.execute('''
CREATE TABLE recent_plays (
song_id INTEGER PRIMARY KEY,
played_at TEXT NOT NULL
)
''');
}
// ========== 收藏操作 ==========
Future<void> addToFavorites(int songId) async {
final db = await database;
await db.insert(
'favorites',
{
'song_id': songId,
'added_at': DateTime.now().toIso8601String(),
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
Future<void> removeFromFavorites(int songId) async {
final db = await database;
await db.delete(
'favorites',
where: 'song_id = ?',
whereArgs: [songId],
);
}
Future<bool> isFavorite(int songId) async {
final db = await database;
final result = await db.query(
'favorites',
where: 'song_id = ?',
whereArgs: [songId],
);
return result.isNotEmpty;
}
Future<List<int>> getFavoriteSongIds() async {
final db = await database;
final result = await db.query('favorites');
return result.map((row) => row['song_id'] as int).toList();
}
// ========== 播放列表操作 ==========
Future<int> createPlaylist(String name, {String? coverPath}) async {
final db = await database;
return await db.insert('playlists', {
'name': name,
'cover_path': coverPath,
'created_at': DateTime.now().toIso8601String(),
});
}
Future<List<dynamic>> getPlaylists() async {
final db = await database;
final result = await db.query('playlists', orderBy: 'created_at DESC');
return result;
}
Future<void> addSongToPlaylist(int playlistId, int songId, int position) async {
final db = await database;
await db.insert(
'playlist_songs',
{
'playlist_id': playlistId,
'song_id': songId,
'position': position,
'added_at': DateTime.now().toIso8601String(),
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
Future<List<int>> getPlaylistSongs(int playlistId) async {
final db = await database;
final result = await db.query(
'playlist_songs',
where: 'playlist_id = ?',
whereArgs: [playlistId],
orderBy: 'position ASC',
);
return result.map((row) => row['song_id'] as int).toList();
}
// ========== 最近播放操作 ==========
Future<void> addToRecent(int songId) async {
final db = await database;
await db.insert(
'recent_plays',
{
'song_id': songId,
'played_at': DateTime.now().toIso8601String(),
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
Future<List<int>> getRecentPlayed({int limit = 20}) async {
final db = await database;
final result = await db.query(
'recent_plays',
orderBy: 'played_at DESC',
limit: limit,
);
return result.map((row) => row['song_id'] as int).toList();
}
}
5. 音频状态管理 - lib/providers/audio_provider.dart
import 'package:flutter/material.dart';
import '../services/media_scanner_service.dart';
import '../services/audio_service.dart';
import '../services/database_helper.dart';
import '../models/song_model.dart';
/// 音频播放状态管理
class AudioProvider extends ChangeNotifier {
final MediaScannerService _scanner = MediaScannerService();
final AudioPlayerService _player = AudioPlayerService();
final DatabaseHelper _db = DatabaseHelper();
List<Song> _songs = [];
List<dynamic> _albums = [];
List<Song> _favoriteSongs = [];
List<Song> _recentSongs = [];
bool _isLoading = false;
bool _isPlaying = false;
Song? _currentSong;
Duration _currentPosition = Duration.zero;
Duration _totalDuration = Duration.zero;
// Getters
List<Song> get songs => _songs;
List<dynamic> get albums => _albums;
List<Song> get favoriteSongs => _favoriteSongs;
List<Song> get recentSongs => _recentSongs;
bool get isLoading => _isLoading;
bool get isPlaying => _isPlaying;
Song? get currentSong => _currentSong;
Duration get currentPosition => _currentPosition;
Duration get totalDuration => _totalDuration;
double get progress => _totalDuration.inMilliseconds > 0
? _currentPosition.inMilliseconds / _totalDuration.inMilliseconds
: 0.0;
AudioProvider() {
_player.init();
_initPlayerListeners();
}
void _initPlayerListeners() {
_player.stateStream.listen((state) {
_isPlaying = _player.isPlaying;
notifyListeners();
});
_player.positionStream.listen((position) {
_currentPosition = position;
notifyListeners();
});
_player.durationStream.listen((duration) {
_totalDuration = duration;
notifyListeners();
});
}
/// 加载所有歌曲
Future<void> loadSongs() async {
_isLoading = true;
notifyListeners();
try {
_songs = await _scanner.scanAllSongs();
// 加载收藏状态
final favoriteIds = await _db.getFavoriteSongIds();
for (var song in _songs) {
song.isFavorite = favoriteIds.contains(song.id);
}
// 加载最近播放
await _loadRecentSongs();
// 加载收藏歌曲
await _loadFavoriteSongs();
} catch (e) {
print('加载歌曲失败:$e');
}
_isLoading = false;
notifyListeners();
}
/// 加载收藏歌曲
Future<void> _loadFavoriteSongs() async {
final favoriteIds = await _db.getFavoriteSongIds();
_favoriteSongs = _songs.where((s) => favoriteIds.contains(s.id)).toList();
}
/// 加载最近播放
Future<void> _loadRecentSongs() async {
final recentIds = await _db.getRecentPlayed(limit: 20);
_recentSongs = recentIds
.map((id) => _songs.firstWhere((s) => s.id == id, orElse: () => _songs.first))
.toList();
}
/// 播放歌曲
Future<void> playSong(Song song) async {
await _player.playSong(song);
_currentSong = song;
_isPlaying = true;
// 添加到最近播放
await _db.addToRecent(song.id);
await _loadRecentSongs();
notifyListeners();
}
/// 播放/暂停
Future<void> playPause() async {
await _player.playPause();
_isPlaying = _player.isPlaying;
notifyListeners();
}
/// 播放上一首
Future<void> playPrevious() async {
await _player.playPrevious();
_currentSong = _player.currentSong;
notifyListeners();
}
/// 播放下一首
Future<void> playNext() async {
await _player.playNext();
_currentSong = _player.currentSong;
notifyListeners();
}
/// 切换收藏状态
Future<void> toggleFavorite(int songId) async {
final song = _songs.firstWhere((s) => s.id == songId);
song.isFavorite = !song.isFavorite;
if (song.isFavorite) {
await _db.addToFavorites(songId);
_favoriteSongs.add(song);
} else {
await _db.removeFromFavorites(songId);
_favoriteSongs.removeWhere((s) => s.id == songId);
}
notifyListeners();
}
/// 设置播放进度
Future<void> seek(Duration position) async {
await _player.seek(position);
}
/// 播放下一首(快捷方法)
void next() {
playNext();
}
/// 播放上一首(快捷方法)
void previous() {
playPrevious();
}
/// 切换循环
void toggleLoop() {
_player.toggleLoop();
notifyListeners();
}
/// 切换随机播放
void toggleShuffle() {
_player.toggleShuffle();
notifyListeners();
}
/// 跳转到指定位置
void seekTo(Duration position) {
seek(position);
}
/// 获取当前歌曲时长
String get formattedPosition {
final minutes = _currentPosition.inMinutes;
final seconds = (_currentPosition.inSeconds % 60).toString().padLeft(2, '0');
return '$minutes:$seconds';
}
/// 获取总时长
String get formattedTotalDuration {
final minutes = _totalDuration.inMinutes;
final seconds = (_totalDuration.inSeconds % 60).toString().padLeft(2, '0');
return '$minutes:$seconds';
}
}
6. 播放列表管理 - lib/providers/playlist_provider.dart
import 'package:flutter/material.dart';
import '../services/database_helper.dart';
import '../models/song_model.dart';
/// 播放列表管理
class PlaylistProvider extends ChangeNotifier {
final DatabaseHelper _db = DatabaseHelper();
List<dynamic> _playlists = [];
bool _isLoading = false;
List<dynamic> get playlists => _playlists;
bool get isLoading => _isLoading;
/// 加载所有播放列表
Future<void> loadPlaylists() async {
_isLoading = true;
notifyListeners();
try {
_playlists = await _db.getPlaylists();
} catch (e) {
print('加载播放列表失败:$e');
}
_isLoading = false;
notifyListeners();
}
/// 创建播放列表
Future<void> createPlaylist(String name) async {
await _db.createPlaylist(name);
await loadPlaylists();
}
/// 添加歌曲到播放列表
Future<void> addSongToPlaylist(int playlistId, Song song) async {
final songs = await _db.getPlaylistSongs(playlistId);
await _db.addSongToPlaylist(playlistId, song.id, songs.length);
}
/// 删除播放列表
Future<void> deletePlaylist(int playlistId) async {
// TODO: 实现删除逻辑
}
}
7. 主界面 - lib/screens/home_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/audio_provider.dart';
import '../providers/playlist_provider.dart';
import '../widgets/mini_player_bar.dart';
import 'player_screen.dart';
import 'playlist_screen.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
int _selectedIndex = 0;
void initState() {
super.initState();
// 初始化加载歌曲
Future.microtask(() {
context.read<AudioProvider>().loadSongs();
});
}
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: _selectedIndex,
children: [
_buildMusicTab(),
_buildPlaylistsTab(),
_buildProfileTab(),
],
),
bottomNavigationBar: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 迷你播放器
Consumer<AudioProvider>(
builder: (context, provider, child) {
if (provider.currentSong == null) {
return const SizedBox.shrink();
}
return MiniPlayerBar(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const PlayerScreen(),
),
);
},
);
},
),
// 底部导航栏
BottomNavigationBar(
currentIndex: _selectedIndex,
onTap: (index) => setState(() => _selectedIndex = index),
type: BottomNavigationBarType.fixed,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.music_note),
label: '音乐',
),
BottomNavigationBarItem(
icon: Icon(Icons.playlist_play),
label: '播放列表',
),
BottomNavigationBarItem(
icon: Icon(Icons.person),
label: '我的',
),
],
),
],
),
);
}
Widget _buildMusicTab() {
return Consumer<AudioProvider>(
builder: (context, provider, child) {
if (provider.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (provider.songs.isEmpty) {
return const Center(
child: Text('没有找到音乐文件'),
);
}
return ListView.builder(
itemCount: provider.songs.length,
itemBuilder: (context, index) {
final song = provider.songs[index];
return ListTile(
leading: const CircleAvatar(
child: Icon(Icons.music_note),
),
title: Text(song.title),
subtitle: Text(song.artist),
trailing: IconButton(
icon: Icon(
song.isFavorite ? Icons.favorite : Icons.favorite_border,
),
onPressed: () => provider.toggleFavorite(song.id),
),
onTap: () => provider.playSong(song),
);
},
);
},
);
}
Widget _buildPlaylistsTab() {
return Consumer<PlaylistProvider>(
builder: (context, provider, child) {
if (provider.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (provider.playlists.isEmpty) {
return const Center(
child: Text('暂无播放列表'),
);
}
return ListView.builder(
itemCount: provider.playlists.length,
itemBuilder: (context, index) {
final playlist = provider.playlists[index];
return ListTile(
leading: const CircleAvatar(
child: Icon(Icons.playlist_play),
),
title: Text(playlist['name'] ?? '未命名'),
subtitle: Text('播放列表'),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => PlaylistScreen(playlistId: playlist['id']),
),
);
},
);
},
);
},
);
}
Widget _buildProfileTab() {
return const Center(
child: Text('个人中心'),
);
}
}
8. 播放器界面 - lib/screens/player_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/audio_provider.dart';
import '../widgets/seek_slider.dart';
import '../widgets/circular_album_art.dart';
class PlayerScreen extends StatelessWidget {
const PlayerScreen({super.key});
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: SafeArea(
child: Consumer<AudioProvider>(
builder: (context, provider, child) {
final song = provider.currentSong;
if (song == null) {
return const Center(
child: Text('没有正在播放的歌曲'),
);
}
return Column(
children: [
// 顶部栏
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
IconButton(
icon: const Icon(Icons.arrow_downward, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
const Spacer(),
IconButton(
icon: const Icon(Icons.more_vert, color: Colors.white),
onPressed: () => _showOptionsMenu(context, provider),
),
],
),
),
// 专辑封面
Expanded(
flex: 3,
child: CircularAlbumArt(
song: song,
isPlaying: provider.isPlaying,
),
),
const SizedBox(height: 32),
// 歌曲信息
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Column(
children: [
Text(
song.title,
style: const TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
Text(
song.artist,
style: TextStyle(
color: Colors.white.withOpacity(0.7),
fontSize: 16,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
const SizedBox(height: 32),
// 进度条
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: SeekSlider(
progress: provider.progress,
position: provider.currentPosition,
duration: provider.totalDuration,
onChanged: (value) {
final position = Duration(
milliseconds: (value * provider.totalDuration.inMilliseconds).round(),
);
provider.seekTo(position);
},
),
),
const SizedBox(height: 24),
// 控制按钮
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.skip_previous, size: 40),
color: Colors.white,
onPressed: provider.previous,
),
const SizedBox(width: 24),
_buildPlayPauseButton(context, provider),
const SizedBox(width: 24),
IconButton(
icon: const Icon(Icons.skip_next, size: 40),
color: Colors.white,
onPressed: provider.next,
),
],
),
),
const SizedBox(height: 24),
// 播放模式按钮
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
icon: const Icon(Icons.shuffle, color: Colors.white),
onPressed: provider.toggleShuffle,
),
IconButton(
icon: const Icon(Icons.repeat, color: Colors.white),
onPressed: provider.toggleLoop,
),
],
),
),
const SizedBox(height: 20),
],
);
},
),
),
);
}
Widget _buildPlayPauseButton(BuildContext context, AudioProvider provider) {
return GestureDetector(
onTap: provider.playPause,
child: Container(
width: 70,
height: 70,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).primaryColor,
boxShadow: [
BoxShadow(
color: Colors.purple.withOpacity(0.3),
blurRadius: 20,
spreadRadius: 5,
),
],
),
child: Icon(
provider.isPlaying ? Icons.pause : Icons.play_arrow,
size: 40,
color: Colors.white,
),
),
);
}
void _showOptionsMenu(BuildContext context, AudioProvider provider) {
showModalBottomSheet(
context: context,
builder: (context) => Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.favorite),
title: const Text('收藏'),
onTap: () {
if (provider.currentSong != null) {
provider.toggleFavorite(provider.currentSong!.id);
}
Navigator.pop(context);
},
),
ListTile(
leading: const Icon(Icons.playlist_add),
title: const Text('添加到播放列表'),
onTap: () {
Navigator.pop(context);
// TODO: 显示播放列表选择对话框
},
),
],
),
);
}
}
9. 播放列表界面 - lib/screens/playlist_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/audio_provider.dart';
import '../providers/playlist_provider.dart';
class PlaylistScreen extends StatelessWidget {
final int playlistId;
const PlaylistScreen({super.key, required this.playlistId});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('播放列表'),
backgroundColor: Colors.black,
foregroundColor: Colors.white,
),
backgroundColor: Colors.black,
body: Consumer<PlaylistProvider>(
builder: (context, provider, child) {
// TODO: 实现播放列表歌曲列表
return const Center(
child: Text('播放列表详情'),
);
},
),
);
}
}
10. 迷你播放器 - lib/widgets/mini_player_bar.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/audio_provider.dart';
class MiniPlayerBar extends StatelessWidget {
final VoidCallback onTap;
const MiniPlayerBar({super.key, required this.onTap});
Widget build(BuildContext context) {
return Consumer<AudioProvider>(
builder: (context, provider, child) {
final song = provider.currentSong;
if (song == null) {
return const SizedBox.shrink();
}
return GestureDetector(
onTap: onTap,
child: Container(
height: 64,
decoration: BoxDecoration(
color: Colors.grey[900],
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, -2),
),
],
),
child: Column(
children: [
// 进度条
LinearProgressIndicator(
value: provider.progress,
backgroundColor: Colors.grey[800],
valueColor: const AlwaysStoppedAnimation<Color>(Colors.white),
minHeight: 2,
),
// 歌曲信息和控制按钮
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
// 专辑封面缩略图
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: Colors.grey[800],
borderRadius: BorderRadius.circular(4),
),
child: const Icon(Icons.music_note, color: Colors.white),
),
const SizedBox(width: 12),
// 歌曲信息
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
song.title,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Text(
song.artist,
style: TextStyle(
color: Colors.grey[400],
fontSize: 12,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
// 播放控制按钮
Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.skip_previous, color: Colors.white),
onPressed: provider.previous,
),
IconButton(
icon: Icon(
provider.isPlaying ? Icons.pause : Icons.play_arrow,
color: Colors.white,
),
onPressed: provider.playPause,
),
],
),
],
),
),
),
],
),
),
);
},
);
}
}
11. 进度条控件 - lib/widgets/seek_slider.dart
import 'package:flutter/material.dart';
class SeekSlider extends StatelessWidget {
final double progress;
final Duration position;
final Duration duration;
final ValueChanged<double> onChanged;
const SeekSlider({
super.key,
required this.progress,
required this.position,
required this.duration,
required this.onChanged,
});
Widget build(BuildContext context) {
return Column(
children: [
SliderTheme(
data: SliderTheme.of(context).copyWith(
trackHeight: 2.0,
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 6.0),
overlayShape: const RoundSliderOverlayShape(overlayRadius: 14.0),
activeTrackColor: Colors.white,
inactiveTrackColor: Colors.white24,
thumbColor: Colors.white,
overlayColor: Colors.white24,
),
child: Slider(
value: progress.clamp(0.0, 1.0),
onChanged: onChanged,
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
_formatDuration(position),
style: TextStyle(
color: Colors.white.withOpacity(0.7),
fontSize: 12,
),
),
Text(
_formatDuration(duration),
style: TextStyle(
color: Colors.white.withOpacity(0.7),
fontSize: 12,
),
),
],
),
),
],
);
}
String _formatDuration(Duration duration) {
final minutes = duration.inMinutes;
final seconds = (duration.inSeconds % 60).toString().padLeft(2, '0');
return '$minutes:$seconds';
}
}
12. 圆形专辑封面 - lib/widgets/circular_album_art.dart
import 'dart:typed_data';
import 'package:flutter/material.dart';
import '../models/song_model.dart';
import '../services/media_scanner_service.dart';
class CircularAlbumArt extends StatefulWidget {
final Song song;
final bool isPlaying;
const CircularAlbumArt({
super.key,
required this.song,
required this.isPlaying,
});
State<CircularAlbumArt> createState() => _CircularAlbumArtState();
}
class _CircularAlbumArtState extends State<CircularAlbumArt>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
Uint8List? _albumArt;
final MediaScannerService _scanner = MediaScannerService();
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 20),
vsync: this,
);
if (widget.isPlaying) {
_controller.repeat();
}
_loadAlbumArt();
}
void didUpdateWidget(CircularAlbumArt oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.isPlaying != oldWidget.isPlaying) {
if (widget.isPlaying) {
_controller.repeat();
} else {
_controller.stop();
}
}
if (widget.song.id != oldWidget.song.id) {
_loadAlbumArt();
}
}
Future<void> _loadAlbumArt() async {
final artwork = await _scanner.getAlbumArt(widget.song.albumId);
if (artwork != null && mounted) {
setState(() {
_albumArt = Uint8List.fromList(artwork);
});
}
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Center(
child: RotationTransition(
turns: _controller,
child: Container(
width: 300,
height: 300,
decoration: BoxDecoration(
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.5),
blurRadius: 20,
spreadRadius: 5,
),
],
),
child: ClipOval(
child: _albumArt != null
? Image.memory(
_albumArt!,
fit: BoxFit.cover,
)
: Container(
color: Colors.grey[800],
child: const Icon(
Icons.music_note,
size: 100,
color: Colors.white54,
),
),
),
),
),
);
}
}
13. 应用入口 - lib/main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'providers/audio_provider.dart';
import 'providers/playlist_provider.dart';
import 'screens/home_screen.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => AudioProvider()),
ChangeNotifierProvider(create: (_) => PlaylistProvider()),
],
child: MaterialApp(
title: '音乐播放器',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.purple,
brightness: Brightness.dark,
),
useMaterial3: true,
),
home: const HomeScreen(),
),
);
}
}
HarmonyOS 配置
1. 配置 module.json5
文件位置:ohos/entry/src/main/module.json5
{
"module": {
"name": "entry",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"deviceTypes": [
"phone",
"tablet"
],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:EntryAbility_desc",
"icon": "$media:icon",
"label": "$string:EntryAbility_name",
"startWindowIcon": "$media:icon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": [
"entity.system.home"
],
"actions": [
"action.system.home"
]
}
]
}
],
"requestPermissions": [
{
"name": "ohos.permission.READ_MEDIA",
"reason": "$string:permission_read_media_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
},
{
"name": "ohos.permission.MEDIA_LOCATION",
"reason": "$string:permission_media_location_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
},
{
"name": "ohos.permission.KEEP_BACKGROUND_RUNNING",
"reason": "$string:permission_keep_background_running_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "always"
}
}
]
}
}
2. 配置字符串资源
文件位置:ohos/entry/src/main/resources/base/element/string.json
{
"string": [
{
"name": "module_desc",
"value": "音乐播放器模块"
},
{
"name": "EntryAbility_desc",
"value": "音乐播放器入口"
},
{
"name": "EntryAbility_name",
"value": "音乐播放器"
},
{
"name": "permission_read_media_reason",
"value": "需要访问您的媒体文件以播放音乐"
},
{
"name": "permission_media_location_reason",
"value": "需要访问媒体位置信息"
},
{
"name": "permission_keep_background_running_reason",
"value": "需要在后台运行以持续播放音乐"
}
]
}
3. 配置 build-profile.json5
文件位置:ohos/build-profile.json5
确保配置了正确的签名配置:
{
"app": {
"signingConfigs": [],
"products": [
{
"name": "default",
"signingConfig": "default",
"compatibleSdkVersion": "5.0.0(12)",
"runtimeOS": "HarmonyOS"
}
]
}
}
常见问题解决
1. 权限问题
如果应用无法扫描到音乐文件,请确保:
- 已在
module.json5中正确配置权限 - 已在设备设置中授予应用存储权限
- 设备中有音频文件
2. 依赖包问题
如果遇到依赖包解析错误:
flutter clean
flutter pub get
3. 构建失败
如果 HarmonyOS 构建失败:
- 确保 DevEco Studio 已正确安装
- 检查签名配置是否正确
- 尝试重启 DevEco Studio
成果展示

更多推荐




所有评论(0)