Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Music app框架設計及總結

Music app框架設計及總結

編輯:關於Android編程

總體上Music App分為UI界面、服務兩個模塊,其中關於音樂文件的播放都由服務負責,服務配合AIDL使用的,界面綁定服務後可以拿到服務裡所有參數及狀態進行UI刷新。

A. 界面模塊:

1、主界面MusicMainActivity:

 

\

 

主界面主要負責分類顯示音樂文件,以及對音樂文件的各類操作。

MusciAllFragment:顯示所有單曲。

 

\

 

SingerFragment:根據歌手分類顯示。

 

\

 

AlbumFragment:根據專輯分類顯示。

RrecentlyPlayFragment:顯示最近播放的歌曲

PlayListFragment:顯示用戶收藏、錄音以及自己創建的播放列表。

2、音樂播放界面MusicPlayingActivity

主要負責展示具體某一首歌曲的詳細信息以及播放操作等。

 

\

 

3、音樂搜索界面SearchMusicActivity

輸入內容後自動從本地的音樂文件的音樂名,專輯名,歌手名去匹配,匹配後顯示到搜索列表裡。

4、音樂列表界面MusicListActivity

負責顯示播放列表裡的歌曲,跟單曲差不多。

5、編輯界面EditMusicActivity

批量編輯音樂文件,包括刪除和批量添加到播放列表。

6、PlayingFromUriActivity

負責接收外來資源的播放界面,邏輯跟播放界面一樣。

B. 服務模塊:

 

\

 

啟動主界面後綁定服務,所有界面在onResume裡根據服務是否存在判斷是否進行綁定,在onStop裡根據通知欄是否存在判斷是否進行解綁(因為很多時候寫在onDestroy裡執行不到解綁服務的,導致服務永生不死,不符合谷歌規范)。由於服務綁定的都是單個Activity,若結束當前綁定的Activity,服務則會自動解綁執行onUnbind方法。

為了讓服務能一直播放音樂…所以調用服務播放音樂時,就會調用startService為當前服務進行續命,並顯示通知欄。所以就算殺掉APP,服務也會繼續後台播放,若關閉通知欄則調用stopSelf殺掉服務。若此時點擊通知欄調出UI播放界面後(此時的服務是之前續命的服務,並沒有綁定任何Activity),再關閉通知欄,則會先stopSelf再發送一個廣播通知當前Activity進行重新綁定服務。

C. 具體實現:

進入界面後首先要做的就是掃描本地所有音樂文件:

String[] paths = new String[] {Environment.getExternalStoragePublicDirectory(

Environment.DIRECTORY_MUSIC).toString()};

// String[] paths = new

// String[]{Environment.getExternalStorageDirectory().toString()};

MediaScannerConnection.scanFile(c,paths, null, new OnScanCompletedListener() {

@Override

publicvoid onScanCompleted(String path, Uri uri) {

ObservableManager.getInstance().setData(Constants.DATA_CHANGE_DELETE_SONGS);

}

});

接著從媒體庫拿各個Fragment的數據,如單曲:

Stringwhere = MediaStore.Audio.Media.IS_MUSIC + "=1";

Cursor cursor = context.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, where,

null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER);

拿到Cursor後轉成你所需要的對象即可展示了:

Listinfos = new ArrayList();

if (cursor == null) {

return infos;

}

while (cursor.moveToNext()) {

MusicInfo info = new MusicInfo();

// 歌曲ID:MediaStore.Audio.Media._ID

long id;

if (type == Constants.TYPE_PLAYLIST) {

id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists.Members.AUDIO_ID));

} else {

id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID));

}

String title, artist;

// 歌曲文件的路徑:MediaStore.Audio.Media.DATA

String url = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA));

String name = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME));

// 歌曲的名稱:MediaStore.Audio.Media.TITLE

title = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE));

// 歌曲的歌手名: MediaStore.Audio.Media.ARTIST

artist = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST));

String album = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM));

// 歌曲的總播放時長:MediaStore.Audio.Media.DURATION

long duration = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION));

// 歌曲文件的大小:MediaStore.Audio.Media.SIZE

long size = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE));

long artistsId = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST_ID));

long albumId = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID));

String displayName = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME));

info.setName(title);

info.setId(id);

info.setPath(url);

info.setArtists(artist);

info.setAlbum(album);

info.setArtistsId(artistsId);

info.setAlbumId(albumId);

info.setSize(size);

info.setDuration(duration);

info.setDisplayName(displayName);

String tag = PingYinUtil.chineneToSpell(title);

if (tag.length() < 1) {

cursor.close();

return infos;

}

char c = tag.toUpperCase().charAt(0);

if (!('A' <= c && c <= 'Z')) {

tag = "#";

}

info.setFirstLetter(String.valueOf(tag.charAt(0)).toUpperCase());

info.setPingYinName(tag);

infos.add(info);

}

cursor.close();

其他的就不一一列出來了。

數據UI都有了,接下來就要開始創建服務准備播放了,先在服務裡封裝好一個播放器並與AIDL關聯好:

private class MultiPlayer {

private MediaPlayer mCurrentMediaPlayer = new MediaPlayer();

private MediaPlayer mNextMediaPlayer;

private Handler mHandler;

private boolean mIsInitialized = false;

public MultiPlayer() {

mCurrentMediaPlayer.setWakeMode(MediaPlaybackService.this, PowerManager.PARTIAL_WAKE_LOCK);

}

public void setDataSource(String path) {

mIsInitialized = setDataSourceImpl(mCurrentMediaPlayer, path);

Log.i(TAG, "setDataSource() mIsInitialized :" + mIsInitialized);

if (mIsInitialized) {

setNextDataSource(null);

}

}

private boolean setDataSourceImpl(MediaPlayer player, String path) {

try {

Log.d(TAG, "setDataSourceImpl() player : " + player + ",path : " + path + ",cursor: " + mCursor);

if (mCursor == null) {

return false;

}

player.reset();

if (path.startsWith("content://")) {

player.setDataSource(MediaPlaybackService.this, Uri.parse(path));

} else {

player.setDataSource(path);

}

player.setAudioStreamType(AudioManager.STREAM_MUSIC);

player.prepare();

Log.i(TAG, "setDataSourceImpl() afterprepare()");

} catch (IOException ex) {

// TODO: notify the user why the file couldn't beopened

return false;

} catch (IllegalArgumentException ex) {

// TODO: notify the user why the file couldn't beopened

return false;

}

player.setOnCompletionListener(listener);

player.setOnErrorListener(errorListener);

player.setOnPreparedListener(new OnPreparedListener() {

@Override

public void onPrepared(MediaPlayer mp) {

// TODO Auto-generated method stub

// mp.start();

}

});

return true;

}

public void setNextDataSource(String path) {

Log.d(TAG, "setNextDataSource() enter path :" + path + ",mNextMediaPlayer : " + mNextMediaPlayer);

if (mNextMediaPlayer != null) {

mNextMediaPlayer.release();

mNextMediaPlayer = null;

mCurrentMediaPlayer.setNextMediaPlayer(null);

}

if (path == null) {

return;

}

mNextMediaPlayer = new MediaPlayer();

mNextMediaPlayer.setWakeMode(MediaPlaybackService.this, PowerManager.PARTIAL_WAKE_LOCK);

mNextMediaPlayer.setAudioSessionId(getAudioSessionId());

if (setDataSourceImpl(mNextMediaPlayer, path)) {

mCurrentMediaPlayer.setNextMediaPlayer(mNextMediaPlayer);

} else {

// failed to open next, we'll transitionthe old fashioned way,

// which will skip over the faulty file

mNextMediaPlayer.release();

mNextMediaPlayer = null;

}

}

public boolean isInitialized() {

return mIsInitialized;

}

public void start() {

MusicUtils.debugLog(new Exception("MultiPlayer.start called"));

mCurrentMediaPlayer.start();

}

public void stop() {

mCurrentMediaPlayer.reset();

mIsInitialized = false;

}

/**

*You CANNOT use this player anymore after calling release()

*/

public void release() {

stop();

mCurrentMediaPlayer.release();

}

public void pause() {

mCurrentMediaPlayer.pause();

}

public void setHandler(Handler handler) {

mHandler = handler;

}

MediaPlayer.OnCompletionListener listener = new MediaPlayer.OnCompletionListener() {

public void onCompletion(MediaPlayer mp) {

Log.d(TAG, "onCompletion : " + (mp == mCurrentMediaPlayer && mNextMediaPlayer != null)

+ ",mRepeatMode : " + mRepeatMode);

if (mRepeatMode != REPEAT_CURRENT && !mCurrentDataIsremove) {

// mCurrentMediaPlayer.release();

setNextTrack();

mCurrentMediaPlayer = mNextMediaPlayer;

mNextMediaPlayer = null;

mHandler.sendEmptyMessage(TRACK_WENT_TO_NEXT);

} else {

mWakeLock.acquire(30000);

mHandler.sendEmptyMessage(TRACK_ENDED);

mHandler.sendEmptyMessage(RELEASE_WAKELOCK);

}

}

};

MediaPlayer.OnErrorListener errorListener = new MediaPlayer.OnErrorListener() {

public boolean onError(MediaPlayer mp, int what, int extra) {

Log.e(TAG, "MediaPlayer.onError() what: " + what + "," + extra);

switch (what) {

case MediaPlayer.MEDIA_ERROR_SERVER_DIED:

return true;

case -38:

if (mPlayList != null && mPlayListLen <= 1) {

MediaPlaybackService.this.stop(true);

stopForeground(true);

}

break;

default:

playSongFail(mp);

break;

}

return true;

}

};

播放之前要先准備好待播放文件:

public boolean open(String path) {

Log.d(TAG, "open() path : " + path);

synchronized (this) {

if (path == null) {

return false;

}

// if mCursor is null, try to associatepath with a database cursor

if (mCursor == null) {

ContentResolver resolver = getContentResolver();

Uri uri;

String where;

String selectionArgs[];

if (path.startsWith("content://media/")) {

uri = Uri.parse(path);

where = null;

selectionArgs = null;

} else {

uri = MediaStore.Audio.Media.getContentUriForPath(path);

where = MediaStore.Audio.Media.DATA + "=?";

selectionArgs = new String[] { path };

}

try {

mCursor = resolver.query(uri, mCursorCols, where, selectionArgs, null);

if (mCursor != null) {

if (mCursor.getCount() == 0) {

mCursor.close();

mCursor = null;

} else {

mCursor.moveToNext();

ensurePlayListCapacity(1);

mPlayListLen = 1;

mPlayList[0] = mCursor.getLong(IDCOLIDX);

mPlayPos = 0;

}

}

} catch (UnsupportedOperationException ex) {

Log.d(TAG, "UnsupportedOperationException");

}

}

mFileToPlay = path;

mPlayer.setDataSource(mFileToPlay);

if (mPlayer.isInitialized()) {

mOpenFailedCounter = 0;

return true;

}

stop(true);

return false;

}

}

到這裡差不多就可以調用mPlayer.start()播放音樂了。

接著再說下Service的綁定跟解綁的事,服務若直接跟applicationContext綁定,你會發現你的服務就算執行onUnbind,但它還是沒死,所以,最好還是選擇跟Activity綁定:

@Override

protected void onResume() {

// TODOAuto-generated method stub

super.onResume();

if (MusicApplication.getmToken() == null && !(BaseActivity.this instanceof MusicMainActivity)) {

MusicApplication.setmToken(MusicUtils.bindToService(this, mServiceConnection));

} else {

new Handler().postDelayed(new Runnable() {

public void run() {

if (MusicApplication.getmToken() == null && !(BaseActivity.this instanceof MusicMainActivity)) {

MusicApplication.setmToken(MusicUtils.bindToService(BaseActivity.this, mServiceConnection));

}

}

}, 400);

}

}

@Override

protected void onStop() {

// TODOAuto-generated method stub

super.onStop();

if (!MusicApplication.isNotifacationExist()&& MusicUtils.sService != null

&& MusicUtils.isApplicationBroughtToBackground(getApplicationContext())){

MusicUtils.unbindFromService(MusicApplication.getmToken());

}

}

但這樣做的唯一缺點就是,只要綁定的Activity結束掉,服務就自動執行了onUnbind。所以只要一播放音樂你可以先彈出通知欄:

private void updateNotification(Context context, Bitmap bitmap) {

Log.d(TAG, "updateNotification");

MusicApplication.setNotifacationExist(true);

RemoteViews views = new RemoteViews(getPackageName(), R.layout.messagecenter_contralbar);

String trackinfo = getTrackName();

String artist = getArtistName();

if (artist == null || artist.equals(MediaStore.UNKNOWN_STRING)) {

artist = getString(R.string.unknown_artist_name);

}

trackinfo += " -" + artist;

views.setTextViewText(R.id.txt_trackinfo, trackinfo);

Intent intent;

PendingIntent pIntent;

intent = new Intent("com.android.music.PLAYBACK_VIEWER");

intent.setPackage(getPackageName());

pIntent = PendingIntent.getActivity(context, 0, intent, 0);

views.setOnClickPendingIntent(R.id.rl_newstatus, pIntent);

intent = new Intent(PREVIOUS_ACTION);

intent.setClass(context, MediaPlaybackService.class);

pIntent = PendingIntent.getService(context, 0, intent, 0);

views.setOnClickPendingIntent(R.id.btn_prev, pIntent);

intent = new Intent(NOTIFICATION_PAUSE_PLAY_ACTION);

intent.setClass(context, MediaPlaybackService.class);

pIntent = PendingIntent.getService(context, 0, intent, 0);

views.setOnClickPendingIntent(R.id.btn_pause, pIntent);

if (isPlaying()) {

views.setImageViewResource(R.id.btn_pause, R.drawable.music_message_stop);

} else {

views.setImageViewResource(R.id.btn_pause, R.drawable.music_message_play);

}

intent = new Intent(NEXT_ACTION);

intent.setClass(context, MediaPlaybackService.class);

pIntent = PendingIntent.getService(context, 0, intent, 0);

views.setOnClickPendingIntent(R.id.btn_next, pIntent);

intent = new Intent(NOTIFICATION_STOP_ACTION);

intent.setClass(context, MediaPlaybackService.class);

pIntent = PendingIntent.getService(context, 0, intent, 0);

views.setOnClickPendingIntent(R.id.btn_close, pIntent);

if (bitmap != null) {

views.setImageViewBitmap(R.id.iv_cover, bitmap);

}

Notification status = new Notification();

status.contentView = views;

status.flags |= Notification.FLAG_ONGOING_EVENT;

status.icon = R.drawable.icon_notify_musicplayer;

status.contentIntent = PendingIntent.getService(context, 0, intent, 0);

startForeground(PLAYBACKSERVICE_STATUS, status);

}

並且調用

startService(new Intent(this, MediaPlaybackService.class));為綁定的服務續命,這樣就算Activity掛掉後,服務照樣能繼續播放音樂,如果你想結束掉續命的服務,就只要調用MediaPlaybackService.this.stopSelf();就好了。這樣服務就不管怎麼樣都會執行onDestroy來釋放資源了。

D.總結

音樂的核心就在於服務,最近遇到的問題基本上都與服務有關。接手之前,服務是永遠存在的,這是不符合谷歌規范的,長時間空閒的服務將使所在進程一直處在B Services(oom_adj=8),進程不容易被殺掉、內存較難及時釋放。所以嘗試著改動。之前的是每個界面都去綁定,為了簡化邏輯及代碼,對整個服務進行了統一(如上圖)。該綁定的時候綁定,該解綁的時候解綁,改釋放的資源及時釋放。由於服務貫穿整個音樂,所以每次改動後必須每個邏輯都要測試一遍,否則就會出現很多BUG了。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved