Music app框架设计及总结
来源:互联网 发布:10月份经济数据2017 编辑:程序博客网 时间:2024/05/16 13:56
作者 Chaoqian.chen
总体上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后转成你所需要的对象即可展示了:
List<MusicInfo>infos = new ArrayList<MusicInfo>();
if (cursor ==null) {
returninfos;
}
while (cursor.moveToNext()) {
MusicInfo info = new MusicInfo();
// 歌曲ID:MediaStore.Audio.Media._ID
longid;
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
longduration = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION));
// 歌曲文件的大小:MediaStore.Audio.Media.SIZE
longsize = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE));
longartistsId = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST_ID));
longalbumId = 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();
returninfos;
}
charc = 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 MediaPlayermCurrentMediaPlayer = new MediaPlayer();
private MediaPlayermNextMediaPlayer;
private HandlermHandler;
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) {
returnfalse;
}
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 (IOExceptionex) {
// TODO: notify the user why the file couldn't beopened
return false;
} catch (IllegalArgumentExceptionex) {
// 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() {
returnmIsInitialized;
}
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:
returntrue;
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 (UnsupportedOperationExceptionex) {
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.thisinstanceof MusicMainActivity)) {
MusicApplication.setmToken(MusicUtils.bindToService(this,mServiceConnection));
} else {
new Handler().postDelayed(new Runnable() {
public void run() {
if (MusicApplication.getmToken() ==null && !(BaseActivity.thisinstanceof 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(Contextcontext, 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了。
- Music app框架设计及总结
- APP 基本框架设计
- Share Your Music - HTML5 Music Web App
- app后台设计总结
- 王学岗app设计框架:mvc框架
- 【助手APP】简介及框架
- 设计模式 && android 框架 && app 设计
- 框架页设计总结.
- Android框架设计总结
- 去哪儿APP设计总结
- app 原形设计常用工具总结
- APP 自动化框架设计思路分享
- Android BaseActivity App框架设计BaseActivity封装
- App框架设计与重构
- Android App框架设计 基类BaseActivity
- 高效App框架设计与重构
- Music
- music
- 嵌入式技能点
- spring mvc 下载文件,ie不支持解决
- Java substring lastIndex
- pyqt实现界面化编程
- 符号表
- Music app框架设计及总结
- spring容器归纳(二)
- Linux命令 - su
- datetimepicker用法
- tjut 4310
- 猜数字游戏
- Python 程序扩展名(py, pyc, pyw, pyo, pyd)及发布程序时的选择
- 不可以! 南阳理工ACM 题目1071
- 《机器学习》周志华学习笔记——第一章 绪论