GitHub地址:github.com/lizixian18/…java
在平常開發中,若是項目中須要添加音頻播放功能,是一件很麻煩的事情。通常須要處理的事情大概有音頻服務的封裝,播放器的封裝,通知欄管理,聯動系統媒體中心,音頻焦點的獲取,播放列表維護,各類API方法的編寫等等...若是完善一點,還須要用到IPC去實現。 可見須要處理的事情很是多。android
因此 MusicLibrary 就這樣編寫出來了,它的目標是幫你所有實現好因此音頻相關的事情,讓你能夠專一於其餘事情。git
爲體現 MusicLibrary 在實際上的應用,編寫了一個簡單的音樂播放器 NiceMusic。github
GitHub地址:github.com/lizixian18/…算法
MusicLibrary 的基本用法,能夠參考這個項目中的實現。
在 NiceMusic 中,你能夠學到下面的東西:數組
關於 IPC 和 AIDL 等用法和原理再也不講,若是不瞭解請本身查閱資料。
能夠看到,PlayControl
實際上是一個Binder
,鏈接着客戶端和服務端。app
QueueManager
是播放列表管理類,裏面維護着當前的播放列表和當前的音頻索引。
播放列表存儲在一個 ArrayList 裏面,音頻索引默認是 0:框架
public QueueManager(MetadataUpdateListener listener, PlayMode playMode) { mPlayingQueue = Collections.synchronizedList(new ArrayList<SongInfo>()); mCurrentIndex = 0; ... } 複製代碼
當調用設置播放列表相關API的時候,其實是調用了裏面的setCurrentQueue
方法,每次播放列表都會先清空,再賦值:oop
public void setCurrentQueue(List<SongInfo> newQueue, int currentIndex) { int index = 0; if (currentIndex != -1) { index = currentIndex; } mCurrentIndex = Math.max(index, 0); mPlayingQueue.clear(); mPlayingQueue.addAll(newQueue); //通知播放列表更新了 List<MediaSessionCompat.QueueItem> queueItems = QueueHelper.getQueueItems(mPlayingQueue); if (mListener != null) { mListener.onQueueUpdated(queueItems, mPlayingQueue); } } 複製代碼
當播放列表更新後,會把播放列表封裝成一個 QueueItem 列表回調給 MediaSessionManager 作鎖屏的時候媒體相關的操做。
獲得當前播放音樂,播放指定音樂等操做其實是操做音頻索引mCurrentIndex
,而後根據索引取出列表中對應的音頻信息。
上一首下一首等操做,其實是調用了skipQueuePosition
方法,這個方法中採用了取餘的算法來計算上一首下一首的索引,
而不是加一或者減一,這樣的一個好處是避免了數組越界或者說計算更方便:
public boolean skipQueuePosition(int amount) { int index = mCurrentIndex + amount; if (index < 0) { // 在第一首歌曲以前向後跳,讓你在第一首歌曲上 index = 0; } else { //當在最後一首歌時點下一首將返回第一首個 index %= mPlayingQueue.size(); } if (!QueueHelper.isIndexPlayable(index, mPlayingQueue)) { return false; } mCurrentIndex = index; return true; } 複製代碼
參數 amount 是維度的意思,能夠看到,傳 1 則會取下一首,傳 -1 則會取上一首,事實上能夠取到任何一首音頻,
只要維度不同就能夠。
播放音樂時,先是調用了setCurrentQueueIndex
方法設置好音頻索引後再經過回調交給PlaybackManager
去作真正的播放處理。
private void setCurrentQueueIndex(int index, boolean isJustPlay, boolean isSwitchMusic) { if (index >= 0 && index < mPlayingQueue.size()) { mCurrentIndex = index; if (mListener != null) { mListener.onCurrentQueueIndexUpdated(mCurrentIndex, isJustPlay, isSwitchMusic); } } } 複製代碼
QueueManager 須要說明的感受就這些,其餘若是有興趣能夠clone代碼後再具體細看。
PlaybackManager 是播放管理類,負責操做播放,暫停等播放控制操做。
它實現了 Playback.Callback 接口,而 Playback 是定義了播放器相關操做的接口。
具體的播放器 ExoPlayer、MediaPlayer 的實現均實現了 Playback 接口,而 PlaybackManager 則是經過 Playback
來統一管理播放器的相關操做。 因此,若是想再添加一個播放器,只須要實現 Playback 接口便可。
播放:
public void handlePlayRequest() { SongInfo currentMusic = mQueueManager.getCurrentMusic(); if (currentMusic != null) { String mediaId = currentMusic.getSongId(); boolean mediaHasChanged = !TextUtils.equals(mediaId, mCurrentMediaId); if (mediaHasChanged) { mCurrentMediaId = mediaId; notifyPlaybackSwitch(currentMusic); } //播放 mPlayback.play(currentMusic); //更新媒體信息 mQueueManager.updateMetadata(); updatePlaybackState(null); } } 複製代碼
播放方法有幾個步驟:
mPlayback.play(currentMusic)
交給具體播放器去播放。暫停:
public void handlePauseRequest() { if (mPlayback.isPlaying()) { mPlayback.pause(); updatePlaybackState(null); } } 複製代碼
暫停是直接交給具體播放器去暫停,而後回調播放狀態狀態。
中止:
public void handleStopRequest(String withError) { mPlayback.stop(true); updatePlaybackState(withError); } 複製代碼
中止也是一樣道理。
基本上PlaybackManager
裏面的操做都是圍繞着這三個方法進行,其餘則是一些封裝和回調的處理。
具體的播放器實現參考的是Google的官方例子 android-UniversalMusicPlayer 這項目真的很是不錯。
這個類主要是管理媒體信息MediaSessionCompat
,他的寫法是比較固定的,能夠參考這篇文章中的聯動系統媒體中心 的介紹
也能夠參考 Google的官方例子
這個類是封裝了通知欄的相關操做。自定義通知欄的狀況可算是很是複雜了,遠不止是 new 一個 Notification。(可能我仍是菜鳥)
NotificationCompat.Builder 裏面 setContentView
的方法一共有兩個,一個是 setCustomContentView()
一個是 setCustomBigContentView()
可知道區別就是大小的區別吧,對應的 RemoteView 也是兩個:RemoteView 和 BigRemoteView
而不一樣的手機,有的通知欄背景是白色的,有的是透明或者黑色的(如魅族,小米等),這時候你就須要根據不一樣的背景顯示不一樣的樣式(除非你在佈局裏面寫死背景色,可是那樣真的很醜)
因此通知欄總共須要的佈局有四個:
設置 ContentView 以下所示:
... if (Build.VERSION.SDK_INT >= 24) { notificationBuilder.setCustomContentView(mRemoteView); if (mBigRemoteView != null) { notificationBuilder.setCustomBigContentView(mBigRemoteView); } } ... Notification notification; if (Build.VERSION.SDK_INT >= 16) { notification = notificationBuilder.build(); } else { notification = notificationBuilder.getNotification(); } if (Build.VERSION.SDK_INT < 24) { notification.contentView = mRemoteView; if (Build.VERSION.SDK_INT >= 16 && mBigRemoteView != null) { notification.bigContentView = mBigRemoteView; } } ... 複製代碼
在配置通知欄的時候,最重要的就是如何獲取到對應的資源文件和佈局裏面相關的控件,是經過 Resources#getIdentifier
方法去獲取:
private Resources res; private String packageName; public MediaNotificationManager(){ packageName = mService.getApplicationContext().getPackageName(); res = mService.getApplicationContext().getResources(); } private int getResourceId(String name, String className) { return res.getIdentifier(name, className, packageName); } 複製代碼
由於須要能動態配置,因此對通知欄的相關資源和id等命名就須要制定好約定了。好比我要獲取
白色背景下ContentView的佈局文件賦值給RemoteView:
RemoteViews remoteView = new RemoteViews(packageName, getResourceId("view_notify_light_play", "layout")); 複製代碼
只要你的佈局文件命名爲 view_notify_light_play.xml
就能正確獲取了。
因此不一樣的佈局和不一樣的資源獲取所有都是經過 getResourceId
方法獲取。
更新UI分爲下面3個步驟:
更新開始播放的時候播放/暫停按鈕UI:
public void updateViewStateAtStart() { if (mNotification != null) { boolean isDark = NotificationColorUtils.isDarkNotificationBar(mService); mRemoteView = createRemoteViews(isDark, false); mBigRemoteView = createRemoteViews(isDark, true); if (Build.VERSION.SDK_INT >= 16) { mNotification.bigContentView = mBigRemoteView; } mNotification.contentView = mRemoteView; if (mRemoteView != null) { mRemoteView.setImageViewResource(getResourceId(ID_IMG_NOTIFY_PLAY_OR_PAUSE, "id"), getResourceId(isDark ? DRAWABLE_NOTIFY_BTN_DARK_PAUSE_SELECTOR : DRAWABLE_NOTIFY_BTN_LIGHT_PAUSE_SELECTOR, "drawable")); if (mBigRemoteView != null) { mBigRemoteView.setImageViewResource(getResourceId(ID_IMG_NOTIFY_PLAY_OR_PAUSE, "id"), getResourceId(isDark ? DRAWABLE_NOTIFY_BTN_DARK_PAUSE_SELECTOR : DRAWABLE_NOTIFY_BTN_LIGHT_PAUSE_SELECTOR, "drawable")); } mNotificationManager.notify(NOTIFICATION_ID, mNotification); } } } 複製代碼
點擊事件經過的就是 RemoteView.setOnClickPendingIntent(PendingIntent pendingIntent)
方法去實現的。
若是可以動態配置,關鍵就是配置 PendingIntent
就能夠了。
思路就是:若是外部有傳PendingIntent
進來,就用傳進來的PendingIntent
,不然就用默認的PendingIntent
。
private PendingIntent startOrPauseIntent; public MediaNotificationManager(){ setStartOrPausePendingIntent(creater.getStartOrPauseIntent()); } private RemoteViews createRemoteViews(){ if (startOrPauseIntent != null) { remoteView.setOnClickPendingIntent(getResourceId(ID_IMG_NOTIFY_PLAY_OR_PAUSE, "id"), startOrPauseIntent); } } private void setStartOrPausePendingIntent(PendingIntent pendingIntent) { startOrPauseIntent = pendingIntent == null ? getPendingIntent(ACTION_PLAY_PAUSE) : pendingIntent; } private PendingIntent getPendingIntent(String action) { Intent intent = new Intent(action); intent.setClass(mService, PlayerReceiver.class); return PendingIntent.getBroadcast(mService, 0, intent, 0); } 複製代碼
能夠看到,完整代碼如上所示,當 creater.getStartOrPauseIntent()
不爲空時,就用 creater.getStartOrPauseIntent()
不然用默認的。
但願你們喜歡! ^_^