先看看本篇的最終效果:
一、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》 (初步入門級,寫的很好)
3、《app widget 進入主客戶端代碼》 (講述了,點擊桌面widget如何進入主app的代碼)
5、《Android 桌面組件【app widget】 進階項目--心情記錄器》
6、《Android Widget開發的相關技術點記錄》 (其中有:存儲widgetID,以防app崩潰後,無法更新widget的問題)
10、《 Android基礎之AppWidgetProvider》