Android App Widget 开发
2017-09-07 13:43
351 查看
概述
App Widget是应用程序窗口小部件(Widget),是微型的应用程序视图,它可以被嵌入到其它应用程序中(比如桌面)并接收周期性的更新。你可以通过一个App Widget Provider来发布一个Widget。Widget布局
appwidget-provider标签
这个东西是用来定义桌面widget的大小,初始状态等等信息的,它的位置应该放在res/xml文件夹下,具体的xml参数如下:android:minWidth : 最小宽度 android:minHeight : 最小高度 android:updatePeriodMillis : 更新widget的时间间隔(ms) android:previewImage : 预览图片 android:resizeMode : widget可以被拉伸的方向。horizontal表示可以水平拉伸,vertical表示可以竖直拉伸 android:widgetCategory : widget可以被显示的位置home_screen表示可以将widget添加到桌面,keyguard表示widget可以被添加到锁屏界面 android:initialLayout : 加载到桌面时对应的布局文件 android:initialKeyguardLayout : 加载到锁屏界面时对应的布局文件
我自己的配置文件如下:res/xml/my_appwidget_info.xml
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:initialLayout="@layout/my_appwidget" android:minHeight="60dp" android:minWidth="180dp" android:previewImage="@mipmap/preview" android:resizeMode="horizontal|vertical" android:widgetCategory="home_screen|keyguard"> </appwidget-provider>
布局文件:res/layout/my_appwidget.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#33000000" android:orientation="vertical"> <TextView android:id="@+id/song_name" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="5dip" android:gravity="center_horizontal" android:textSize="16sp" android:text="song name"/> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="5dp" android:gravity="center_horizontal" android:orientation="horizontal" > <ImageView android:id="@+id/prev_song" android:layout_width="40dp" android:layout_height="40dp" android:layout_marginRight="8dip" android:background="@mipmap/car_musiccard_up" /> <ImageView android:id="@+id/play_pause" android:layout_width="40dp" android:layout_height="40dp" android:layout_marginRight="8dip" android:src="@mipmap/car_musiccard_play" /> <ImageView android:id="@+id/next_song" android:layout_width="40dp" android:layout_height="40dp" android:background="@mipmap/car_musiccard_down" /> </LinearLayout> </LinearLayout>
注意:在构造Widget布局时,App Widget支持的布局和控件非常有限,有如下几个:
//App Widget支持的布局: FrameLayout、LinearLayout、RelativeLayout、GridLayout //App Widget支持的控件: AnalogClock、Button、Chronometer、ImageButton、ImageView、ProgressBar、TextView、ViewFlipper、ListView、GridView、StackView、AdapterViewFlipper
除此之外的所有控件(包括自定义控件)都无法显示,无法显示时,添加出来的widget会显示“加载布局出错”。
AppWidgetProvider类
上面我们通过appwidget-provider标签就可以得到初始化的布局,视图等,但我们的widget要实时更新怎么办,要响应用户操作怎么办,这就需要额外的类来辅助处理了,这个类就是AppWidgetProvider。由于AppWidgetProvider要接收到当前widget的状态(是否被添加,是否被删除等),所以要接收通知,必然是继承自BroadcastReceiver。
AppWidgetProvider中的广播处理函数如下:(根据不同的使用情况,重写不同的函数)
onUpdate():
onUpdate()在主线程中执行,如果处理需要花费时间多于10秒,处理应在service中完成。3种情况下会调用onUpdate():
(1)在时间间隔到时调用,时间间隔在widget定义的android:updatePeriodMillis中设置;
(2)用户拖拽到主页,widget实例生成。
(3)机器重启,实例在主页上显示
onDeleted(Context, int[]):
当 widget 被删除时被触发。
onEnabled(Context):
当第1个 widget 的实例被创建时触发。也就是说,如果用户对同一个 widget 增加了两次(两个实例),那么onEnabled()只会在第一次增加widget时触发。
onDisabled(Context):
当最后1个 widget 的实例被删除时触发。
onReceive(Context, Intent):
在接收到广播时调用。
我们可以先写个简单的Provider类,后面根据需求慢慢丰富:
public class MyAppWidgetProvider extends AppWidgetProvider { // widget更新时调用 @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { Log.d("hx", "onUpdate: appWidgetIds.length=" + appWidgetIds.length + " appWidgetIds[0]=" + appWidgetIds[0]); super.onUpdate(context, appWidgetManager, appWidgetIds); } // widget被删除时调用 @Override public void onDeleted(Context context, int[] appWidgetIds) { Log.d("hx", "onDeleted: appWidgetIds.length=" + appWidgetIds.length + " appWidgetIds[0]=" + appWidgetIds[0]); super.onDeleted(context, appWidgetIds); } // 最后一个widget被删除时调用 @Override public void onDisabled(Context context) { Log.d("hx", "onDisabled"); super.onDisabled(context); } // 第一个widget被创建时调用 @Override public void onEnabled(Context context) { Log.d("hx", "onEnabled"); super.onEnabled(context); } // 接收广播的回调函数 @Override public void onReceive(Context context, Intent intent) { Log.d("hx", "onReceive"); super.onReceive(context, intent); } }
这里不是继承接口,这几个函数并不需要全部重写,要用到哪个就重写哪个。
注册MyAppWidgetProvider
至此我们的Widget还不能添加到桌面,还需要最后一步。前面我们讲到AppWidgetProvider派生自BroadcastReciver,所以要提前注册,BroadcastReciver的注册有两种方法,静态注册和动态注册,因为这里要接收来自系统的消息,而且在程序启动时就开始自动监听,所以需要静态注册。<!-- 声明widget对应的AppWidgetProvider --> <receiver android:name=".MyAppWidgetProvider" > <intent-filter> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> </intent-filter> <meta-data android:name="android.appwidget.provider" android:resource="@xml/my_appwidget_info" /> </receiver>
(1)接收的action定义为:”android.appwidget.action.APPWIDGET_UPDATE”这表明接收系统发来的有关这个app的所有widget的消息(主要是增加、删除)。
(2)<.meta-data> 指定了 AppWidgetProviderInfo 对应的资源文件:
android:name – 指定metadata名,指定为android.appwidget.provider表示这个data中的数据是AppWidgetProviderInfo 类型的
android:resource – 指定 AppWidgetProviderInfo 对应的资源路径。即,xml/my_appwidget_info.xml。
现在可以把Widget添加到桌面啦,效果图如下:
Widget 交互
上面我们实现了Widget的界面,但此时Widget还不能更新内容和响应用户操作,接下来看看Widget的交互。发送广播与按钮事件绑定
因为appwidget运行的进程和我们创建的应用不在一个进程中,所以我们也就不能像平常引用控件那样来获得控件的实例。这时候,我们就要靠RemoteViews,直译成中文应该是远程视图, 也就是说通过这个东西我们能够获得不在同一进程中的对象。先看一下Demo代码,后面慢慢分析:
@Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { /**构造RemoteViews实例*/ RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.my_appwidget); //添加点击事件 remoteViews.setOnClickPendingIntent(R.id.prev_song, getPendingIntent(context, R.id.prev_song)); remoteViews.setOnClickPendingIntent(R.id.play_pause, getPendingIntent(context, R.id.play_pause)); remoteViews.setOnClickPendingIntent(R.id.next_song, getPendingIntent(context, R.id.next_song)); // 更新Appwidget appWidgetManager.updateAppWidget(appWidgetIds, remoteViews); super.onUpdate(context, appWidgetManager, appWidgetIds); } private PendingIntent getPendingIntent(Context context,int resID){ Intent intent = new Intent(); //注意这个intent构造的是显式intent,直接将这个广播发送给MyAppWidgetProvider,使用Action的方式接收不到 intent.setClass(context, MyAppWidgetProvider.class); intent.setData(Uri.parse("hx:" + resID)); PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0,intent, PendingIntent.FLAG_UPDATE_CURRENT); return pendingIntent; }
首先创建了一个remoteViews实例,然后将其中的按钮与点击事件绑定,后面的参数是一个PendingIntent。下面看看PendingIntent又是个什么。
intent意思是意图,pending表示即将发生或来临的事情。
PendingIntent这个类用于处理即将发生的事情。比如在通知Notification中用于跳转页面,但不是马上跳转。所以我们可以将它理解成一个封装成消息的intent。即这个intent并不是立即start,而是像消息一样被发送出去,等接收方接到以后,再分析里面的内容。
可以看到,PendingIntent是Intent的封装,在构造PendingIntent前,需要先构造一个Intent(注意这里是显式intent,直接将这个广播发送给MyAppWidgetProvider),并可以利用Intent的属性传进去action,Extra等,同样,在接收时,对方依然是接收Intent,而不是接收PaddingIntent。这个问题,我们后面可以看到。
PendingIntent.getBroadcast(context, 0, intent, 0);
指从系统中获取一个用于可以发送BroadcastReceiver广播的PendingIntent对象。所以流程就是我们点击控件,就会发出一个广播,广播携带了Intent,接收方(onReceive)通过Intent判断我们点击的是哪个控件,再作出相应的处理。
这里有一点需要注意:
我们intent传递数据使用的是setData
intent.setData(Uri.parse("hx:" + resID));
那么能不能用putExtra呢?
intent.putExtra("id", resID);
实践发现这种方式取到的id值永远只有一个,即使点击不同的控件也不会变。究其原因是因为没有创建新的PendingIntent,仍然是复用的前一个。如果要创建两个不同的PendingIntent,而不要系统替换前一个,不要仅仅在PutExtra()中包含不同的内容,因为在Extra的不同,并不会用来识别两个不同的PendingInent,要看两个PendingIntent是否相同,可以利用filterEquals (Intent other)来判断两个Intent是否相同,即除了Extra域的任何不同都会标识为两个不同的Intent。所以用setData就标识了两个不同的Intent。还有另外一种方式,使用不同的RequestCode也可以构建新的PendingIntent。
总结一下,两种方式:
利用filterEquals (Intent other)里的那几个域的不同来构造不同的Intent
在构建PendingIntent时使用不同的RequestCode
创建remoteViews实例后,还有一句:
appWidgetManager.updateAppWidget(appWidgetIds, remoteViews);
就是利用updateAppWidget()将构造的RemoteView更新指定的widget界面。注意这里有个appWidgetIds,这个参数是通过OnUpdate()传过来的,它是一个int数组,里面存储了用户所创建的所有widget的ID值。更新的时候也是通过widget的ID值,一个个更新的。
接收广播
由于我们在创建广播的Intent时,使用的显示Intent,所以我们的广播不需要注册就会发到这们这个类(MyAppWidgetProvider.java)里面。在接收到广播后,我们先判断Intent中是不是包含data,因为我们在发送广播时放data中塞了数据(控件的ID),所以只要data中有数据就可以判定是用户点击控件发来的广播。然后同样利用RemoteView修改textView的文字,代码如下:
@Override public void onReceive(Context context, Intent intent) { Uri data = intent.getData(); int resID = -1; if(data != null) { resID = Integer.parseInt(data.getSchemeSpecificPart()); } /**通过远程对象设置文字*/ RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.my_appwidget); switch (resID) { case R.id.prev_song: remoteViews.setTextViewText(R.id.song_name,"prev_song click"); break; case R.id.play_pause: remoteViews.setTextViewText(R.id.song_name,"play_pause click"); break; case R.id.next_song: remoteViews.setTextViewText(R.id.song_name,"next_song click"); break; } // 获得appwidget管理实例,用于管理appwidget以便进行更新操作 AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); // 相当于获得所有本程序创建的appwidget ComponentName componentName = new ComponentName(context, MyAppWidgetProvider.class); // 更新appwidget appWidgetManager.updateAppWidget(componentName, remoteViews); super.onReceive(context, intent); }
其实RemoteView中的操作控件的方法非常有限,可以看这里:Android App Widget中如何调用RemoteView中的函数。
两个地方需要注意:
(1)在OnUpdate中,我们更新界面是通过传过来的widget的id数组来更新所有widget的。而这里是通过获取ComponentName来更新的。其实这里还有另一种实现方式,即我们可以把OnUpdate中传过来的appWidgetIds保存起来,在这里同样使用OnUpdate中的appWidgetManager.updateAppWidget(appWidgetIds, remoteViews);来更新。但我更推荐这里利用ComponentName的这种方式来更新,因为当我们的应用程序进程被杀掉后再起来的时候,会赋予新的appWidgetIds,使用ComponentName这种更新方式还是可以继续响应的,而利用保存appWidgetIds的方式是不会响应的。
(2)在实际项目中,大家可能会想到复用remoteView,即如果已经创建了就不再重新加载layout,而是重新绑定控件及数据,千万不要这样!!!
如果你在绑定数据时涉及图片等大数据,remoteView不会每次清理,所以如果每次都使用同一个remoteView进行传输会因为溢出而绐终无响应! 你看着每次动作都通过updateAppWidget传送出去了,但界面死活就是没响应;而且重装应用程序也不会有任何反应,只能重启手机才会重新有反应,也是醉了。
主要原因在于:Binder data size limit is 512k,由于传输到appWidget进程中的Binder最大数据量是512K,并且RemoteView也不会每次清理, 所以如果每次都使用同一个RemoteView进行传输会因为溢出而报错.所以必须每次重新建一个RemoteView来传输!!!!!!
话不多说,看效果:
实战(音乐播放器)
首先需要一个播放音乐的Service,这里叫做MusicManageService:public class MusicManageService extends Service { private MediaPlayer mPlayer; private int mIndex = 4;// 从中间开始放 private int[] mArrayList = new int[9]; public static String ACTION_CONTROL_PLAY = "action_control_play"; 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; //接收MyAppWidgetProvider发过来的广播,控制播放器播放 private BroadcastReceiver receiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (ACTION_CONTROL_PLAY.equals(action)) { int widget_action = intent.getIntExtra(KEY_USR_ACTION, -1); switch (widget_action){ case ACTION_PRE: playPrev(context); Log.d("hx","action_prev"); break; case ACTION_PLAY_PAUSE: if (mPlayState) { pause(context); Log.d("hx","action_pause"); }else{ play(context); Log.d("hx","action_play"); } break; case ACTION_NEXT: playNext(context); Log.d("hx","action_next"); break; default: break; } } } }; @Override public IBinder onBind(Intent intent) { return null; } @Override public void onCreate() { super.onCreate(); IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(ACTION_CONTROL_PLAY); registerReceiver(receiver, intentFilter); initList(); mediaPlayerStart(); } private void mediaPlayerStart(){ mPlayer = new MediaPlayer(); mPlayer = MediaPlayer.create(getApplicationContext(), mArrayList[mIndex]); mPlayer.start(); mPlayState = true; updateUI(getApplicationContext(), MyAppWidgetProvider.UPDATE_UI_PLAY, mIndex); } 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(); unregisterReceiver(receiver); } /** * 播放下一首 * @param context */ public void playNext(Context context) { if (++mIndex > 8) { mIndex = 0; } mPlayState = true; playSong(context); updateUI(context, MyAppWidgetProvider.UPDATE_UI_PLAY,mIndex); } /** * 播放上一首 * * @param context */ public void playPrev(Context context) { if (--mIndex < 0) { mIndex = 8; } mPlayState = true; playSong(context); updateUI(context, MyAppWidgetProvider.UPDATE_UI_PLAY, mIndex); } /** * 继续播放 */ public void play(Context context) { mPlayState = true; mPlayer.start(); updateUI(context, MyAppWidgetProvider.UPDATE_UI_PLAY, mIndex); } /** * 暂停播放 * * @param context */ public void pause(Context context) { mPlayState = false; mPlayer.pause(); updateUI(context, MyAppWidgetProvider.UPDATE_UI_PAUSE, mIndex); } /** * 播放指定的歌曲 * * @param context */ private void playSong(Context context) { 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("hx","Unable to play audio queue do to exception: "+ e.getMessage(), e); } } //发送广播到MyAppWidgetProvider用来改变widget的显示 private void updateUI(Context context, int state, int songId) { Intent actionIntent = new Intent(MyAppWidgetProvider.ACTION_UPDATE_UI); actionIntent.putExtra(MyAppWidgetProvider.KEY_UI_PLAY_BTN, state); actionIntent.putExtra(MyAppWidgetProvider.KEY_UI_TEXT, songId); context.sendBroadcast(actionIntent); } }
Service写完之后记得在Manifest中注册,还有Service需要发广播给MyAppWidgetProvider通知更新UI,所以MyAppWidgetProvider除了响应系统的Action之外,还需要加上自己的Action:
<!-- 声明widget对应的AppWidgetProvider --> <receiver android:name=".MyAppWidgetProvider" > <intent-filter> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> <action android:name="action_update_ui" /> </intent-filter> <meta-data android:name="android.appwidget.provider" android:resource="@xml/my_appwidget_info" /> </receiver> <!-- 播放音乐service--> <service android:name=".MusicManageService"/>
然后就是重写MyAppWidgetProvider文件:
public class MyAppWidgetProvider extends AppWidgetProvider { private boolean mStop = true; public static String ACTION_UPDATE_UI = "action_update_ui"; //Action public static String KEY_UI_PLAY_BTN = "ui_play_btn_key"; //putExtra中传送当前播放状态的key public static String KEY_UI_TEXT = "ui_text_key"; //putExtra中传送TextView的key public static final int UPDATE_UI_PLAY = 1, UPDATE_UI_PAUSE =2;//当前歌曲的播放状态 // 更新所有的 widget private void updateRemoteViews(Context context,AppWidgetManager appWidgetManager, String songName, Boolean play_pause) { RemoteViews remoteView = new RemoteViews(context.getPackageName(),R.layout.my_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.mipmap.car_musiccard_pause); }else { remoteView.setImageViewResource(R.id.play_pause, R.mipmap.car_musiccard_play); } // 相当于获得所有本程序创建的appwidget ComponentName componentName = new ComponentName(context, MyAppWidgetProvider.class); appWidgetManager.updateAppWidget(componentName, remoteView); } @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { Log.d("hx", "onUpdate: appWidgetIds.length=" + appWidgetIds.length + " appWidgetIds[0]=" + appWidgetIds[0]); updateRemoteViews(context, appWidgetManager, "", false); } private PendingIntent getPendingIntent(Context context,int resID){ Intent intent = new Intent(); //注意这个intent构造的是显式intent,直接将这个广播发送给MyAppWidgetProvider intent.setClass(context, MyAppWidgetProvider.class); intent.addCategory(Intent.CATEGORY_ALTERNATIVE); intent.setData(Uri.parse("hx:" + resID)); PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0,intent,PendingIntent.FLAG_UPDATE_CURRENT); return pendingIntent; } // 接收广播的回调函数 @Override public void onReceive(Context context, Intent intent) { Log.d("hx", "onReceive"); String action = intent.getAction(); Log.d("hx", "action:"+action); if (intent.hasCategory(Intent.CATEGORY_ALTERNATIVE)) { //手势操作后系统发来的广播 Uri data = intent.getData(); int resID = Integer.parseInt(data.getSchemeSpecificPart()); switch (resID) { case R.id.play_pause: controlPlay(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: controlPlay(context, MusicManageService.ACTION_PRE); break; case R.id.next_song: controlPlay(context, MusicManageService.ACTION_NEXT); break; } } else if (ACTION_UPDATE_UI.equals(action)) { //MusicManageService发来的广播 int play_pause = intent.getIntExtra(KEY_UI_PLAY_BTN, -1); int songId = intent.getIntExtra(KEY_UI_TEXT, -1); switch (play_pause) { case UPDATE_UI_PLAY: updateRemoteViews(context, AppWidgetManager.getInstance(context), "current sond id:" + songId, true); break; case UPDATE_UI_PAUSE: updateRemoteViews(context, AppWidgetManager.getInstance(context), "current sond id:" + songId, false); break; default: break; } } super.onReceive(context, intent); } //发送广播到MusicManageService控制播放 private void controlPlay(Context context, int ACTION) { Intent actionIntent = new Intent(MusicManageService.ACTION_CONTROL_PLAY); actionIntent.putExtra(MusicManageService.KEY_USR_ACTION, ACTION); context.sendBroadcast(actionIntent); } }
Demo下载地址
相关文章推荐
- Android AppWidget 开发中PendingIntent传送数据丢失解决办法
- 【Android AppWidget开发流程】
- Android 开发之实时更新 App Widget
- Android Launcher开发(二)AppWidget(桌面小部件)解析
- 开发android的桌面小程序AppWidget
- Android开发历程_15(AppWidget的使用)
- android开发之App widget
- android之App Widget开发实例
- Android开发之App widget用法实例分析
- android开发之App widget
- Android AppWidget开发实战
- Android 开发之实时更新 App Widget
- Android开发入门之Appwidget用法分析
- android之App Widget开发实例
- Android之AppWidget 开发浅析
- Android开发之创建App Widget和更新Widget内容
- Android桌面组件App Widget开发三步走
- Android开发视频第二季之七:App Widget(3)
- android开发-app widget
- Android Launcher开发(二)AppWidget(桌面小部件)解析