系列文章
- Android開源在線音樂播放器——波尼音樂
- Android開源音樂播放器之播放器基本功能
- Android開源音樂播放器之高仿云音樂黑膠唱片
- Android開源音樂播放器之自動滾動歌詞
- Android開源音樂播放器之在線音樂列表自動加載更多
前言
音樂播放器是我們最常用的應用之一晰骑,也是每部手機都會預裝的應用蕉毯。作為一個合格的音樂播放器曼库,應該具有哪些功能呢惹悄?“無非是播放脆贵、暫停忆植、切換歌曲、進度調節(jié)法褥、切換播放模式茫叭、專輯封面顯示、歌詞顯示半等、歌曲列表揍愁、歌曲管理(由于國產手機大多都是修改過的Android系統(tǒng),因此系統(tǒng)自帶播放器功能也不一樣杀饵,這里以Android原生播放器為參考)這些功能” 一開始我也是這么認為的吗垮,但當我著手做的時候,才發(fā)現(xiàn)這些功能遠遠不夠凹髓。如手機來電時,音樂需要自動暫停播放怯屉,耳機拔出時蔚舀,同樣需要暫停,還要支持耳機線控锨络,等等赌躺,這些都是需要我們考慮的。
一個合格的音樂播放器應該具有哪些基本素質羡儿?
由于播放礼患、暫停、切換歌曲掠归、進度調節(jié)等這些功能過于簡單缅叠,因此不過多討論,這里只討論一些容易被忽略的功能虏冻。
掃描本地音樂
掃描歌曲是播放器的基本功能肤粱,一般通過ContentProvider配合Media相關類查詢系統(tǒng)數(shù)據(jù)庫,獲得媒體庫中的歌曲信息厨相。
/**
* 掃描歌曲
*/
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领曼,根據(jù)該id可以獲得專輯封面圖片
long albumId = cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.AudioColumns.ALBUM_ID));
// 持續(xù)時間
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系統(tǒng)媒體庫,有時新增音樂后沒有通知系統(tǒng)掃描庶骄,就無法獲得該音樂的信息毁渗,不夠靈活。
避免播放器內存被系統(tǒng)回收
我們都知道Android系統(tǒng)有自動回收內存機制单刁,如果系統(tǒng)內存緊張灸异,就會觸發(fā)該機制,應用就有可能被回收幻碱,不過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));
}
捕獲/丟棄音樂焦點
大家可能不懂這個標題是什么意思褥傍,別著急儡嘶,讓我細細道來。
大家有沒有試過恍风,如果手機上安裝了兩個音樂播放器蹦狂,當一個正在播放的時候,打開第二個播放歌曲朋贬,有沒有發(fā)現(xiàn)第一個自動暫停了凯楔?
或者正在聽歌時來電話了,音樂暫停了锦募,掛斷電話后音樂又繼續(xù)播放了摆屯,
或者收到通知的時候音樂的音量變小了一下又恢復。
“-納尼糠亩!難道不是自動暫停虐骑?”
“-圖樣圖森破!”
這其實是因為播放器在后臺處理了音頻焦點的原因赎线。
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);
}
/**
* 音頻焦點監(jiān)聽回調
*/
@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);
}
}
播放時注冊廣播接收器狭归,暫停時取消注冊即可。
聯(lián)動系統(tǒng)媒體中心
這個標題大家可能也不懂文判,先放張圖吧
明白了吧唉铜,我的播放器除了播放了一首音樂之外什么都沒做,就可以分別在任務管理律杠、鎖屏潭流、負一屏控制我的播放器了竞惋,是不是感覺碉堡了。
這些圖是在我的小米手機上截的灰嫉,不保證所有手機都有這些控制功能拆宛,但是只要你的Android版本是5.0以上,應該都會有媒體中心讼撒,無非是表現(xiàn)形式不同浑厚。
Android 5.0中新增了MediaSession類,官方說明是
允許與媒體控制器根盒、音量鍵钳幅、媒體按鈕和傳輸控件交互。
一個類包含了媒體控制和線控等功能炎滞,是不是很好用敢艰。
現(xiàn)在support-v4包加入了MediaSessionCompat用于在低版本上也能使用這個高大上的功能,
但是低版本上并不能實現(xiàn)媒體控制和線控等功能册赛,低版本的線控功能我會在后面講钠导。
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);
}
/**
* 更新播放狀態(tài),播放/暫停/拖動進度條時調用
*/
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 !”
是的,需要我們自己控制扼睬。
如果你已經(jīng)按照上面的方法激活了MediaSession逮栅,那么在5.0以上的系統(tǒng)你已經(jīng)不需要關心線控功能了,但是在5.0以下仍然需要自己監(jiān)聽耳機按鍵窗宇。
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>
總結
感謝你能耐心的看到最后证芭,本文主要討論了音樂播放器容易忽略的重要功能,如果還有其他的本文沒有提到的担映,請大家不吝賜教。