GitHub地址:github.com/lizixian18/…java
在平常開發中,若是項目中須要添加音頻播放功能,是一件很麻煩的事情。通常須要處理的事情大概有音頻服務的封裝,播放器的封裝,通知欄管理,聯動系統媒體中心,音頻焦點的獲取,播放列表維護,各類API方法的編寫等等...若是完善一點,還須要用到IPC去實現。 可見須要處理的事情很是多。android
因此 MusicLibrary 就這樣編寫出來了,它的目標是幫你所有實現好因此音頻相關的事情,讓你能夠專一於其餘事情。git
爲體現 MusicLibrary 在實際上的應用,編寫了一個簡單的音樂播放器 NiceMusic。github
GitHub地址:github.com/lizixian18/…算法
MusicLibrary 的基本用法,能夠參考這個項目中的實現。
在 NiceMusic 中,你能夠學到下面的東西:數組
關於 IPC 和 AIDL 等用法和原理再也不講,若是不瞭解請本身查閱資料。
能夠看到,PlayControl
實際上是一個Binder
,鏈接着客戶端和服務端。框架
QueueManager
是播放列表管理類,裏面維護着當前的播放列表和當前的音頻索引。
播放列表存儲在一個 ArrayList 裏面,音頻索引默認是 0:佈局
public QueueManager(MetadataUpdateListener listener, PlayMode playMode) {
mPlayingQueue = Collections.synchronizedList(new ArrayList<SongInfo>());
mCurrentIndex = 0;
...
}
複製代碼
當調用設置播放列表相關API的時候,其實是調用了裏面的setCurrentQueue
方法,每次播放列表都會先清空,再賦值:ui
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()
不然用默認的。
但願你們喜歡! ^_^