桌面widget詳解(四)——桌面音樂播放器(實戰)

先看看本篇的最終效果:

 

一、Service控制播放部分(MusicManageService.java)

 

首先由於我們要與按鈕相交互,所以在Service中的交互一般是通過BroadcastReceiver來實現的,所以在MusicManageService的OnCreate函數中(Service起來的時候調用OnCreate)應該包括下面幾個步驟:註冊Receiver,初始化歌曲播放列表,開始播放默認歌曲;


所以首先是註冊Receiver:

IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(ACTION);
registerReceiver(receiver, intentFilter);

對應的BroadcastReceiver receiver主要是根據接收來的消息來上一首,下一首,暫停、播放歌曲:

public static String ACTION = "to_service";
public static String KEY_USR_ACTION = "key_usr_action";
public static final int ACTION_PRE = 0, ACTION_PLAY_PAUSE = 1, ACTION_NEXT = 2;
private boolean mPlayState = false;
 
private BroadcastReceiver receiver = new BroadcastReceiver() {
	@Override
	public void onReceive(Context context, Intent intent) {
		String action  = intent.getAction();
		if (ACTION.equals(action)) {
			int widget_action = intent.getIntExtra(KEY_USR_ACTION, -1);
			
			switch (widget_action) {
			case ACTION_PRE:
				playPrev(context);
				Log.d("harvic","action_prev");
				break;
			case ACTION_PLAY_PAUSE:
				if (mPlayState) {
					pause(context);
					Log.d("harvic","action_pause");
				}else{
					play(context);
					Log.d("harvic","action_play");
				}	
				break;
			case ACTION_NEXT:
				playNext(context);
				Log.d("harvic","action_next");
				break;
			default:
				break;
			}
		}
	}
};

然後是初始化播放列表:

private int[] mArrayList = new int[9];
private void initList() {
	mArrayList[0] = R.raw.dui_ni_ai_bu_wan;
	mArrayList[1] = R.raw.fei_yu;
	mArrayList[2] = R.raw.gu_xiang_de_yun;
	mArrayList[3] = R.raw.hen_ai_hen_ai_ni;
	mArrayList[4] = R.raw.new_day;
	mArrayList[5] = R.raw.shi_jian_li_de_hua;
	mArrayList[6] = R.raw.ye_gui_ren;
	mArrayList[7] = R.raw.yesterday_once_more;
	mArrayList[8] = R.raw.zai_lu_shang;
}

最後在Service起來時就應該讓它播放歌曲:

private void mediaPlayerStart(){
	mPlayer = new MediaPlayer();
	mPlayer = MediaPlayer.create(getApplicationContext(), mArrayList[mIndex]);
	mPlayer.start();
	mPlayState = true;
}

上面就基本上就是MusicManageService的骨架了,其它就是上一首,下一首,播放、暫停,這些難度都不大,就不細講了,完整的MusicManageService.java代碼如下:

public class MusicManageService extends Service {
 
	private MediaPlayer mPlayer;
	private int mIndex = 4;// 從中間開始放
	private int[] mArrayList = new int[9];
	public static String ACTION = "to_service";
	public static String KEY_USR_ACTION = "key_usr_action";
	public static final int ACTION_PRE = 0, ACTION_PLAY_PAUSE = 1, ACTION_NEXT = 2;
	private boolean mPlayState = false;
 
	private BroadcastReceiver receiver = new BroadcastReceiver() {
		@Override
		public void onReceive(Context context, Intent intent) {
			String action  = intent.getAction();
			if (ACTION.equals(action)) {
				int widget_action = intent.getIntExtra(KEY_USR_ACTION, -1);
				
				switch (widget_action) {
				case ACTION_PRE:
					playPrev(context);
					Log.d("harvic","action_prev");
					break;
				case ACTION_PLAY_PAUSE:
					if (mPlayState) {
						pause(context);
						Log.d("harvic","action_pause");
					}else{
						play(context);
						Log.d("harvic","action_play");
					}	
					break;
				case ACTION_NEXT:
					playNext(context);
					Log.d("harvic","action_next");
					break;
				default:
					break;
				}
			}
		}
	};
 
	@Override
	public IBinder onBind(Intent intent) {
		// TODO Auto-generated method stub
		return null;
	}
 
	@Override
	public void onCreate() {
		// TODO Auto-generated method stub
		super.onCreate();
 
		IntentFilter intentFilter = new IntentFilter();
		intentFilter.addAction(ACTION);
		registerReceiver(receiver, intentFilter);
 
		initList();
 
		mediaPlayerStart();		
	}
	private void mediaPlayerStart(){
		mPlayer = new MediaPlayer();
		mPlayer = MediaPlayer.create(getApplicationContext(), mArrayList[mIndex]);
		mPlayer.start();
		mPlayState = true;
	}
 
	private void initList() {
		mArrayList[0] = R.raw.dui_ni_ai_bu_wan;
		mArrayList[1] = R.raw.fei_yu;
		mArrayList[2] = R.raw.gu_xiang_de_yun;
		mArrayList[3] = R.raw.hen_ai_hen_ai_ni;
		mArrayList[4] = R.raw.new_day;
		mArrayList[5] = R.raw.shi_jian_li_de_hua;
		mArrayList[6] = R.raw.ye_gui_ren;
		mArrayList[7] = R.raw.yesterday_once_more;
		mArrayList[8] = R.raw.zai_lu_shang;
	}
 
	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
 
		return super.onStartCommand(intent, flags, startId);
	}
 
	@Override
	public void onDestroy() {
 
		super.onDestroy();
		mPlayer.stop();
	}
 
	/**
	 * 播放下一首
	 * 
	 * @param context
	 */
	public void playNext(Context context) {
		if (++mIndex > 8) {
			mIndex = 0;
		}
		mPlayState = true;
		playSong(context, mArrayList[mIndex]);
	}
 
	/**
	 * 播放上一首
	 * 
	 * @param context
	 */
	public void playPrev(Context context) {
		if (--mIndex < 0) {
			mIndex = 8;
		}
		mPlayState = true;
		playSong(context, mArrayList[mIndex]);
	}
 
	/*
	 * 繼續播放
	 */
	public void play(Context context) {
		mPlayState = true;
		mPlayer.start();
	}
 
	/**
	 * 暫停播放
	 * 
	 * @param context
	 */
	public void pause(Context context) {
		mPlayState = false;
		mPlayer.pause();		
	}
 
	/**
	 * 播放指定的歌曲
	 * 
	 * @param context
	 * @param resid
	 */
	private void playSong(Context context, int resid) {
		AssetFileDescriptor afd = context.getResources().openRawResourceFd(
				mArrayList[mIndex]);
		try {
			mPlayer.reset();
			mPlayer.setDataSource(afd.getFileDescriptor(),
					afd.getStartOffset(), afd.getDeclaredLength());
			mPlayer.prepare();
			mPlayer.start();
			afd.close();
		} catch (Exception e) {
			Log.e("harvic","Unable to play audio queue do to exception: "+ e.getMessage(), e);
		}
 
	}
}

二、widget發送廣播部分(ExampleAppWidgetProvider.java)

首先,在用戶添加widget時,會調用OnUpdate()函數,所在我們在OnUpdate()中要實現綁定RemoteView和更新Widget的操作。

private void pushUpdate(Context context,AppWidgetManager appWidgetManager) {
	
	RemoteViews remoteView = new RemoteViews(context.getPackageName(),R.layout.example_appwidget);
	//將按鈕與點擊事件綁定
	remoteView.setOnClickPendingIntent(R.id.play_pause,getPendingIntent(context, R.id.play_pause));
	remoteView.setOnClickPendingIntent(R.id.prev_song, getPendingIntent(context, R.id.prev_song));
	remoteView.setOnClickPendingIntent(R.id.next_song, getPendingIntent(context, R.id.next_song));
 
	// 相當於獲得所有本程序創建的appwidget
	ComponentName componentName = new ComponentName(context,ExampleAppWidgetProvider.class);
	appWidgetManager.updateAppWidget(componentName, remoteView);
}
 
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager,
		int[] appWidgetIds) {
	
	pushUpdate(context,appWidgetManager);
}

首先是新建RemoteView,並將它與那三個按鈕相綁定,其中的GetPendingIntent的實現與上一篇一樣,也是把按鈕ID傳進去,通過Uri來傳送,這裏爲了接收到以後方便識別是按鈕點擊傳過去的消息,我們隨便加一個Category字段,所以在接收方只需要通過intent.hasCategory(Intent.CATEGORY_ALTERNATIVE)來判斷是不是我們這裏傳過去的Intent即可,這裏又比上篇高級了一點點有沒有,哈哈。

private PendingIntent getPendingIntent(Context context, int buttonId) {
	Intent intent = new Intent();
	intent.setClass(context, ExampleAppWidgetProvider.class);
	intent.addCategory(Intent.CATEGORY_ALTERNATIVE);
	intent.setData(Uri.parse("harvic:" + buttonId));
	PendingIntent pi = PendingIntent.getBroadcast(context, 0, intent, 0);
	return pi;
}

然後是接收部分,在接收時,首先根據當前用戶點擊的哪個按鈕,然後給MusicManageService發送不同的廣播,讓MusicManageService做出不同的響應,接收代碼如下 :

public void onReceive(Context context, Intent intent) {
	String action = intent.getAction();
	Log.d("harvic", "action:"+action);
	
	if (intent.hasCategory(Intent.CATEGORY_ALTERNATIVE)) {
		Uri data = intent.getData();
        int buttonId = Integer.parseInt(data.getSchemeSpecificPart());
        switch (buttonId) {
        case R.id.play_pause:
        	pushAction(context,MusicManageService.ACTION_PLAY_PAUSE);
        	if(mStop){
        		Intent startIntent = new Intent(context,MusicManageService.class);
				context.startService(startIntent);
        		mStop = false;
        	}
        	break;
        case R.id.prev_song:
        	pushAction(context, MusicManageService.ACTION_PRE);
        	break;
        case R.id.next_song:
        	pushAction(context, MusicManageService.ACTION_NEXT);
        	break;
        }
 
	}
	super.onReceive(context, intent);
}

在這裏首先根據是不是包含intent.hasCategory(Intent.CATEGORY_ALTERNATIVE)這個category來判斷是不是點擊按鈕發出來的消息,然後提取出按鈕ID,最後根據不同的按鈕ID發送出不同的消息:

private void pushAction(Context context, int ACTION) {
    Intent actionIntent = new Intent(MusicManageService.ACTION);
    actionIntent.putExtra(MusicManageService.KEY_USR_ACTION, ACTION);
    context.sendBroadcast(actionIntent);
}

這裏發送消息與MusicManageService的消息接收方式是統一的,發送和接收都是通過Action來匹配,攜帶的值是當前的Action,下面就是MusicManageService接收時的部分代碼,我再摘一遍,方便大家理解:

private BroadcastReceiver receiver = new BroadcastReceiver() {
	@Override
	public void onReceive(Context context, Intent intent) {
		String action  = intent.getAction();
		if (ACTION.equals(action)) {
			int widget_action = intent.getIntExtra(KEY_USR_ACTION, -1);
			
			switch (widget_action) {
			case ACTION_PRE:
				break;
			case ACTION_PLAY_PAUSE:
				break;
			case ACTION_NEXT:
				break;
			}
		}
	}
};

所以完整的ExampleAppWidgetProvider代碼是這樣的:

public class ExampleAppWidgetProvider extends AppWidgetProvider {
 
	private ExampleAppWidgetProvider mProvider = null;
	private boolean mStop = true;
	
	private PendingIntent getPendingIntent(Context context, int buttonId) {
		Intent intent = new Intent();
		intent.setClass(context, ExampleAppWidgetProvider.class);
		intent.addCategory(Intent.CATEGORY_ALTERNATIVE);
		intent.setData(Uri.parse("harvic:" + buttonId));
		PendingIntent pi = PendingIntent.getBroadcast(context, 0, intent, 0);
		return pi;
	}
	
	// 更新所有的 widget
	private void pushUpdate(Context context,AppWidgetManager appWidgetManager) {
		
		RemoteViews remoteView = new RemoteViews(context.getPackageName(),R.layout.example_appwidget);
		//將按鈕與點擊事件綁定
		remoteView.setOnClickPendingIntent(R.id.play_pause,getPendingIntent(context, R.id.play_pause));
		remoteView.setOnClickPendingIntent(R.id.prev_song, getPendingIntent(context, R.id.prev_song));
		remoteView.setOnClickPendingIntent(R.id.next_song, getPendingIntent(context, R.id.next_song));
 
		// 相當於獲得所有本程序創建的appwidget
		ComponentName componentName = new ComponentName(context,ExampleAppWidgetProvider.class);
		appWidgetManager.updateAppWidget(componentName, remoteView);
	}
 
	@Override
	public void onUpdate(Context context, AppWidgetManager appWidgetManager,
			int[] appWidgetIds) {
		
		pushUpdate(context,appWidgetManager);
	}
	
	// 接收廣播的回調函數
	@Override
	public void onReceive(Context context, Intent intent) {
		String action = intent.getAction();
		Log.d("harvic", "action:"+action);
		
		if (intent.hasCategory(Intent.CATEGORY_ALTERNATIVE)) {
			Uri data = intent.getData();
	        int buttonId = Integer.parseInt(data.getSchemeSpecificPart());
	        switch (buttonId) {
	        case R.id.play_pause:
	        	pushAction(context,MusicManageService.ACTION_PLAY_PAUSE);
	        	if(mStop){
	        		Intent startIntent = new Intent(context,MusicManageService.class);
					context.startService(startIntent);
	        		mStop = false;
	        	}
	        	break;
	        case R.id.prev_song:
	        	pushAction(context, MusicManageService.ACTION_PRE);
	        	break;
	        case R.id.next_song:
	        	pushAction(context, MusicManageService.ACTION_NEXT);
	        	break;
	        }
	
		}
		super.onReceive(context, intent);
	}
	
	private void pushAction(Context context, int ACTION) {
        Intent actionIntent = new Intent(MusicManageService.ACTION);
        actionIntent.putExtra(MusicManageService.KEY_USR_ACTION, ACTION);
        context.sendBroadcast(actionIntent);
    }
}

到這裏,效果是這樣的:(在模擬器上點擊看起來沒有任何反應,其實已經在播放歌曲了,上一首,下一首,播放、暫停功能都是可用的)

三、Service反向通知Widget更新當前狀態

上面我們已經實現了widget按鈕向Service發廣播來播放歌曲的播放、暫停,上一首,下一首,但是我們的widget狀態確沒有改變,這節我們就需要根據當前歌曲的狀態來更新widget控件的狀態。

1、發送當前狀態廣播

首先,我們要在MusicManageService中根據當前的播放狀態往ExampleAppWidgetProvider發送廣播,廣播的目的主要是改變當前播放按鈕的狀態(播放、暫停)還有更新TextView,讓它顯示當前播放歌曲的ID值。

所以我們在發送廣播時,需要定義Intent的Action,和存放播放狀態、歌曲Id的putExtra(key,value)中的key值:

public static String MAIN_UPDATE_UI = "main_activity_update_ui";  //Action
public static String KEY_MAIN_ACTIVITY_UI_BTN = "main_activity_ui_btn_key"; //putExtra中傳送當前播放狀態的key
public static String KEY_MAIN_ACTIVITY_UI_TEXT = "main_activity_ui_text_key"; //putextra中傳送TextView的key
public static final int  VAL_UPDATE_UI_PLAY = 1,VAL_UPDATE_UI_PAUSE =2;//當前歌曲的播放狀態

發送時:

private void postState(Context context, int state,int songid) {
	Intent actionIntent = new Intent(ExampleAppWidgetProvider.MAIN_UPDATE_UI);
	actionIntent.putExtra(ExampleAppWidgetProvider.KEY_MAIN_ACTIVITY_UI_BTN,state);
	actionIntent.putExtra(ExampleAppWidgetProvider.KEY_MAIN_ACTIVITY_UI_TEXT, songid);
	context.sendBroadcast(actionIntent);
}

其中:

 

state就是上面VAL_UPDATE_UI_PLAY = 1,VAL_UPDATE_UI_PAUSE =2其中一個狀態;

songid:表示當前播放歌曲的id值

所以在播放歌曲狀態和歌曲id值改變時,就應該發送廣播:

首先,在Service起來時,我們開始播放歌曲:
 

private void mediaPlayerStart(){
	mPlayer = new MediaPlayer();
	mPlayer = MediaPlayer.create(getApplicationContext(), mArrayList[mIndex]);
	mPlayer.start();
	mPlayState = true;
	postState(getApplicationContext(), ExampleAppWidgetProvider.VAL_UPDATE_UI_PLAY,mIndex);
}

同樣,在上一首,下一首,播放,暫停時都要發送廣播:

 

播放時:(改變了播放狀態)

public void play(Context context) {
	……
	postState(context, ExampleAppWidgetProvider.VAL_UPDATE_UI_PLAY,mIndex);
}

暫停時:(改變了播放狀態)

public void pause(Context context) {
	……		
	postState(context, ExampleAppWidgetProvider.VAL_UPDATE_UI_PAUSE,mIndex);
}

上一首:(改變了歌曲ID)

public void playPrev(Context context) {
	……
	postState(context, ExampleAppWidgetProvider.VAL_UPDATE_UI_PLAY,mIndex);
}

下一首:(改變了歌曲ID)

public void playNext(Context context) {
	……
	postState(context, ExampleAppWidgetProvider.VAL_UPDATE_UI_PLAY,mIndex);
}

OK啦,到這裏發送就結束了,下面就是接收了。

2、接收廣播

 

首先在接收廣播之前要註冊,ExampleAppWidgetProvider以前說過是直接派生自 BroadcastReceiver的,所有我們只能採用靜態註冊的方式:注意的action要與發送的一致,即:main_activity_update_ui

        <receiver android:name=".ExampleAppWidgetProvider" >
            <intent-filter>
				<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
				<action android:name="main_activity_update_ui" />
			</intent-filter>
			<meta-data android:name="android.appwidget.provider"
				android:resource="@xml/example_appwidget_info" />
        </receiver>

在上面,我們在ExampleAppWidgetProvider中更新RemoteView的pushUpdate() 的代碼是這樣的:

private void pushUpdate(Context context,AppWidgetManager appWidgetManager) {
	
	RemoteViews remoteView = new RemoteViews(context.getPackageName(),R.layout.example_appwidget);
	//將按鈕與點擊事件綁定
	remoteView.setOnClickPendingIntent(R.id.play_pause,getPendingIntent(context, R.id.play_pause));
	remoteView.setOnClickPendingIntent(R.id.prev_song, getPendingIntent(context, R.id.prev_song));
	remoteView.setOnClickPendingIntent(R.id.next_song, getPendingIntent(context, R.id.next_song));
 
	// 相當於獲得所有本程序創建的appwidget
	ComponentName componentName = new ComponentName(context,ExampleAppWidgetProvider.class);
	appWidgetManager.updateAppWidget(componentName, remoteView);
}

在這裏,我們只是綁定了三個按鈕控件,但並沒有更新當前播放按鈕狀態和TextView上的字體,所以我們對它加以更改,在綁定按鈕以後,根據當前接收到的狀態,更新RemoteView

private void pushUpdate(Context context,AppWidgetManager appWidgetManager,String songName,Boolean play_pause) {
	
	RemoteViews remoteView = new RemoteViews(context.getPackageName(),R.layout.example_appwidget);
	//將按鈕與點擊事件綁定
	remoteView.setOnClickPendingIntent(R.id.play_pause,getPendingIntent(context, R.id.play_pause));
	remoteView.setOnClickPendingIntent(R.id.prev_song, getPendingIntent(context, R.id.prev_song));
	remoteView.setOnClickPendingIntent(R.id.next_song, getPendingIntent(context, R.id.next_song));
	
	//設置內容
	if (!songName.equals("")) {
		remoteView.setTextViewText(R.id.song_name, songName);
	}
	//設定按鈕圖片
	if (play_pause) {
		remoteView.setImageViewResource(R.id.play_pause, R.drawable.car_musiccard_pause);
	}else {
		remoteView.setImageViewResource(R.id.play_pause, R.drawable.car_musiccard_play);
	}
	// 相當於獲得所有本程序創建的appwidget
	ComponentName componentName = new ComponentName(context,ExampleAppWidgetProvider.class);
	appWidgetManager.updateAppWidget(componentName, remoteView);
}

其中songName存儲接收過來的歌曲id值,play_pause表示當前歌曲的播放狀態,根據當前的播放狀態加載不同的播放狀態圖片;

 

在理解了上面的更新RemoteView的部分以後,下面看看接收廣播的代碼:

public void onReceive(Context context, Intent intent) {
	String action = intent.getAction();
	Log.d("harvic", "action:"+action);
	
	if (intent.hasCategory(Intent.CATEGORY_ALTERNATIVE)) {
		…… //接收到的按鈕點擊廣播的處理部分
	}else if (MAIN_UPDATE_UI.equals(action)){
			int play_pause =  intent.getIntExtra(KEY_MAIN_ACTIVITY_UI_BTN, -1);
			int songid = intent.getIntExtra(KEY_MAIN_ACTIVITY_UI_TEXT, -1);
			switch (play_pause) {
			case VAL_UPDATE_UI_PLAY:
				pushUpdate(context, AppWidgetManager.getInstance(context), "current sond id:"+songid,true);
				break;
			case VAL_UPDATE_UI_PAUSE:
				pushUpdate(context, AppWidgetManager.getInstance(context), "current sond id:"+songid,false);
				break;
			default:
				break;
			}
			
		}
 
	super.onReceive(context, intent);
}

首先,我們通過獲取action值來判斷當前是不是MusicManageService傳過來的消息,然後得到傳過來當前的播放狀態和歌曲ID值,然後利用pushUpdate更新RemoteView;

四、附:相關問題

下面記錄一下,我在實際開發中遇到的問題,分享給大家

1、有關RemoteViews實例複用(絕對要每次新建RemoteView實例)

在實際項目中,大家可能會想到複用remoteView,即如果已經創建了就不再重新加載layout,而是重新綁定控件及數據


 !!!!!!!千萬不要這樣!!!!!!!一定要每次都要新建remoteView!!!!!這是血和淚的教訓!!!!!


 因爲:如果你在綁定數據時涉及圖片等大數據,remoteView不會每次清理,所以如果每次都使用同一個remoteView進行傳輸會因爲溢出而紿終無響應!!!! 你看着每次動作都通過updateAppWidget傳送出去了,但界面死活就是沒響應;而且重裝應用程序也不會有任何反應,只能重啓手機纔會重新有反應,也是醉了。
 主要原因在於:Binder data size limit is 512k,由於傳輸到appWidget進程中的Binder最大數據量是512K,並且RemoteView也不會每次清理, 所以如果每次都使用同一個RemoteView進行傳輸會因爲溢出而報錯.所以必須每次重新建一個RemoteView來傳輸!!!!!!

2、操作RemoteView中控件的方法

在RemoteView中的操作控件的方法非常有限,但我們的需求確是非常多樣的,所以怎樣才能像操作平常控件一樣多樣性的操作RemoteView呢,

舉例:
如果我們需要把widget中的一個view臨時隱藏,我們可以這樣調用:remoteviews.setInt(textviewid,"setVisibility",VIEW.INVISIBLE);
 

OK啦,終於寫完了,有點要累尿了,這部分涉及到的代碼量太大,我上面講的也不太詳細,大家匹配代碼再仔細琢磨琢磨一下吧。

參考文章:

1、《Android 之窗口小部件詳解--App Widget》  (初步入門級,寫的很好)

2、《Android桌面組件AppWidget講解》

3、《app widget 進入主客戶端代碼》  (講述了,點擊桌面widget如何進入主app的代碼)

4、《AppWidget基礎小結》

5、《Android 桌面組件【app widget】 進階項目--心情記錄器》

6、《Android Widget開發的相關技術點記錄》 (其中有:存儲widgetID,以防app崩潰後,無法更新widget的問題)

7、《android widget開發點滴》

8、《Android Appwidget 之按鈕事件》

9、《android 轉載 widget點擊事件》

10、《 Android基礎之AppWidgetProvider》

11、《android之widget詳解》

 

 

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章