說(shuō)明:本文的客戶端是開(kāi)發(fā)版本,電臺(tái)logo等圖標(biāo)做了虛化處理,以避免誤導(dǎo); 另外源碼涉及到豆瓣內(nèi)部api的地方也省略。
功能需求
下面是豆瓣電臺(tái)網(wǎng)絡(luò)版和客戶端(開(kāi)發(fā)版)的界面。
豆瓣電臺(tái)網(wǎng)絡(luò)版
下圖的豆瓣電臺(tái)客戶端和網(wǎng)絡(luò)版一樣,除了播放歌曲,顯示專輯 封面, 播放時(shí)間,歌手和歌的名稱,以及喜歡/不喜歡,垃圾桶,跳過(guò)的3個(gè)操作按鈕外,還涉及到登錄,更換用戶,暫停的操作。
設(shè)計(jì)需求
- 豆瓣電臺(tái)是網(wǎng)絡(luò)音樂(lè)服務(wù),客戶端需要有后臺(tái)播放的服務(wù),前臺(tái)的播放器以及任務(wù)條的通知;
- 推薦給用戶的歌曲列表是從豆瓣網(wǎng)站上獲取的,歌曲也是在線播放的,需要網(wǎng)絡(luò)數(shù)據(jù)的獲取和異常處理;
- 本地還要保存一些數(shù)據(jù),比如播放歷史和自動(dòng)登錄使用的信息;
- 網(wǎng)絡(luò)操作都是比較耗時(shí)的,所以操作需要做成異步的方式來(lái)通知更新UI;
- 針對(duì)手機(jī)應(yīng)用的功能:比如接電話自動(dòng)暫停,掛電話自動(dòng)繼續(xù)播放,橫豎屏轉(zhuǎn)換要加載不同的UI Layout;
- 利用OPhone平臺(tái)提供的一些特性來(lái)改進(jìn)用戶體驗(yàn),比如使用Toast提示,動(dòng)畫效果,重力感應(yīng),手勢(shì)等
架構(gòu)設(shè)計(jì)
針對(duì)上面的應(yīng)用本身的功能和設(shè)計(jì)的需求,主要的架構(gòu)就是前臺(tái)Player的Activity,主要負(fù)責(zé)界面顯示和用戶的交互; 加上后臺(tái)的 Service,負(fù)責(zé)歌曲的播放和向豆瓣服務(wù)器發(fā)送請(qǐng)求,是個(gè)C/S的結(jié)構(gòu)。
如下圖所示:
Player和Service的架構(gòu)
Player調(diào)用Service
使用OMS平臺(tái)提供的service機(jī)制,通過(guò)下面的aidl把接口定義好。比如用戶點(diǎn)跳過(guò)按鈕時(shí),Player就會(huì)調(diào)用Service的skip。
- interface IRadioService{
- void stop();
- void exit();
- void play();
- void like(boolean isLike);
- void skip();
- void hate();
- boolean isPlaying();
- Song getSongPlaying();
- void logout();
- Bitmap getSongPicture();
- int pos();
- int duration();
- }
這里值得注意的是service的啟動(dòng)方式:
- private static final Intent service_intent = new Intent(Consts.INTENT_RADIO_SERVICE);
- protected void onCreate(Bundle savedInstanceState){
- super.onCreate(savedInstanceState);
- startService(service_intent);
- }
在Player的onCreate方法里面使用startService,這樣在Player Activity退出時(shí),Service進(jìn)程不會(huì)被殺死。而使用bindService方式 啟動(dòng)的service會(huì)在所有的client unbind后結(jié)束。
然后要在onResume和onDestroy的時(shí)候分別進(jìn)行bindService和unbindService。
- protected void onResume(){
- super.onResume();
- bindService(service_intent, connection, Context.BIND_AUTO_CREATE);
- }
- protected void onDestroy(){
- super.onDestroy();
- unbindService(connection);
- }
另外,如果希望service返回你自定義的對(duì)象,你需要實(shí)現(xiàn)parcelable接口,比如上面的Song。
Service里面的Handler
因?yàn)榫W(wǎng)絡(luò)通訊都是比較耗時(shí)的。比如上面的skip,是要向豆瓣提交跳過(guò)的是哪首歌的信息,并獲取新的播放列表。實(shí)際上,為了比較好的用戶體驗(yàn),用戶點(diǎn)跳過(guò)按鈕時(shí),在該按鈕的onClick方法里面調(diào)用了Service的skip,而Service只是向Downloader發(fā)了一個(gè)消息,告訴執(zhí)行了skip操作就返回了。這樣按鈕就不會(huì)一直停在按下去的狀態(tài)。
- private Handler downloader = new Handler() {
- public void handleMessage(Message msg) {
- Bundle bundle = msg.getData();
- switch (msg.what){
- case Consts.MSG_PLAYLIST_REQUIRE:
- //下載播放列表
- requireList(...);
- //播放下一首
- playNext();
- break;
- case Consts.MSG_PICTURE_DOWNLOADING:
- //下載圖片
- pic = web.getImage(bundle.getString("pic_url"));
- //圖片下載完成,通知Player更新圖片
- Intent intent = new Intent(Consts.INTENT_UPDATE_SONG_PICTURE);
- sendBroadcast(intent);
- break;
- ...
- }
- }
- };
如上面代碼所示,在service里面的Downloader是一個(gè)Handler, Handler本身實(shí)現(xiàn)了一個(gè)消息隊(duì)列,在handleMessage函數(shù)里面來(lái)處理消息。這里的Handler是在service線程的,并沒(méi)有新起線程。因?yàn)檫@里用Handler最主要的目的是使Player的調(diào)用馬上返回,達(dá) 到異步的目的。上面的skip就是向Downloader發(fā)了一個(gè)消息,而Downloader收到這個(gè)消息后,就去下載新的列表。
這里的Downloader最早的設(shè)計(jì)是在一個(gè)Thread里的,但是那樣需要service的主線程也要有一個(gè)handler來(lái)處理Thread發(fā)給主線程 的消息,比較復(fù)雜。而且對(duì)于電臺(tái)本身來(lái)講,都是先下載播放列表,開(kāi)始播放后,才需要下載正在播的歌曲的封面圖片,所以不需要真正的并發(fā)下載,也就是說(shuō),同一時(shí)刻只有一個(gè)下載的任務(wù)在執(zhí)行就可以了。所以最后是使用現(xiàn)在的設(shè)計(jì),可以避免多創(chuàng)建一個(gè)線 程,成本更低。
Service通知Player
上面講了Player怎么調(diào)用Service的,但Service還需要通知Player。比如當(dāng)圖片下載完成時(shí), Service需要通知Player來(lái)拿圖片,因?yàn)?Service和Player是不同進(jìn)程,所以在Player里面注冊(cè)了一個(gè)Receiver來(lái)實(shí)現(xiàn)的。
BroadcastReceiver本身可以接受Intent,也可以設(shè)置filter接受特定的Intent,然后在onReceive函數(shù)里面來(lái)具體處理。
-
- private BroadcastReceiver receiver = new BroadcastReceiver(){
- public void onReceive(Context context, Intent intent) {
- if (intent.getAction.equals(Consts.INTENT_UPDATE_SONG_PICTURE)){
- updateSongPicture(); //更新專輯封面
- }
- ...
- }
- }
上面的代碼里,就是在Service里面圖片下載完成后sendBroadcast,然后Receiver收到后去更新專輯封面。這里要注意的是,要在 Player的onResume和onPause方法里面分別調(diào)用registerReceiver和unregisterReceiver。
- protected void onResume(){
- super.onResume();
- ...
- filter = new IntentFilter();
- filter.addAction(Consts.INTENT_UPDATE_SONG_PICTURE);
- ...
- registerReceiver(receiver, filter);
- }
- protected void onPause(){
- super.onPause();
- unregisterReceiver(receiver);
- }
Login里面的Handler
Login也是一個(gè)單獨(dú)的Activity,左圖為L(zhǎng)ogin的頁(yè)面,因?yàn)榈卿洷?較耗時(shí),所以要顯示一個(gè)有進(jìn)度的提示框給用戶,告訴用戶正在 登錄。
這個(gè)時(shí)候,登錄的網(wǎng)絡(luò)操作是新起一個(gè)線程去做的,因?yàn)镺Phone的 UI是單線程的模型,在子線程里是不能直接去碰UI的。所以子線 程完成登錄要在主線程里面有一個(gè)Handler去處理子線程的消息 來(lái)更新UI。
如下圖所示
下面是在登錄按鈕的OnClickListener里面onClick方法里面的調(diào)用,向子線程發(fā)完消息后就會(huì)返回,不會(huì)阻塞住UI。
-
- Message msg = looper.handler.obtainMessage(MSG_LOGIN); Bundle bundle = new Bundle();
- ...
- //向子線程發(fā)消息執(zhí)行l(wèi)ogin
- looper.handler.sendMessage(msg);
- //顯示進(jìn)度對(duì)話框
- dialog.show();
下面的代碼就是主線程里的Handler,里面根據(jù)子線程的消息來(lái)更新UI。
- private Handler mainHandler = new Handler() {
- public void handleMessage(Message msg) {
- switch (msg.what){
- case MSG_DONE:
- dialog.dismiss(); //把對(duì)話框關(guān)掉
- int error = msg.arg1;
- if (error == 0){
- Intent intent = new Intent(Consts.INTENT_RADIO_PLAYER);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- startActivity(intent); //啟動(dòng)Player
- finish();
- }else{
- showToast(error); //顯示錯(cuò)誤提示
- }
- ...
- }
- }
- };
下面是執(zhí)行登錄的子線程,這里使用了平臺(tái)提供的Looper,可以方便的在線程里實(shí)現(xiàn)一個(gè)消息隊(duì)列,然后用一個(gè)Handler來(lái)處理消 息。Looper的用法很簡(jiǎn)單,只需要在循環(huán)的開(kāi)始和結(jié)束分別進(jìn)行prepare和loop就好了。
- private class LooperThread extends Thread {
- private Handler handler;
- public void run() {
- Looper.prepare();
- handler = new Handler(){
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case MSG_LOGIN:
- ... //執(zhí)行l(wèi)ogin網(wǎng)絡(luò)操作
- Message m = mainHandler.obtainMessage(MSG_DONE);
- ...
- mainHandler.sendMessage(m); //完成后給主線程發(fā)消息
- break;
- ...
- }
- };
- Looper.loop();
- }
- }
- }
OPhone平臺(tái)的進(jìn)程/線程間通訊
上面用到了Service,Receiver和Handler,里面涉及到了不同進(jìn)程,不同線程間的通訊。 進(jìn)程間通訊是基于libutils的Binder機(jī)制的,這里簡(jiǎn)單的總結(jié)一下:
- Service和Client是比較緊的耦合,通過(guò)aidl來(lái)定義接口,Service接口返回的數(shù)據(jù)類型必須實(shí)現(xiàn)parcelable。
- BroadcastReceiver是廣播和訂閱的模式,比較松散,是用發(fā)送Intent來(lái)通訊的,數(shù)據(jù)的話可以放到Bundle里。
受限于UI的單線程模型,需要把比較耗時(shí)的操作用子線程來(lái)做,避免UI阻塞。平臺(tái)提供了Handler和Looper,可以比較方便的在主線程和子線程之間通訊。這種方法比AsycTask的方式要更靈活,而且避免了反復(fù)創(chuàng)建啟動(dòng)線程的開(kāi)銷。
網(wǎng)絡(luò)操作
下面是網(wǎng)絡(luò)操作涉及到的一些問(wèn)題,使用的是Apache接口。
設(shè)置連接參數(shù)
- HttpParams params = new BasicHttpParams();
- //設(shè)置超時(shí)為Consts.TIMEOUT毫秒
- HttpConnectionParams.setConnectionTimeout(params, Consts.TIMEOUT);
- //buffer大小
- HttpConnectionParams.setSocketBufferSize(params, Consts.BUFFER_SIZE);
- //user agent
- HttpProtocolParams.setUserAgent(params, Consts.USER_AGENT);
- DefaultHttpClient client = new DefaultHttpClient(params);
HttpGet
以get方法訪問(wèn)一個(gè)url,注意args需要用URLEncoder.encode()處理下。一般網(wǎng)絡(luò)調(diào)用返回JSON格式,可以把返回的字符串轉(zhuǎn)換成 JSONObject就可以處理了。
- public String getString(String path, String args) throws Exception{
- client.getCookieStore().clear(); //清cookie,根據(jù)需要設(shè)置
- HttpGet get = new HttpGet(new URI("http", null, Consts.HOST, 80, path, args, null));
- HttpResponse response = client.execute(get);
- HttpEntity entity = response.getEntity();
- return EntityUtils.toString(entity);}
HttpPost
- public String post(String url, List <NameValuePair> nvps) throws Exception{
- HttpPost httpost = new HttpPost(url);
- httpost.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8));
- HttpResponse response = client.execute(httpost);
- HttpEntity entity = response.getEntity();
- return EntityUtils.toString(entity);
- }
下載圖片
OPhone平臺(tái)沒(méi)有UI控件可以直接顯示一個(gè)網(wǎng)絡(luò)上的圖片,必須要先下載下來(lái)再顯示.
- public Bitmap getImage(String url) throws Exception{
- HttpGet get = new HttpGet(url);
- HttpResponse response = client.execute(get);
- HttpEntity entity = response.getEntity();
- byte[] data = EntityUtils.toByteArray(entity);
- return BitmapFactory.decodeByteArray(data, 0, data.length);
- }
數(shù)據(jù)庫(kù)操作
OPhone平臺(tái)的數(shù)據(jù)庫(kù)是Sqlite,并且提供了SQLiteOpenHelper這個(gè)類來(lái)方便使用
數(shù)據(jù)庫(kù)表創(chuàng)建和設(shè)計(jì)
- class DatabaseHelper extends SQLiteOpenHelper{
- public DatabaseHelper(Context context){
- super(context, DATABASE_NAME, null, DATABASE_VERSION);
- }
- public void onCreate(SQLiteDatabase db) {
- db.execSQL(create_table_sql); //這里創(chuàng)建表
- }
- public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
- //當(dāng)數(shù)據(jù)庫(kù)版本升級(jí)的時(shí)候會(huì)調(diào)這個(gè)函數(shù)
- }
- }
查詢
這里使用的rawQuery,直接是執(zhí)行sql,我覺(jué)得比別的接口更靈活。這里注意的是,從cursor里取數(shù)據(jù)時(shí),首先要cursor.moveToFirst... 還有記得cursor和help最后都要調(diào)他們的close方法來(lái)釋放資源,否則會(huì)內(nèi)存泄漏。
- public String[] getStrings(){
- SQLiteDatabase db = helper.getReadableDatabase();
- try {
- Cursor cursor = db.rawQuery("select col from table", null);
- int total = cursor.getCount();
- if (total > 0){
- String[] ss = new String[total];
- cursor.moveToFirst();
- for (int i = 0; i < total; i++){
- ss[i] = cursor.getString(0);
- cursor.moveToNext();
- }
- cursor.close();
- return ss;
- }
- cursor.close();
- } catch (SQLiteException e) {
- }
- return null;
- }
UI相關(guān)
下面是電臺(tái)這個(gè)應(yīng)用涉及到的一些比較常用的UI組件的用法介紹。
Notification
電臺(tái)在后臺(tái)播放時(shí),Service會(huì)在任務(wù)欄上放一個(gè)通知,以便Player退出時(shí),來(lái)顯示當(dāng)前播放的歌曲,而且點(diǎn)通知會(huì)打開(kāi)Player。Notification在下來(lái)列表里的UI是RemoteView, 可以設(shè)置簡(jiǎn)單的view進(jìn)去,比如TextView,ImageView,也可以用自己的layout,但是不能設(shè)置別的復(fù)雜View。如果有人找到方法,請(qǐng)發(fā)個(gè)mail給我。: )
- private void sendNotifcation(){
- int icon = R.drawable.title_icon;
- CharSequence tickerText = context.getString(R.string.notification_title);
- long when = System.currentTimeMillis();
- if (notification == null){
- notification = new Notification(icon, tickerText, when);
- notification.flags = Notification.FLAG_NO_CLEAR; //設(shè)置不能被清除
- }
-
- Intent notificationIntent = new Intent(Consts.INTENT_RADIO_PLAYER);
- PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
- RemoteViews contentView = new RemoteViews(getPackageName(), R.layout.notification);
- contentView.setImageViewResource(R.id.notification_image, R.drawable.icon);
- contentView.setTextViewText(R.id.songName, song.title);
- contentView.setTextViewText(R.id.artistName, song.artist);
-
- notification.contentView = contentView;
- notification.contentIntent = contentIntent;
-
- notifyManager.notify(Consts.NOTIFICATION_PLAY, notification);
- }
帶進(jìn)度條的對(duì)話框
如果只是像登錄時(shí)那個(gè)登錄進(jìn)度框的話,比較簡(jiǎn)單,就像下面的方式定義一個(gè)ProgressDialog就好了。如果需要自己來(lái)更新進(jìn)度的話,就要在handler里面去更新進(jìn)度值了。
- dialog = new ProgressDialog(this);
- dialog.setMessage(getResources().getString(R.string.logging));
- dialog.setIndeterminate(true);
- dialog.setCancelable(true);
對(duì)話框風(fēng)格的Activity
有時(shí)候你需要一個(gè)長(zhǎng)的像Dialog的Activity,比如之前的登錄頁(yè)面。那么需要就使用style,就像css一樣,但是還沒(méi)那么好用就是了。如下
- <activity android:name=".Login" ...
- android:theme="@style/DoubanTheme.Dialog">
- ...
- </activity>
-
- 而style在res/value/style.xml里面定義
- <resources>
- <style name="DoubanTheme.Dialog" parent="@android:style/Theme.Dialog">
- <item name="android:windowNoTitle">true</item>
- ...
- </style>
- ...
- </resources>
自動(dòng)補(bǔ)齊的輸入框
自動(dòng)補(bǔ)齊的輸入框,也就是AutoCompleteTextView,使用很簡(jiǎn)單,只需要把匹配的數(shù)據(jù)做成一個(gè)ArrayAdapter,然后setAdapter就好了,根據(jù)需要選擇layout,比如simple_dropdown_item_1line。
- emailText = (AutoCompleteTextView)findViewById(R.id.emailText);
- emailText.setAdapter(new ArrayAdapter<String>
- (this, android.R.layout.simple_dropdown_item_1line, emails));
超鏈接
如果你需要超鏈接,不是那么容易的。比如Login頁(yè)面那個(gè)注冊(cè)的鏈接,是先要寫一個(gè)正則表達(dá)式,然后用Linkify.addLinks把一個(gè)view里的符合正則表達(dá)式的文字加上鏈接。
- registerLink = (TextView)findViewById(R.id.registerLink);
- Pattern p = Pattern.compile(getResources().getString(R.string.register_link_text));
- Linkify.addLinks(registerLink, p, Consts.REGISTER_URL);
菜單
OPhone平臺(tái)創(chuàng)建菜單也比較方便,下面是Player里面Options Menu的用法
- public boolean onCreateOptionsMenu(Menu menu){
- super.onCreateOptionsMenu(menu);
- menu.add(0, MENU_PAUSE, 0, R.string.menu_pause);
- menu.add(0, MENU_LOGOUT, 0, R.string.menu_logout);
- menu.add(0, MENU_QUIT, 0, R.string.menu_quit);
- return true;
- }
你也可以根據(jù)需要在prepare的時(shí)候來(lái)更改菜單
- public boolean onPrepareOptionsMenu(Menu menu){
- super.onPrepareOptionsMenu(menu);
- MenuItem item = menu.findItem(MENU_PAUSE);
-
- //根據(jù)現(xiàn)在是不是在播放來(lái)顯示是“暫?!边€是"開(kāi)始"的title
- item.setTitle(isPlaying ? R.string.menu_pause : R.string.menu_start);
- return true;
- }
然后在用戶點(diǎn)擊菜單項(xiàng)的時(shí)候處理
- public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case MENU_PAUSE:
- pause();
- return true;
- case MENU_LOGOUT:
- logout();
- return true;
- case MENU_QUIT:
- quit();
- return true;
- }
- return false;
- }
動(dòng)畫
在歌曲專輯封面圖片的加載時(shí)使用了一個(gè)淡入/淡出的動(dòng)畫效果。下面簡(jiǎn)單介紹一下OPhone里面動(dòng)畫效果的用法,只是用的xml的方式。
定義動(dòng)畫效果的xml,在res/anim/fade_in.xml
- <alpha
- android:fromAlpha="0.1"
- android:toAlpha="1.0"
- android:duration="3000"
- android:interpolator="@android:anim/accelerate_decelerate_interpolator"
- />
定義ImageView,加載動(dòng)畫
- songPicture = (ImageView)findViewById(R.id.songPicture);
- inAnimation = AnimationUtils.loadAnimation(this,R.anim.fade_in);
當(dāng)更新專輯封面時(shí),開(kāi)始動(dòng)畫
- songPicture.setImageBitmap(pic);
- songPicture.startAnimation(inAnimation);
AndroidManifest里面的設(shè)置
permission
需要的permission,主要有網(wǎng)絡(luò),存儲(chǔ),監(jiān)聽(tīng)手機(jī)和網(wǎng)絡(luò)狀態(tài)。
- <uses-permission android:name="android.permission.INTERNET" />
- <uses-permission android:name="android.permission.WRITE_OWNER_DATA" />
- <uses-permission android:name="android.permission.READ_OWNER_DATA" />
- <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
- <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
要求sdk最低版本
- <uses-sdk android:minSdkVersion="3"/>
設(shè)置應(yīng)用為單實(shí)例模式
- <application ... android:launchMode="singleInstance" ...>
設(shè)置Service
禁止別的應(yīng)用使用該Service,需要將里面的exported設(shè)為false,另外設(shè)置Service的進(jìn)程名字后綴為":radio"。
- <service android:name=".RadioService" android:exported="false" android:process=":radio" >
- <intent-filter>
- <action android:name="..." />
- </intent-filter></service>
其他技巧
MediaPlayer的prepare
MediaPlayer有2種prepare方式,建議使用異步的prepare,然后在OnPreparedListener里面監(jiān)聽(tīng),當(dāng)準(zhǔn)備好時(shí)處理。
- mplayer.setOnPreparedListener(prepareListener); //設(shè)置listener
- ...
- mplayer.reset();
- mplayer.setDataSource(song.url); //設(shè)置歌曲url
- mplayer.prepareAsync();
- ...
- private MediaPlayer.OnPreparedListener prepareListener = new MediaPlayer.OnPreparedListener(){
-
- public void onPrepared(MediaPlayer mp){
- mp.start(); //開(kāi)始播放
- //通知Player更新歌曲時(shí)間
- sendBroadcast(new Intent(Consts.INTENT_UPDATE_SONG_TIME));
- }
- };
查看有沒(méi)有可用的網(wǎng)絡(luò)連接
下面是監(jiān)聽(tīng)網(wǎng)絡(luò)連接狀態(tài)的函數(shù),這里只是檢查有沒(méi)有可用的連接。其實(shí)還可用根據(jù)不同連接的類型來(lái)使用不同的策略,比如用WIFI的時(shí)候就用高音質(zhì)的音樂(lè),而當(dāng)用3G/2G上網(wǎng)時(shí)就用比較低的音樂(lè)。
- public boolean isNetworkAvailable() {
- Context context = getApplicationContext();
- ConnectivityManager connectivity =
- (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
- nkify.addLinksif (connectivity == null) {
- return false;
- } else {
- NetworkInfo[] info = connectivity.getAllNetworkInfo();
- if (info != null) {
- for (int i = 0; i < info.length; i++) {
- if (info[i].getState() == NetworkInfo.State.CONNECTED) {
- return true;
- }
- }
- }
- }
- return false;
- }
監(jiān)聽(tīng)手機(jī)狀態(tài)
利用PhoneStateListener來(lái)監(jiān)聽(tīng)手機(jī)是否收到電話。這里沒(méi)有處理打電話的問(wèn)題是因?yàn)槲矣X(jué)得,你如果要播電話的時(shí)候一定會(huì)想先把電臺(tái)關(guān)了的,呵呵。
- private class TeleListener extends PhoneStateListener{
- public void onCallStateChanged(int state, String incomingNumber){
- super.onCallStateChanged(state, incomingNumber);
- switch (state){
- case TelephonyManager.CALL_STATE_IDLE:
- startPlay(); //當(dāng)掛斷電話時(shí),繼續(xù)播放
- break;
- case TelephonyManager.CALL_STATE_OFFHOOK:
- doStop(); //當(dāng)有電話在等時(shí),暫停音樂(lè)
- break;
- case TelephonyManager.CALL_STATE_RINGING:
- doStop(); //當(dāng)有電話進(jìn)來(lái)時(shí),暫停音樂(lè)
- break;
- }
- }
- }
橫豎屏轉(zhuǎn)換
默認(rèn)橫豎屏轉(zhuǎn)換是會(huì)把你的Activity關(guān)掉重新啟動(dòng)的,但你可以在你的manifest里面設(shè)置Activity的屬性configChanges來(lái)自己處理,也可以在手機(jī)橫豎屏轉(zhuǎn)換的時(shí)候來(lái)加載不同的UI layout。
- <activity android:name=".RadioPlayer" ....
- android:configChanges="orientation|keyboardHidden|navigation" >
- ...
- </activity>
然后在onConfigurationChanged來(lái)處理加載不同的layout。
- public void onConfigurationChanged(Configuration newConfig){
- if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE){
- setContentView(R.layout.landscape);
- }else{
- setContentView(R.layout.portrait);
- }
- }
結(jié)束語(yǔ)
本文的例子豆瓣電臺(tái)還在開(kāi)發(fā)中,在做UI的美化以及一些改進(jìn)用戶體驗(yàn)的特性,會(huì)在不遠(yuǎn)的將來(lái)發(fā)布,還請(qǐng)大家關(guān)注。
就到這里,本文是以O(shè)MS平臺(tái)的網(wǎng)絡(luò)應(yīng)用為例,講一個(gè)web開(kāi)發(fā)人員在學(xué)習(xí)OMS平臺(tái)的應(yīng)用開(kāi)發(fā)時(shí)會(huì)遇到的典型問(wèn)題的解決方案,呵呵。希望對(duì)大家有所幫助。