前言:这将是这个系列的最后一篇了,我写这几篇文章也是累的快不行了,再写就真的要吐了,言归正转,前面三篇已经把widget中涉及到的基本知识基本上讲完了,今天我们就做一个小例子,看看桌面音乐播放器widget是怎么做出来的。
相关文章:
1、《桌面widget详解(一)——基本demo构建》
2、《桌面widget详解(二)—— 基本的与service通信》
3、《桌面widget详解(三)——桌面widget中的控件交互方法》
4、《桌面widget详解(四)——桌面音乐播放器(实战)》
先看看本篇的最终效果:
这一篇是建立在《桌面widget详解(三)——桌面widget中的控件交互方法》和《桌面widget详解(二)—— 基本的与service通信》 的基础之上,我们这里需要用到service通信的基础知识,有关按钮Service播放歌曲的东东已经在《桌面widget详解(二)—— 基本的与service通信》 里讲过了,这里我只是给大家简单回忆一下代码,不会再细讲,如果有不明白的地方翻翻这两篇博客,这篇以实际实现为主。
首先由于我们要与按钮相交互,所以在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时,会调用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); } }
到这里,效果是这样的:(在模拟器上点击看起来没有任何反应,其实已经在播放歌曲了,上一首,下一首,播放、暂停功能都是可用的)
首先,我们要在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啦,到这里发送就结束了,下面就是接收了。
首先在接收广播之前要注册,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;
下面记录一下,我在实际开发中遇到的问题,分享给大家
在实际项目中,大家可能会想到复用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);
更多看这里:《Android App Widget中如何调用RemoteView中的函数》
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详解》
源码包含两部分内容:
1、《初步实现》:这是第二部分结束时对应的源码,还没有加上service反向通知更新RemoteView代码之前的代码;
2、《完整实现》:这是本篇的完整实现代码;
如果我的文章有帮到你,记得关注哦
源码地址:http://download.csdn.net/detail/harvic880925/8232951
请大家尊重原创者版权,转载请标明出处:http://blog.csdn.net/harvic880925/article/details/41754477 谢谢!