系列文章
- Android开源在线音乐播放器——波尼音乐
- Android开源音乐播放器之播放器基本功能
- Android开源音乐播放器之高仿云音乐黑胶唱片
- Android开源音乐播放器之自动滚动歌词
- Android开源音乐播放器之在线音乐列表自动加载更多
前言
音乐播放器是我们最常用的应用之一,也是每部手机都会预装的应用。作为一个合格的音乐播放器,应该具有哪些功能呢?“无非是播放、暂停、切换歌曲、进度调节、切换播放模式、专辑封面显示、歌词显示、歌曲列表、歌曲管理(由于国产手机大多都是修改过的Android系统,因此系统自带播放器功能也不一样,这里以Android原生播放器为参考)这些功能” 一开始我也是这么认为的,但当我着手做的时候,才发现这些功能远远不够。如手机来电时,音乐需要自动暂停播放,耳机拔出时,同样需要暂停,还要支持耳机线控,等等,这些都是需要我们考虑的。
一个合格的音乐播放器应该具有哪些基本素质?
由于播放、暂停、切换歌曲、进度调节等这些功能过于简单,因此不过多讨论,这里只讨论一些容易被忽略的功能。
扫描本地音乐
扫描歌曲是播放器的基本功能,一般通过ContentProvider配合Media相关类查询系统数据库,获得媒体库中的歌曲信息。
/**
* 扫描歌曲
*/
public static void scanMusic(Context context, List<Music> musicList) {
musicList.clear();
Cursor cursor = context.getContentResolver().query(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
new String[]{
BaseColumns._ID,
MediaStore.Audio.AudioColumns.IS_MUSIC,
MediaStore.Audio.AudioColumns.TITLE,
MediaStore.Audio.AudioColumns.ARTIST,
MediaStore.Audio.AudioColumns.ALBUM,
MediaStore.Audio.AudioColumns.ALBUM_ID,
MediaStore.Audio.AudioColumns.DATA,
MediaStore.Audio.AudioColumns.DISPLAY_NAME,
MediaStore.Audio.AudioColumns.SIZE,
MediaStore.Audio.AudioColumns.DURATION
},
null,
null,
MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
if (cursor == null) {
return;
}
while (cursor.moveToNext()) {
// 是否为音乐
int isMusic = cursor.getInt(cursor.getColumnIndex(MediaStore.Audio.Media.IS_MUSIC));
if (isMusic == 0) {
continue;
}
long id = cursor.getLong(cursor.getColumnIndex(BaseColumns._ID));
// 标题
String title = cursor.getString((cursor.getColumnIndex(MediaStore.Audio.AudioColumns.TITLE)));
// 艺术家
String artist = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.AudioColumns.ARTIST));
// 专辑
String album = cursor.getString((cursor.getColumnIndex(MediaStore.Audio.AudioColumns.ALBUM)));
// 专辑封面id,根据该id可以获得专辑封面图片
long albumId = cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.AudioColumns.ALBUM_ID));
// 持续时间
long duration = cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.Media.DURATION));
// 音乐文件路径
String path = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.AudioColumns.DATA));
// 音乐文件名
String fileName = cursor.getString((cursor.getColumnIndex(MediaStore.Audio.AudioColumns.DISPLAY_NAME)));
// 音乐文件大小
long fileSize = cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.Media.SIZE));
Music music = new Music();
music.set...
musicList.add(music);
}
cursor.close();
}
/**
* 从媒体库加载封面
*/
private Bitmap loadCoverFromMediaStore(long albumId) {
ContentResolver resolver = mContext.getContentResolver();
Uri uri = MusicUtils.getMediaStoreAlbumCoverUri(albumId);
InputStream is;
try {
is = resolver.openInputStream(uri);
} catch (FileNotFoundException ignored) {
return null;
}
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
return BitmapFactory.decodeStream(is, null, options);
}
通过以上方法基本可以获得音乐的所有信息,弊端是依赖于Android系统媒体库,有时新增音乐后没有通知系统扫描,就无法获得该音乐的信息,不够灵活。
避免播放器内存被系统回收
我们都知道Android系统有自动回收内存机制,如果系统内存紧张,就会触发该机制,应用就有可能被回收,不过Android提供了前台机制,保证内存不足时也不会回收该应用。
/**
* 播放时启动前台机制
*/
public static void showPlay(Music music) {
playService.startForeground(NOTIFICATION_ID, buildNotification(playService, music, true));
}
/**
* 暂停时取消前台机制
*/
public static void showPause(Music music) {
playService.stopForeground(false);
notificationManager.notify(NOTIFICATION_ID, buildNotification(playService, music, false));
}
捕获/丢弃音乐焦点
大家可能不懂这个标题是什么意思,别着急,让我细细道来。
大家有没有试过,如果手机上安装了两个音乐播放器,当一个正在播放的时候,打开第二个播放歌曲,有没有发现第一个自动暂停了?
或者正在听歌时来电话了,音乐暂停了,挂断电话后音乐又继续播放了,
或者收到通知的时候音乐的音量变小了一下又恢复。
“-纳尼!难道不是自动暂停?”
“-图样图森破!”
这其实是因为播放器在后台处理了音频焦点的原因。
public class AudioFocusManager implements AudioManager.OnAudioFocusChangeListener {
private PlayService mPlayService;
private AudioManager mAudioManager;
private boolean isPausedByFocusLossTransient;
private int mVolumeWhenFocusLossTransientCanDuck;
public AudioFocusManager(@NonNull PlayService playService) {
mPlayService = playService;
mAudioManager = (AudioManager) playService.getSystemService(AUDIO_SERVICE);
}
/**
* 播放音乐前先请求音频焦点
*/
public boolean requestAudioFocus() {
return mAudioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN)
== AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
}
/**
* 退出播放器后不再占用音频焦点
*/
public void abandonAudioFocus() {
mAudioManager.abandonAudioFocus(this);
}
/**
* 音频焦点监听回调
*/
@Override
public void onAudioFocusChange(int focusChange) {
int volume;
switch (focusChange) {
// 重新获得焦点
case AudioManager.AUDIOFOCUS_GAIN:
if (!willPlay() && isPausedByFocusLossTransient) {
// 通话结束,恢复播放
mPlayService.playPause();
}
volume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
if (mVolumeWhenFocusLossTransientCanDuck > 0 && volume == mVolumeWhenFocusLossTransientCanDuck / 2) {
// 恢复音量
mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, mVolumeWhenFocusLossTransientCanDuck,
AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE);
}
isPausedByFocusLossTransient = false;
mVolumeWhenFocusLossTransientCanDuck = 0;
break;
// 永久丢失焦点,如被其他播放器抢占
case AudioManager.AUDIOFOCUS_LOSS:
if (willPlay()) {
forceStop();
}
break;
// 短暂丢失焦点,如来电
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
if (willPlay()) {
forceStop();
isPausedByFocusLossTransient = true;
}
break;
// 瞬间丢失焦点,如通知
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
// 音量减小为一半
volume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
if (willPlay() && volume > 0) {
mVolumeWhenFocusLossTransientCanDuck = volume;
mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, mVolumeWhenFocusLossTransientCanDuck / 2,
AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE);
}
break;
}
}
private boolean willPlay() {
return mPlayService.isPreparing() || mPlayService.isPlaying();
}
}
耳机拔出时暂停播放
“-纳尼!难道耳机拔出时不是自动暂停吗?”
“-图样……”
private IntentFilter mNoisyFilter = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
public class NoisyAudioStreamReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// 耳机拔出时暂停播放
PlayService.startCommand(context, Actions.ACTION_MEDIA_PLAY_PAUSE);
}
}
播放时注册广播接收器,暂停时取消注册即可。
联动系统媒体中心
这个标题大家可能也不懂,先放张图吧
明白了吧,我的播放器除了播放了一首音乐之外什么都没做,就可以分别在任务管理、锁屏、负一屏控制我的播放器了,是不是感觉碉堡了。
这些图是在我的小米手机上截的,不保证所有手机都有这些控制功能,但是只要你的Android版本是5.0以上,应该都会有媒体中心,无非是表现形式不同。
Android 5.0中新增了MediaSession类,官方说明是
允许与媒体控制器、音量键、媒体按钮和传输控件交互。
一个类包含了媒体控制和线控等功能,是不是很好用。
现在support-v4包加入了MediaSessionCompat用于在低版本上也能使用这个高大上的功能,
但是低版本上并不能实现媒体控制和线控等功能,低版本的线控功能我会在后面讲。
public class MediaSessionManager {
private static final String TAG = "MediaSessionManager";
private static final long MEDIA_SESSION_ACTIONS = PlaybackStateCompat.ACTION_PLAY
| PlaybackStateCompat.ACTION_PAUSE
| PlaybackStateCompat.ACTION_PLAY_PAUSE
| PlaybackStateCompat.ACTION_SKIP_TO_NEXT
| PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
| PlaybackStateCompat.ACTION_STOP
| PlaybackStateCompat.ACTION_SEEK_TO;
private PlayService mPlayService;
private MediaSessionCompat mMediaSession;
public MediaSessionManager(PlayService playService) {
mPlayService = playService;
setupMediaSession();
}
/**
* 初始化并激活MediaSession
*/
private void setupMediaSession() {
mMediaSession = new MediaSessionCompat(mPlayService, TAG);
mMediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS
| MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS);
mMediaSession.setCallback(callback);
mMediaSession.setActive(true);
}
/**
* 更新播放状态,播放/暂停/拖动进度条时调用
*/
public void updatePlaybackState() {
int state = (mPlayService.isPlaying() || mPlayService.isPreparing())
? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED;
mMediaSession.setPlaybackState(
new PlaybackStateCompat.Builder()
.setActions(MEDIA_SESSION_ACTIONS)
.setState(state, mPlayService.getCurrentPosition(), 1)
.build());
}
/**
* 更新正在播放的音乐信息,切换歌曲时调用
*/
public void updateMetaData(Music music) {
if (music == null) {
mMediaSession.setMetadata(null);
return;
}
MediaMetadataCompat.Builder metaData = new MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, music.getTitle())
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, music.getArtist())
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, music.getAlbum())
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, music.getArtist())
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, music.getDuration())
.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, CoverLoader.getInstance().loadThumbnail(music));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
metaData.putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, AppCache.getMusicList().size());
}
mMediaSession.setMetadata(metaData.build());
}
/**
* 释放MediaSession,退出播放器时调用
*/
public void release() {
mMediaSession.setCallback(null);
mMediaSession.setActive(false);
mMediaSession.release();
}
private MediaSessionCompat.Callback callback = new MediaSessionCompat.Callback() {
@Override
public void onPlay() {
mPlayService.playPause();
}
@Override
public void onPause() {
mPlayService.playPause();
}
@Override
public void onSkipToNext() {
mPlayService.next();
}
@Override
public void onSkipToPrevious() {
mPlayService.prev();
}
@Override
public void onStop() {
mPlayService.stop();
}
@Override
public void onSeekTo(long pos) {
mPlayService.seekTo((int) pos);
}
};
}
耳机线控(适用于API 19及以下)
“-纳尼……”
“-Shut up !”
是的,需要我们自己控制。
如果你已经按照上面的方法激活了MediaSession,那么在5.0以上的系统你已经不需要关心线控功能了,但是在5.0以下仍然需要自己监听耳机按键。
public class RemoteControlReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
KeyEvent event = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
if (event == null || event.getAction() != KeyEvent.ACTION_UP) {
return;
}
Intent serviceIntent;
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_MEDIA_PLAY:
case KeyEvent.KEYCODE_MEDIA_PAUSE:
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
case KeyEvent.KEYCODE_HEADSETHOOK:
serviceIntent = new Intent(context, PlayService.class);
serviceIntent.setAction(Actions.ACTION_MEDIA_PLAY_PAUSE);
context.startService(serviceIntent);
break;
case KeyEvent.KEYCODE_MEDIA_NEXT:
serviceIntent = new Intent(context, PlayService.class);
serviceIntent.setAction(Actions.ACTION_MEDIA_NEXT);
context.startService(serviceIntent);
break;
case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
serviceIntent = new Intent(context, PlayService.class);
serviceIntent.setAction(Actions.ACTION_MEDIA_PREVIOUS);
context.startService(serviceIntent);
break;
}
}
}
<!--在AndroidManifest中注册Receiver-->
<receiver android:name=".receiver.RemoteControlReceiver">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
总结
感谢你能耐心的看到最后,本文主要讨论了音乐播放器容易忽略的重要功能,如果还有其他的本文没有提到的,请大家不吝赐教。