Android开发日记(十四)—— 音乐播放器要点

一款完整的音乐播放器要具备哪些功能呢?

  1. 能播放音乐,这是最基础的功能;
  2. 能上下首切换,能暂停/播放,能拖动播放条进度,这也是常见的控制功能;
  3. 能在手机后台播放,现在没有这个功能都不好意思叫播放器了;
  4. 能有播放列表,能够进行循环,单曲或随机播放;
  5. 能在通知栏上显示歌曲播放信息;
  6. 能够同步显示歌词;
  7. 能够支持线控,所谓线控是指耳机上的按键能够控制播放器,耳机的插拔实现播放器的播放暂停也算属于这个功能内;
  8. 能在播放时进行音乐锁屏,好吧,其实我很讨厌这个功能,因为有了这个功能后大部分手机解锁时都要滑两次屏,有点画蛇添足的意味。

MusicService:在 Android 中后台任务一般使用 Service 来实现,所以可以建立一个 MusicService 来后台播放音乐。

MediaPlayerManager:同时为了更好的管理 MediaPlayer 可以再创建 MediaPlayerManager 类来管理音乐的控制。

MusicPlaylist:播放列表,记录当前要播放歌曲的列表,以便切换歌曲的播放。

音乐播放器细节备忘录

MediaPlayer

一般使用 MediaPlayer 就可以实现多媒体的播放,同时 SetWakeMode 为 PowerManager.PARTIAL_WAKE_LOCK 保证能够在后台运行。

1
2
3
4
5
6
mediaPlayer = new MediaPlayer();

// Make sure the media player will acquire a wake-lock while
// playing. If we don't do that, the CPU might go to sleep while the
// song is playing, causing playback to stop.
mediaPlayer.setWakeMode(mContext, PowerManager.PARTIAL_WAKE_LOCK);

这里同时要处理一下 AudioFocus 的问题,在播放前去请求硬件资源,播放结束后释放硬件资源。

1
2
3
4
5
6
7
8
9
10
/**
* Try to get the system audio focus.
*/

audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN);

/**
* Give up the audio focus.
*/

audioManager.abandonAudioFocus(this);

为了防止 Service 在运行一段时间后自动结束,在播放歌曲时要将其设置为前台服务。

1
2
3
4
5
6
7
public void setAsForeground() {
startForeground(MusicNotification.NOTIFICATION_ID, MusicNotification.getNotification());
}

public void removeForeground(boolean removeNotification) {
stopForeground(removeNotification);
}

使用 MediaSession 来控制播放器

MediaSession 框架是 Google 推出专门解决媒体播放时界面和服务通讯问题。这个框架可以让我们不再使用广播来控制播放器,而且也能适配耳机,蓝牙等一些其它设备,实现线控的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mState = new PlaybackStateCompat.Builder()
.setActions(
ACTION_PLAY |
ACTION_PAUSE |
ACTION_PLAY_PAUSE |
ACTION_SKIP_TO_NEXT |
ACTION_SKIP_TO_PREVIOUS |
ACTION_STOP |
ACTION_PLAY_FROM_MEDIA_ID |
ACTION_PLAY_FROM_SEARCH |
ACTION_SKIP_TO_QUEUE_ITEM |
ACTION_SEEK_TO)
.setState(state, PLAYBACK_POSITION_UNKNOWN, 1.0f, SystemClock.elapsedRealtime())
.build();
mediaSession.setPlaybackState(mState);

线控的实现,MediaSessionCallback 类继承 MediaSessionCompat.Callback,利用 MusicPlayerManager 来实现 onPlay(),onPause,onSkipToNext()等一系列方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 线控
* 使用 {@link MediaButtonReceiver} 来兼容 api21 之前的版本
* 使用{@link MediaSessionCompat#setCallback}控制 api21 之后的版本
*/

private void setUpMediaSession() {
ComponentName mbr = new ComponentName(getPackageName(), MediaButtonReceiver.class.getName());
mediaSession = new MediaSessionCompat(this, "fd", mbr, null);
/* set flags to handle media buttons */
mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS |
MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
/* this is need after Lolipop */
mediaSession.setCallback(new MediaSessionCallback());
setState(STATE_NONE);
}

通知栏

使用 NotificationCompat 创建通知栏信息就足够了。音乐播放器的通知栏一般选择 MediaStyle 风格就能用了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
PendingIntent stopServiceIntent = PendingIntent.getBroadcast(musicService, REQ_CODE, new Intent(ACTION_STOP), PendingIntent.FLAG_CANCEL_CURRENT);
NotificationCompat.Builder builder = new NotificationCompat.Builder(musicService);
builder.setStyle(
new NotificationCompat.MediaStyle().setShowActionsInCompactView(0, 1, 2, 3, 4)
.setMediaSession(musicService.getMediaSession().getSessionToken()).setShowCancelButton(true).setCancelButtonIntent(stopServiceIntent))
.setSmallIcon(R.drawable.music)
.setCategory(CATEGORY_TRANSPORT)
.setVisibility(VISIBILITY_PUBLIC)
.setDeleteIntent(stopServiceIntent)
.setWhen(System.currentTimeMillis())
.setContentIntent(PendingIntent.getActivity(musicService, REQ_CODE,
new Intent(musicService, SongPlayerActivity.class), PendingIntent.FLAG_CANCEL_CURRENT))
.setPriority(PRIORITY_MAX);

// 添加按键动作,包括播放/暂停按钮,上一首下一首按钮
builder.addAction(R.drawable.ic_play_skip_previous, musicService.getString(R.string.music_previous), PendingIntent.getBroadcast(musicService, REQ_CODE,
new Intent(ACTION_PREV), PendingIntent.FLAG_CANCEL_CURRENT));

if (musicService.getState() == STATE_PLAYING) {
builder.addAction(R.drawable.ic_play, musicService.getString(R.string.music_pause), PendingIntent.getBroadcast(musicService, REQ_CODE,
new Intent(ACTION_PAUSE), PendingIntent.FLAG_CANCEL_CURRENT));
} else {
builder.addAction(R.drawable.ic_pause, musicService.getString(R.string.music_play), PendingIntent.getBroadcast(musicService, REQ_CODE,
new Intent(ACTION_PLAY), PendingIntent.FLAG_CANCEL_CURRENT));
}

builder.addAction(R.drawable.ic_play_skip_next, musicService.getString(R.string.music_next), PendingIntent.getBroadcast(musicService, REQ_CODE,
new Intent(ACTION_NEXT), PendingIntent.FLAG_CANCEL_CURRENT));

当歌曲播放状态发生变化时比如上下首切换,暂停等,都要重新向通知栏发送消息以便实时更新通知栏的歌曲信息。

1
NotificationManagerCompat.from(musicService).notify(NOTIFICATION_ID, getNotification());

在一些手机上(特别是MIUI)无法设置通知栏的缩略图片,只能通过自定义View 设置给 NotificationCompat 来避免这个问题。

歌词显示

一般都是利用自己实现的 LrcView 与播放器的进度进行同步,歌词的显示和滚动的效果都是交给 LrcView 处理。
步骤:从文件读取 Lrc 歌词,然后根据[00:02.32]解析歌词时间,与 MediaPlayer 进行同步,根据播放时间显示相应的歌词,动画效果和文字的显示则是交给 LrcView 来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/** 
* 解析歌词时间
* 歌词内容格式如下:
* [00:02.32]陈奕迅
* [00:03.43]好久不见
* [00:05.22]歌词制作 王涛
* @param timeStr
* @return
*/

public int time2Str(String timeStr) {
timeStr = timeStr.replace(":", ".");
timeStr = timeStr.replace(".", "@");

String timeData[] = timeStr.split("@"); //将时间分隔成字符串数组

//分离出分、秒并转换为整型
int minute = Integer.parseInt(timeData[0]);
int second = Integer.parseInt(timeData[1]);
int millisecond = Integer.parseInt(timeData[2]);

//计算上一行与下一行的时间转换为毫秒数
int currentTime = (minute * 60 + second) * 1000 + millisecond * 10;
return currentTime;
}

本地歌曲

本地的专辑和歌曲都可以使用 Context.getContentResolver() 来进行查找,甚至连专辑的封面,艺人等信息也可以找到(只要歌曲携带这些信息,没有携带信息的歌曲都会标记为 unknown)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 查找本地的歌曲
Cursor cursor = context.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, new String[]{"_id", "title", "artist", "album", "duration", "track", "artist_id", "album_id", "_data"}, selectionStatement, null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER);

//查找本地的专辑
Cursor cursor = context.getContentResolver().query(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, new String[]{"_id", "album", "artist", "artist_id", "numsongs", "minyear"}, selection, paramArrayOfString, null);

//查找本地的艺人
Cursor cursor = context.getContentResolver().query(MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI, new String[]{"_id", "artist", "number_of_albums", "number_of_tracks"}, selection, paramArrayOfString, null);

/**
* 获取album的封面照片
*/

private static Uri getAlbumArtUri(long paramInt) {
return ContentUris.withAppendedId(Uri.parse("content://media/external/audio/albumart"), paramInt);
}

网络上的歌曲下载到本地时要想及时的出现在本地歌库中,需要手动的去扫描系统的多媒体库。

1
2
3
4
5
6
7
8
9
/**
* 媒体扫描,防止下载后在sdcard中获取不到歌曲的信息
*
* @param path
*/

public static void mp3Scanner(String path) {
MediaScannerConnection.scanFile(CoreApplication.getInstance().getApplicationContext(),
new String[]{path}, null, null);
}

总结

上面只是列出了一些比较重要的代码,整体的代码可以参考我下面放的 Github 地址,里面有着完整的播放器源码,希望能够帮助你理清如何实现音乐播放的思路。

我自己写的播放器源码
MoeMusic-基于萌否网站api的音乐管理软件


其他一些参考的播放器源码:
googlesample-android-UniversalMusicPlayer
Timber-Material Design Music Player
ListenerMusicPlayer-A Grace Material Design Music Player

关于 MediaSession 的说明
Android:MediaSession框架介绍