1. 程式人生 > >【android】音樂播放器之設計思路

【android】音樂播放器之設計思路

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

        功能介紹如下:    

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

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

 該播放器詳細設計參考下面博文:  

       下面是最終結果展示:



               圖一主介面以及本地音樂列表



圖二: 在本地列表中長按選單鍵可以彈出功能選擇鍵,短按listview中的歌曲列表跳轉到播放列表,長按listview中的個人去items可以歌曲的刪除。



       上面兩幅圖都關於網路列表:按listview中的歌曲items可以實現下載功能


       播放介面了~!~我還是比較喜歡這個播放介面的,仿QQ音樂播放介面風格。通過左右滑動可以實現播放歌曲當前的的圖片到播放歌曲歌詞顯示的切換


     上面這兩幅圖分別為下載時和播放歌曲時候通知欄的顯示,其餘的小功能就不一一展示。如果感興趣可以下載程式碼編也好改也好隨你便吧,~!~hahahha!!!

            這邊文章主要講講大致的設計思路~~~~~~。

        還是先談談service服務吧,音樂播放器用到了兩個service服務:PlayService和DownLoadService。PlayService主要負責的是播放音樂的功能,當然,在啟動的時候到sdcard中讀取資料、動態跟新通知欄以及歌詞等這些也都是在PlayService中完成的;而DownLoadService主要結合DownLoad類實現歌曲下載的功能。也是必須要考慮到服務必須要在後臺長期執行的緣故,因此,這邊的處理是在應用啟動時候,通過application類呼叫它的startService方法啟動service。詳細情況可以參考我的博文

【android】音樂播放器之service服務設計~!~哈哈哈哈哈哈,application類已經在前面提到的博文中已經貼出來這邊就不再貼出相關的程式碼。

        有了application類後可以著手去設計Activity類,很自然想到實現一個父類,把子類中複用的程式碼都放在BaseActivity。然後在子類中覆蓋或者重寫父類中的相關程式碼 ,程式碼如下:

public abstract class BaseActivity extends FragmentActivity {
	protected PlayService mPlayService;
	protected DownloadService mDownloadService;
	
	private final String TAG = BaseActivity.class.getSimpleName();
	
	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;
			
		}
		
	};
	
	private ServiceConnection mDownloadServiceConnection = new ServiceConnection() {
		@Override
		public void onServiceDisconnected(ComponentName name) {
			L.l(TAG, "download--->onServiceDisconnected");
			mDownloadService = null;
		}
		
		@Override
		public void onServiceConnected(ComponentName name, IBinder service) {
			mDownloadService = ((DownloadService.DownloadBinder) service).getService();
		}
	};
	
	
	/*
	 * 音樂播放服務回撥介面的實現類
	 */
	
	private PlayService.OnMusicEventListener mMusicEventListener = 
			new PlayService.OnMusicEventListener() {
		@Override
		public void onPublish(int progress) {
			BaseActivity.this.onPublish(progress);
		}

		@Override
		public void onChange(int position) {
			BaseActivity.this.onChange(position);
		}
	};
	
	/**
	 * Fragment的view載入完成後回撥
	 */
	public void allowBindService() {
		bindService(new Intent(this, PlayService.class), mPlayServiceConnection,
				Context.BIND_AUTO_CREATE);
	}
	
	/**
	 * fragment的view消失後回撥
	 */
	public void allowUnbindService() {
		unbindService(mPlayServiceConnection);
	}
	
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		bindService(new Intent(this, DownloadService.class), mDownloadServiceConnection,Context.BIND_AUTO_CREATE);
	}
	
	@Override
	protected void onDestroy() {
		unbindService(mDownloadServiceConnection);
		super.onDestroy();
	}
	
	
	public DownloadService getDownloadService() {
		return mDownloadService;
	}
	
	/**
	 * 更新進度
	 * @param progress 進度
	 */
	public abstract void onPublish(int progress);
	/**
	 * 切換歌曲
	 * @param position 歌曲在list中的位置
	 */
	public abstract void onChange(int position);
}
        有了BaseActivity,可以設計子類部分。在一個準備類啟動載入一個佈局全屏顯示一張啟動封面延時兩秒鐘進入主介面。主介面設計仿多米音樂風格,在MainActivity中新增5個Fragment(詳細的UI設計見: 【android】音樂播放器之UI設計的點點滴滴),同時監聽按鈕實現Fragment的切換:
@Override
	public void onClick(View v)
	{
		resetImgs();
		switch (v.getId())
		{
		case R.id.id_tab_user:
			setSelect(TAB_USER);
			break;
		case R.id.id_tab_cd:
			setSelect(TAB_CD);
			break;
		case R.id.id_tab_search:
			setSelect(TAB_SEARCH);
			break;
		case R.id.id_tab_compass:
			setSelect(TAB_COMPASS);
			break;
		case R.id.id_tab_topjump:
			startActivity(new Intent(this, PlayActivity.class));
			break;
		case R.id.tv_pop_exit:
			stopService(new Intent(this, PlayService.class));
			//stopService(new Intent(this, DownloadService.class));
		case R.id.tv_pop_shutdown:
			finish();
		case R.id.tv_pop_cancel:
			if(mPopupWindow != null && mPopupWindow.isShowing()) mPopupWindow.dismiss();
			onPopupWindowDismiss();
	 		break;
		default:
			break;
		}
	}
       其中,1、需要注意的是本地音樂列表LocalFragment的監聽事件是通過在UserFragment中監聽獲得並呼叫主活動的的select方法跳轉到本地音樂列表。當然,此時本地音樂列表已經初始化過~~~初始化過程主要在PlayService中啟動過程完成(這個在上面提到的相關博文有詳細的說明);2、在LocalFragment啟動過程通過bingservice()繫結服務,這樣就可以實現當點選本地音樂列表時候播放按鈕同時跳轉到PlayActivity播放介面。程式碼如下:
@Override
	public void onStart() {
		super.onStart();
		mActivity.allowBindService();
	}
	
	@Override
	public void onStop() {
		super.onStop();
		mActivity.allowUnbindService();
	}
@Override
	public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
		Intent intent = new Intent(mActivity, PlayActivity.class);
		intent.putExtra("pos", position);
		startActivity(intent);
		play(position);
	}
private void play(int position) {
		int pos = mActivity.getPlayService().play(position);
		onPlay(pos);
	}
        同時,當長按本地音樂播放列表可以實現刪除本地音樂的功能,通過listView的setOnItemLongClickListener方法監聽,獲取事件時呼叫MusicUtils的remove方法刪除本地音樂,並啟動掃描sdcard通過廣播通知列表變化更新本地列表:
private OnItemLongClickListener mItemLongClickListener = 
			new OnItemLongClickListener() {
		@Override
		public boolean onItemLongClick(AdapterView<?> parent, View view,
				int position, long id) {
			final int pos = position;

			AlertDialog.Builder builder = new AlertDialog.Builder(mActivity);
			builder.setTitle("刪除該條目");
			builder.setMessage("確認要刪除該條目嗎?");
			builder.setPositiveButton("刪除",
				new DialogInterface.OnClickListener() {
					public void onClick(DialogInterface dialog, int which) {
						Music music = MusicUtils.sMusicList.remove(pos);
						mMusicListAdapter.notifyDataSetChanged();
						if (new File(music.getUri()).delete()) {
							scanSDCard();
						}
					}
				});
			builder.setNegativeButton("取消", null);
			builder.create().show();
			return true;
		}
	};
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(mActivity, 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()));
			mActivity.sendBroadcast(intent);
		}
	}
        跳轉到PlayActivity後實現和在LocalFragment實現十分相同,繫結Playservice服務,初始化監聽事件,獲取事件呼叫服務中的方法播放音樂,具體的Ui設計部分見 【android】音樂播放器之UI設計的點點滴滴。另外,注意一點是:在佈局檔案中介個監聽聽按鈕設定了onclick屬性:
<ImageButton
                android:id="@+id/ib_play_pre"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@android:color/transparent"
                android:contentDescription="@string/app_name"
                android:onClick="pre"
                android:src="@drawable/player_btn_pre_normal" />

            <ImageButton
                android:id="@+id/ib_play_start"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="20dp"
                android:layout_marginRight="20dp"
                android:background="@android:color/transparent"
                android:contentDescription="@string/app_name"
                android:onClick="play"
                android:src="@drawable/player_btn_play_normal" />

            <ImageButton
                android:id="@+id/ib_play_next"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@android:color/transparent"
                android:contentDescription="@string/app_name"
                android:onClick="next"
                android:src="@drawable/player_btn_next_normal" />
       當在MainActivity中捕獲到網路按鈕時,跳轉到SearchFragment網路列表,因為在BaseActivity的Oncreate方法直接繫結DownLoadService,所以在該Fragment中就沒必要再去繫結服務。在SearchFragment建立後呼叫setUserVisibleHint方法實現Fragment實現懶載入,結合SongsRecommendation類解析百度音樂中頁面獲取音樂資料,顯示在SearchFragment佈局中,網頁解析獲取歌曲列表主要通過JSOUP解析技術實現(可以參考:jsoup下載地址 以及jsoup中文開發指南)。程式碼如下:
@Override
	public void onResume() {
		super.onResume();
		if(getUserVisibleHint()){
			Boolean isVisibleToUser = getUserVisibleHint();
			setUserVisibleHint(isVisibleToUser);
		}
	}
	
	/**
	 * 該方法實現的功能是: 當該Fragment不可見時,isVisibleToUser=false
	 * 當該Fragment可見時,isVisibleToUser=true
	 * 該方法由系統呼叫,重寫該方法實現使用者可見當前Fragment時再進行資料的載入
	 */
	@Override
	public void setUserVisibleHint(boolean isVisibleToUser) {
		super.setUserVisibleHint(isVisibleToUser);
		// 當Fragment可見且是第一次載入時
		if (isVisibleToUser && isFirstShown) {
			mSearchProgressBar.setVisibility(View.VISIBLE);
			mSearchResultListView.setVisibility(View.GONE);
			SongsRecommendation
				.getInstance()
				.setListener(
					new SongsRecommendation.OnRecommendationListener() {
						@Override
						public void onRecommend(
							ArrayList<SearchResult> results) {
							if (results == null || results.isEmpty())
								return;
							mSearchProgressBar.setVisibility(View.GONE);
							mSearchResultListView
									.setVisibility(View.VISIBLE);
							mResultData.clear();
							mResultData.addAll(results);
							mSearchResultAdapter.notifyDataSetChanged();
						}
					}).get();
			isFirstShown = false;
		}
	}
/**
	 * 真正執行網頁解析的方法
	 * 執行緒池中開啟新的執行緒執行解析,解析完成之後傳送訊息
	 * 將結果傳遞到主執行緒中
	 */
	public void get() {
		mThreadPool.execute(new Runnable() {
			@Override
			public void run() {
				ArrayList<SearchResult> result = getMusicList();
				if (result == null) {
					mHandler.sendEmptyMessage(Constants.FAILED);
					return;
				}
				mHandler.obtainMessage(Constants.SUCCESS, result)
						.sendToTarget();
			}
		});
	}

	private ArrayList<SearchResult> getMusicList() {
		try {
			/**
			 * 一下方法呼叫請參考官網
			 * 說明:timeout設定請求時間,不宜過短。
			 * 時間過短導致異常,無法獲取。
			 */
			Document doc = Jsoup
					.connect(URL)
					.userAgent(
							"Mozilla/5.0 (Windows NT 6.1; Win64; x64)" +
							" AppleWebKit/537.36"
									+ " (KHTML, like Gecko)" +
									" Chrome/42.0.2311.22 Safari/537.36")
					.timeout(60 * 1000).get();
			//select為選擇器,請參考官網說明
			Elements songTitles = doc.select("span.song-title");
			Elements artists = doc.select("span.author_list");
			ArrayList<SearchResult> searchResults = new ArrayList<SearchResult>();

			for (int i = 0; i < songTitles.size(); i++) {
				SearchResult searchResult = new SearchResult();
				Elements urls = songTitles.get(i).getElementsByTag("a");
				searchResult.setUrl(urls.get(0).attr("href"));
				searchResult.setMusicName(urls.get(0).text());

				Elements artistElements = artists.get(i).getElementsByTag("a");
				searchResult.setArtist(artistElements.get(0).text());
				searchResult.setAlbum("最新推薦");
				searchResults.add(searchResult);
			}
			return searchResults;
		} catch (IOException e) {
			e.printStackTrace();
		}

		return null;
	}
        具體監聽點選事件開始下載功能的實現見其他幾篇小博文。這邊就不一一介紹了,如果感興趣還是建議看下原始碼。

理