1. 程式人生 > >【android】音樂播放器之service服務設計

【android】音樂播放器之service服務設計

       學習Android有一個多月,看完了《第一行程式碼》以及mars老師的第一期視訊通過音樂播放器小專案加深對知識點的理解。從本文開始,將詳細的介紹簡單仿多米音樂播放器的實現,以及網路解析資料獲取百度音樂最新排行音樂以及下載功能。

        功能介紹如下:    

        1、獲取本地歌曲列表,實現歌曲播放功能。 
        2、利用jsoup解析網頁資料,從網路獲取歌曲列表,同時實現歌曲和歌詞下載到手機本地的功能。 
        3、通知欄提醒,實現仿QQ音樂播放器的通知欄功能. 

      涉及的技術有: 
       1、jsoup解析網路網頁,從而獲取需要的資料 
       2、android中訪問網路,獲取檔案到本地的網路請求技術,以及下載檔案到本地實現斷點下載 
       3、執行緒池 
       4、圖片快取 
       5、service一直在後臺執行 
       6、Activity與Fragment間的切換以及通訊 
       7、notification通知欄設計 
       8、自定義廣播 
       9、android系統檔案管

       這篇文章主要談談音樂播放器service設計的方方面面。主要用到了兩個service服務:一個用於播放音樂的PlayService;另一個用於下載歌曲的DownLoadService。使用了service的兩種啟動方式:startservice()啟動方式和bindservice()啟動方式(想要深入理解這兩種啟動方式可以參考博文:深入理解Android的startservice和bindservice)。

       我們可以在application類中使用startservice()啟動服務,startservice()方式啟動不會隨著content的銷而銷燬服務,除非呼叫stopservice()停止service。這樣做的好處是儘可能的提高了服務的優先順序可以使service可以在後臺一直執行。根據上面的說明很容易實現application類:

public class App extends Application{
	public static Context sContext;
	public static int sScreenWidth;
	public static int sScreenHeight;
	
	@Override
	public void onCreate() {
		super.onCreate();
		sContext = getApplicationContext();
		
		startService(new Intent(this, PlayService.class));
		startService(new Intent(this, DownloadService.class));
		
		WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
		DisplayMetrics dm = new DisplayMetrics();
		wm.getDefaultDisplay().getMetrics(dm);
		sScreenWidth = dm.widthPixels;
		sScreenHeight = dm.heightPixels;
	}
}

      在application類中啟動類兩個service服務:Playservice和DownloadService。程式碼量比比較大,小編這邊就不逐一進行分析(~!~)概括性的講講,見諒見諒!!!先來看看PlayService啟動後的初始化工作吧:
public void onCreate() {
		super.onCreate();

		MusicUtils.initMusicList();
		mPlayingPosition = (Integer) SpUtils.get(this, Constants.PLAY_POS, 0);
		mPlayer = new MediaPlayer();
		mPlayer.setOnCompletionListener(this);
		
		// 開始更新進度的執行緒
		mProgressUpdatedListener.execute(mPublishProgressRunnable);
		
		if(MusicUtils.sMusicList.size() != 0)
		{
			startNotification();
			readyNotification = true;
		}
	}
       程式碼一目瞭然,完成的主要工作如下幾方面:

       (1)呼叫MusicUtil類的InitMusic()方法通過指定位置到sdcard中讀取.mp3檔案以及.lrc檔案,並將這些資料載入到ArrayList陣列填充本地音樂列表。

       (2)建立MediaPlayer以及設定監聽。

       (3)呼叫startNotification()初始化通知欄,並且在startNotification()方法中註冊廣播器監聽通知欄的點選事件。

       (4)通過回撥介面實現Service與Activity介面歌曲播放進度等一系列功能的更新。

      就先貼出通知欄這塊比較重要點的程式碼吧,PlayService顯示的功能帶後文bingService()啟動後還會進一步分析:

private void startNotification() {
		/**
		 * 該方法雖然被拋棄過時,但是通用!
		 */
		PendingIntent pendingIntent = PendingIntent
				.getActivity(PlayService.this,
				0, new Intent(PlayService.this, PlayActivity.class), 0);
		remoteViews = new RemoteViews(getPackageName(),
				R.layout.play_notification);
		notification = new Notification(R.drawable.icon,
				"歌曲正在播放", System.currentTimeMillis());
		notification.contentIntent = pendingIntent;
		notification.contentView = remoteViews;
		//標記位,設定通知欄一直存在
		notification.flags =Notification.FLAG_ONGOING_EVENT;
		
		Intent intent = new Intent(PlayService.class.getSimpleName());
		intent.putExtra("BUTTON_NOTI", 1);
		PendingIntent preIntent = PendingIntent.getBroadcast(
				PlayService.this,
				1, intent, PendingIntent.FLAG_UPDATE_CURRENT);
		remoteViews.setOnClickPendingIntent(
				R.id.music_play_pre, preIntent);
		
		intent.putExtra("BUTTON_NOTI", 2);
		PendingIntent pauseIntent = PendingIntent.getBroadcast(
				PlayService.this,
				2, intent, PendingIntent.FLAG_UPDATE_CURRENT);
		remoteViews.setOnClickPendingIntent(
				R.id.music_play_pause, pauseIntent);
		
		intent.putExtra("BUTTON_NOTI", 3);
		PendingIntent nextIntent = PendingIntent.getBroadcast
				(PlayService.this,
				3, intent, PendingIntent.FLAG_UPDATE_CURRENT);
		remoteViews.setOnClickPendingIntent(
				R.id.music_play_next, nextIntent);
		
		intent.putExtra("BUTTON_NOTI", 4);
		PendingIntent exit = PendingIntent.getBroadcast(PlayService.this,
				4, intent, PendingIntent.FLAG_UPDATE_CURRENT);
		remoteViews.setOnClickPendingIntent(
				R.id.music_play_notifi_exit, exit);
		
		notificationManager = (NotificationManager)
				getSystemService(NOTIFICATION_SERVICE);
		setRemoteViews();
		
		/**
		 * 註冊廣播接收者
		 * 功能:
		 * 監聽通知欄按鈕點選事件 
		 */
		IntentFilter filter = new IntentFilter(
				PlayService.class.getSimpleName());
		MyBroadCastReceiver receiver = new MyBroadCastReceiver();
		registerReceiver(receiver, filter);
	}
public void setRemoteViews(){
		L.l(TAG, "進入——》setRemoteViews()");
		remoteViews.setTextViewText(R.id.music_name,
				MusicUtils.sMusicList.get(
						getPlayingPosition()).getTitle());
		remoteViews.setTextViewText(R.id.music_author,
				MusicUtils.sMusicList.get(
						getPlayingPosition()).getArtist());
		Bitmap icon = MusicIconLoader.getInstance().load(
				MusicUtils.sMusicList.get(
						getPlayingPosition()).getImage());
		remoteViews.setImageViewBitmap(R.id.music_icon,icon == null
				? ImageTools.scaleBitmap(R.drawable.icon)
						: ImageTools
				.scaleBitmap(icon));
		if (isPlaying()) {
			remoteViews.setImageViewResource(R.id.music_play_pause,
					R.drawable.btn_notification_player_stop_normal);
		}else {
			remoteViews.setImageViewResource(R.id.music_play_pause,
					R.drawable.btn_notification_player_play_normal);
		}
		//通知欄更新
		notificationManager.notify(5, notification);
	}

	private class MyBroadCastReceiver extends BroadcastReceiver{
		@Override
		public void onReceive(Context context, Intent intent) {
			if (intent.getAction().equals(
					PlayService.class.getSimpleName())) {
				L.l(TAG, "MyBroadCastReceiver類——》onReceive()");
				L.l(TAG, "button_noti-->"
				+intent.getIntExtra("BUTTON_NOTI", 0));
				switch (intent.getIntExtra("BUTTON_NOTI", 0)) {
				case 1:
					pre();
					break;
				case 2:
					if (isPlaying()) {
						pause(); // 暫停
					} else {
						resume(); // 播放
					}
					break;
				case 3:
					next();
					break;
				case 4:
					if (isPlaying()) {
						pause();
					}
					//取消通知欄
					notificationManager.cancel(5);
					break;
				default:
					break;
				}
			}
			if (mListener != null) {
				mListener.onChange(getPlayingPosition());
			}
		}
	}
      為了讀者思路的連貫性(~!~一方面也因為DownLoadService初始化確實ye沒做啥工作。。。),我就繼續將PlayService的bindService()方式也一併分析了再說DownLoadService吧,見諒見諒!!!!~·~!

      本地列表LocalFragment在建立時呼叫了onstart()方法呼叫的活動中的bindservice()方法實現繫結服務,同理在銷燬的時候呼叫onstop()方法呼叫活動中的unbindservice()方法銷燬服務,這種啟動方式會隨著content的銷燬而銷燬服務:

@Override
	public void onStart() {
		super.onStart();
		mActivity.allowBindService();
	}
	
	@Override
	public void onStop() {
		super.onStop();
		mActivity.allowUnbindService();
	}
/**
	 * Fragment的view載入完成後回撥
	 */
	public void allowBindService() {
		bindService(new Intent(this, PlayService.class), mPlayServiceConnection,
				Context.BIND_AUTO_CREATE);
	}
private ServiceConnection mPlayServiceConnection = new ServiceConnection() {

		@Override
		public void onServiceConnected(ComponentName arg0, IBinder service) {
			// TODO Auto-generated method stub
			mPlayService = ((PlayService.PlayBinder) service).getService();
			mPlayService.setOnMusicEventListener(mMusicEventListener);
			onChange(mPlayService.getPlayingPosition());
		}

		@Override
		public void onServiceDisconnected(ComponentName arg0) {
			mPlayService = null;
			
		}
		
	};
      同理,在PlayActivity也是通過這種方式實現繫結服務,在LocalFragment以及PlayActivity中通過各自介面的監聽事件呼叫service中的想過方法實現音樂的播放、暫停、下一首、上一首以及通過回撥函式實現播放進度的更新等一系列功能,部分程式碼程式碼如下:
	/**
	 * 播放
	 * @param position 音樂列表的位置
	 * @return 當前播放的位置
	 */
	public int play(int position) {
		if(position < 0) position = 0;
		if(position >= MusicUtils.sMusicList.size()) position = MusicUtils.sMusicList.size() - 1;
		
		try {
			mPlayer.reset();
			
			mPlayer.setDataSource(MusicUtils.sMusicList.get(position).getUri());
			mPlayer.prepare();
			
			start();
			if(mListener != null) mListener.onChange(position);
		} catch (Exception e) {
			e.printStackTrace();
		}
		
		mPlayingPosition = position;
		SpUtils.put(Constants.PLAY_POS, mPlayingPosition);
		if(!readyNotification){
			startNotification();
		}else{
			setRemoteViews();
		}
		return mPlayingPosition;
	}

	private void start() {
		mPlayer.start();
	}
	
	/**
	 * 是否正在播放
	 * @return
	 */
	public boolean isPlaying() {
		return mPlayer != null&& mPlayer.isPlaying(); 
	}
	
	/**
	 * 繼續播放
	 * @return 當前播放的位置 預設為0
	 */
	public int resume() {
		if(isPlaying()){
			return -1;
		}else if(mPlayingPosition <= 0 || mPlayingPosition >= MusicUtils.sMusicList.size()){
			mPlayingPosition = 0;
			play(mPlayingPosition);
			setRemoteViews();
			return mPlayingPosition;
		}else{
			mPlayer.start();
			setRemoteViews();
			return mPlayingPosition;
		}
	}
	
	/**
	 * 暫停播放
	 * @return 當前播放的位置
	 */
	public int pause() {
		if(!isPlaying()) return -1;
		mPlayer.pause();
		setRemoteViews();
		return mPlayingPosition;
	}
	
	/**
	 * 下一曲
	 * @return 當前播放的位置
	 */
	public int next() {
		if(mPlayingPosition >= MusicUtils.sMusicList.size() - 1) {
			return play(0);
		}
		setRemoteViews();
		return play(mPlayingPosition + 1);
	}
	
	/**
	 * 上一曲
	 * @return 當前播放的位置
	 */
	public int pre() {
		if(mPlayingPosition <= 0) {
			return play(MusicUtils.sMusicList.size() - 1);
		}
		setRemoteViews();
		return play(mPlayingPosition - 1);
	}
      馬上來看下DownLoadService程式碼分析:
public class DownloadService extends Service{
	private SparseArray<Download> mDownloads = new SparseArray<Download>();
	private RemoteViews mRemoteViews;
	
	public class DownloadBinder extends Binder {
		public DownloadService getService() {
			return DownloadService.this;
		}
	}
	
	@Override
	public IBinder onBind(Intent intent) {
		return new DownloadBinder();
	}
	
	@Override
	public void onCreate() {
		super.onCreate();
	}
	
	public void download(final int id, final String url, final String name) {
		L.l("download", url);
		Download d = new Download(id, url, MusicUtils.getMusicDir() + name);
		d.setOnDownloadListener(mDownloadListener).start(false);
		mDownloads.put(id, d);
	}
	
	private void refreshRemoteView() {
		@SuppressWarnings("deprecation")
		Notification notification = new Notification(
				android.R.drawable.stat_sys_download, "",
				System.currentTimeMillis());
		mRemoteViews = new RemoteViews(getPackageName(),
				R.layout.download_remote_layout);
	    notification.contentView = mRemoteViews;
	    
	    StringBuilder builder = new StringBuilder();
		for(int i=0,size=mDownloads.size();i<size;i++) {
			builder.append(mDownloads.get(mDownloads.keyAt(i))
					.getLocalFileName());
			builder.append("、");
		}
		
		mRemoteViews.setTextViewText(R.id.tv_download_name, 
				builder.substring(0, builder.lastIndexOf("、")));
	    
	    startForeground(R.drawable.icon, notification);
	}
	
	private void onDownloadComplete(int downloadId) {
		mDownloads.remove(downloadId);
		if(mDownloads.size() == 0) {
			stopForeground(true);
			return;
		}
		
		refreshRemoteView();
	}
	
	/**
	 * 傳送廣播,通知系統掃描指定的檔案
	 */
	private void scanSDCard() {
		
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
			// 判斷SDK版本是不是4.4或者高於4.4
			String[] paths = new String[]{
					Environment.getExternalStorageDirectory().toString()};
			MediaScannerConnection.scanFile(this, paths, null, null);
		} else {
			Intent intent = new Intent(Intent.ACTION_MEDIA_MOUNTED);
			intent.setClassName("com.android.providers.media",
					"com.android.providers.media.MediaScannerReceiver");
			intent.setData(Uri.parse("file://"+ MusicUtils.getMusicDir()));
			sendBroadcast(intent);
		}
	}
	
	private Download.OnDownloadListener mDownloadListener = 
			new Download.OnDownloadListener() {
		
		@Override
		public void onSuccess(int downloadId) {
			L.l("download", "success");
			Toast.makeText(DownloadService.this, 
					mDownloads.get(downloadId).getLocalFileName() + "下載完成",
					Toast.LENGTH_SHORT).show();
			onDownloadComplete(downloadId);
			scanSDCard();
		}
		
		@Override
		public void onStart(int downloadId, long fileSize) {
			L.l("download", "start");
			refreshRemoteView();
			Toast.makeText(DownloadService.this, "開始下載" + 
					mDownloads.get(downloadId).getLocalFileName(),
					Toast.LENGTH_SHORT).show();
		}
		
		@Override
		public void onPublish(int downloadId, long size) {
//			L.l("download", "publish" + size);
		}
		
		@Override
		public void onPause(int downloadId) {
			L.l("download", "pause");
		}
		
		@Override
		public void onGoon(int downloadId, long localSize) {
			L.l("download", "goon");
		}
		
		@Override
		public void onError(int downloadId) {
			L.l("download", "error");
			Toast.makeText(DownloadService.this, 
					mDownloads.get(downloadId).getLocalFileName() + "下載失敗",
					Toast.LENGTH_SHORT).show();
			onDownloadComplete(downloadId);
		}
		
		@Override
		public void onCancel(int downloadId) {
			L.l("download", "cancel");
			onDownloadComplete(downloadId);
		}
	};
}
      相信你看完PlayService程式碼後一定覺得DownLoadService相當的簡單,主要結合DownLoad類實現真正的下載音樂的功能,以及通過回掉介面傳遞資料更新ui的功能。就不多做分析了~·~!!!。

      另外,小編想說移植用模擬器也沒有觀察鎖屏後service服務是否會停止,不過,即使服務被銷燬姐姐辦法也是很簡單很簡單:只要在啟動服務的時候獲取電源瑣,在服務被登出的時候釋放電源瑣就應該可以,感興趣的親可以試試~!~.HAHAHA