APP小挂件指的是一个小型的应用View控件,他可以嵌入到其他应用程序中(比如说桌面),并接受定期的更新。你可以通过Widget Provider来自己发布一个。一个可以持有其他App小挂件的应用组件叫做AppWidget host。
下图表示一个音乐应用的挂件;
此文章将会描述怎么使用AppWidget provider去发布一个应用挂件。创建挂件牵涉到的类是:
android.appwidget.AppWidgetHost |
一、基本的描述
要创建一个App挂件,你需要做到下面的工作。
描述App挂件的元数据,比如说:挂件的布局,更新频率,和AppWidgetProvider类。你需在XML文件中定义
定义一个基于广播事件的允许动态改变挂件界面的方法,通过他,当你的应用挂件更新、变得可用、变得不可用、被删除的事件你都可以收到。
在XML文件中定义一个挂件的初始布局文件。
此外,你可以实现一个挂件配置 Activity,他是一个可选的Activity,当用户要添加你的挂件时,他会启动用于给用户设置和更改挂件的设置。
下面将介绍怎么去设置一个这样的组件
在Manifest中申明一个挂件
首先需要在Manifest中声明AppWidgetProvider类。比如说:
添加AppWidgetProviderInfo元数据
AppWidgetProviderInfo 定义了一个挂件的基本品质,比如说:最小布局尺寸、他初始化的布局资源、更新挂件的频率、和configurationActivity在创建的时候加载。在XML文件中定义AppWidgetProviderInfo 对象只需要使用
下面是
【注意:为了使你的挂件能方便的跨设备,你最好将你的最小尺寸设置为低于4 X 4】
【如果设备正在休眠,但是到了刷新你挂件的时间,设备将会被激活,然后去执行更新操作。如果你一个多小时的刷新一次可能不会对电池造成什么影响,但是,如果你刷新频率比较高,或者可能不需要挂件在休眠的时候刷新,那么你可以基于alarm 刷新,他将不会激活设备。实现方式:使用你的AppWidgetProvider需要接受的Intent设置一个alarm(使用AlarmManager类),将alarm的类型设置为ELAPSED_REALTIME 或者RTC,这样只有在设备唤醒的情况下才通知你的挂件刷新,最后你需要将你的 updatePeriodMillis 设置为 0 】
参考类android.appwidget.AppWidgetProviderInfo以了解更多
创建挂件的布局文件
你必须在XML文件中定义个初始化的布局文件,保存在你工程的res/layout/目录下。你可以使用下面的View对象来设计你的布局。但是,在你设计你的布局之前,你最好先阅读并理解挂件的设计指导方针 AppWidget Design Guidelines:(见右侧)
创建挂件非常简单,如果你熟悉Layout的话。然而,你必须知道的是,挂件的布局使基于RemoteViews(android.widget.RemoteViews)的,不是所有的布局和view控件他都支持。
他们支持的layout如下:
他们支持的控件如下:(他们的衍生品不支持)
(他们都在android.widget包下。)
RemoteView也支持使用ViewStub(android.view.ViewStub)进行运行时懒加载。
为挂件添加margin
我们的控件一般不需要延生到边缘,也不要和其他挂件挤在一起了。因此我们需要在边界上加上一个的空白。那就是“Margin”或者“padding”
在android4.0以后,系统将在挂件之间自动加上padding。我们不需要自己手动的添加。如果低于4.0的,我们自己加上就行了。
例子如下:
…
资源文件如下:
res/values/dimens.xml文件中这样定义:
8dp
res/values-v14/dimens.xml文件中这样定义(由于系统默认给我们加了,我们再添加)
0dp
另外一个可选操作则是:为有margin的布局做一个.9.png图片,为没有margin的布局做一个.9.png图片。
使用AppWidgetProvider 类
类AppWidgetProvider继承了BroadCastReceiver类,方便其接受来自挂件广播的事件。AppWidgetProvider 只接受与挂件相关的广播事件,比如说:挂件更新了、被删除了、挂件变得可用、挂件不可用等。当这些事件发生了,下面的对应方法将会被调用:
onUpdate (Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds)
每当过了updatePeriodMillis 设置的时间后,此方法被调用来更新挂件。当挂件被添加时,此方法也会被调用。因此,他在挂件被创建的时候会被执行,例如:如果需要,为View定义一个事件handler并启动一个临时Service。但是,如果你创建了一个configuration Activity,创建挂件的时候,此方法将不会被调用 。当configuration Activity执行完毕后,此方法将被调用(进行第一次更新)
onAppWidgetOptionsChanged (Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions)
此方法在被第一次放置的时候,或者挂件大小被改变后被调用。你可以使用此回调函数去基于挂件的尺码范围去展示或者隐藏内容,你可以通过调用getAppWidgetOptions (intappWidgetId)方法去获取到尺码范围,他的返回值是一个Bundle类型的对象,包含了下面的内容:
包含了当前挂件的最小宽度尺寸(使用dp为单位)。
包含了当前挂件的最小高度尺寸(使用dp为单位)。
包含了当前挂件的最大宽度尺寸(使用dp为单位)。
包含了当前挂件的最大高度尺寸(使用dp为单位)。
【需要注意的是:此API是在16引出的(android4.1以上)。】
onDeleted (Context context, int[] appWidgetIds)
此方法将在挂件被删除的时候调用
onEnabled (Context context)
在挂件对象实例第一次创建的时候调用。比如说:用户添加了两个你的实例,此方法也只被调用一次。有的事情只需要执行一次的:例如:创建数据库。可以再次进行。
onDisabled (Context context)
此方法将在你所有的挂件实例中的最后对象被删除后调用。用来为onEnable()做一些清除工作。
onReceive (Context context, Intent intent)
此方法是为broadcast所调用,并且优先于上面几个回调方法。一般你不需要实现此方法。因为APPWidgetProvider默认实现了这个方法。
最重要的APPWidgetprovider的回调方法是:onUpdate(),因为他在每次添加到宿主屏幕的时候都会调用(除非你使用了configuration Activity)。如果你的挂件需要接受一些用户的交互事件,那你需要在此会掉方法中注册事件的handler,如果你的挂件不用创建临时的文件或者数据库,或者执行其他的清理工作,那么onUpdate()方法也许就是你唯一需要实现的方法了。比如说:如果你想一个拥有button的挂件,当他被点击的时候你就启动一个Activity,那么你可以这么做。【当然,如果你不想启动Activity,你想出发某个时间,你可以使用PendingIntent来发送广播,广播接收器收到后,通过Intent的Action来执行不同的代码】
public class ExampleAppWidgetProvider extends AppWidgetProvider {
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
final int N = appWidgetIds.length;
// Perform thisloop procedure for each App Widget that belongs to this provider
for (int i=0; i
上面的APPWidgetProvider就只是定义了onUpdate()方法,此方法用来定义一个启动Activity的PendingIntent,然后使用setOnclickPendingIntent(int,PendingIntent)绑定到挂件的Button上。
注意:上面的代码是在一个循环中,他们迭代 appWidgetIds数组的每个实例,每个实例都是一个挂件的ID,通过这种方式,如果用户创建了多个挂件实例(例如在主屏幕创建相同的两个挂件),他们全部都将更新,因为,他们使用的updatePeriodMillis使用的是同一个实例。比如说:如果刷新频率被设置为两个小时,同时,桌面上有两个相同的挂件,第二个挂件在第一个挂件出现的一个小时后添加的。他们最终的结果就是,一旦到了第一个挂件刷新时(也就是第一个挂件添加两个小时后),第二个挂件也一起刷新,但是到了第二个挂件刷新时,将不起任何作用(直接忽视了第二个挂件的周期,以第一个为准)
注意: |
由于APPWidgetProvider是扩展自BroadcastReceiver,因此,不能保证在回调方法返回后你的进程将继续保持运行(BroadcastReceiver的生命周期),如果你需要做一些时间比较长的任务,你最好在onUpdate()中开启一个Service,然后在Service中更新界面。例子见右边--》 |
接收Widget广播Intents
使用APPWidgetProvider仅仅是个便利的做法,如果你喜欢,你也可以将挂件直接继承BroadcastReceiver,或者复写APPWidgetProvider的onRecerve(Context,Intent)方法。有几个Intent你需要注意的:
创建挂件Configuration Activity
如果你想让用户在新添加你的挂件前先做一些设置。那么你可以创建一个Configuration Activity,此Activity将会在挂件添加前启动,以便于用户为挂件做设置,比如说:挂件颜色、挂件大小、挂件更新频率等。
Configuration Activity应该像一般的Activity一样在manifest中申明,只是需要加上
同时,我们需要在APPWidgetProviderInfo的XML文件中的android:configure 中添加上此Activity。比如:
上面的内容就是你启动ConfigurationActivity需要做的所有内容。剩下的需要你做的事情就在这个实际的Activity中,当你在实现Activity的时候,下面有两点重要的事情你需要记住:
从Configuration Activity更新挂件
当挂件使用了Configuration Activity,那么当Configuration Activity处理完毕后,第一次更新挂件将是Configuration Activity的职责。当然,你可以直接从APPWidgetManager请求更新。
下面有一些关于更新挂件和关闭ConfigurationActivity的程序的总结
1、首先通过Intent得到挂件的ID
Intent intent = getIntent();
Bundle extras =intent.getExtras();
if (extras !=null) {
mAppWidgetId = extras.getInt(
AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
}
2、执行你的Configuration Activity交互(一般是用户设置等)
3、当设置完毕,通过getInstance(Context)获取到APPWidgetManager对象
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
4、通过拥有布局的RemoteViews对象调用updateAPPWidget(int,RemoteViews)来更新挂件。
RemoteViews views = newRemoteViews(context.getPackageName(),R.layout.example_appwidget);
appWidgetManager.updateAppWidget(mAppWidgetId, views);
5、最后返回Intent、关闭Activity
Intent resultValue = new Intent();
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,mAppWidgetId);
setResult(RESULT_OK, resultValue);
finish();
小知识:
当你的ConfigurationActivity被创建时,如果你在最后返回的值是:RESULT_CANCELED (或者用户并没有与你的Configuration Activity有太多交互,而是直接结束了此Activity,也会返回这样的值)。那么挂件会接受到这样的事件,最终,他将不会被添加到宿主中。
设置预览图
Android 3.0引进了previewImage字段。他可以定义一个挂件的预览图,方便用户直接筛选,如果此字段没定义,那么系统将使用挂件的图片作为预览。
具体定义如下:
【为了帮助你为你的挂件创建挂件预览图,安卓模拟器包含了一个应用,叫做“Widget Preview”,启动这个应用,选择你的挂件,设置好后,“Widget Preview”将会给你生成预览图,这时候你保存下来,放到你的工程中做为资源图就好了】
带集合物件的挂件
Android 3.0引入了带集合物件的挂件,这些种类的挂件使用RemoteViewsService来将后台远程的数据展示出来,比如:来自content Provider的数据。使用RemoteViewsService提供数据,使用下面的View控件展示内容,我们称这样的View为: “collection views”
在一个垂直方向滚动显示条目的列表。
和ListView相似,只是在水平方向展示的条目多于1条(ListView为1条)。
类似于将很多卡牌堆叠在一起的View(有点像名片盒),
一个由ViewAnimator做后台支撑的adapter,他可以在两个或多个View之间动画。但是同一时间只能有一个child可以被显示。
上面有说道:这些collectionView将显示来自其他地方的数据。这就意味着,他们要使用一个Adapter去绑定他们的数据到UI上,一个适配器绑定了一些条目,而这些条目是将一堆数据分别存放到分别的View对象中。
由于这些collectionview是基于adapter的,所有安卓框架必须要包含额外的建筑样式去支持这样的挂件。在挂件的上下文中,adapter被RemoteViewsFactory所取代,它仅仅对Adapter做了很小的包装,当其中一个条目被请求,那么RemoteViewsFactory将会创建并返回此条目(作为一个RemoteViews对象返回),如果你要包含collection View到你的挂件中,你必须要实现两个类:android.widget.RemoteViewsService和
android.widget.RemoteViewsService.RemoteViewsFactory |
RemoteViewsService是一个允许适配器“远程”请求RemoteViews对象的服务
RemoteViewsFactory 是一个在collection View和其对应的数据的一个接口,
public class StackWidgetService extends RemoteViewsService {
@Override
public RemoteViewsFactory onGetViewFactory(Intent intent) {
return new StackRemoteViewsFactory(this.getApplicationContext(), intent);
}
}
class StackRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {
//... include adapter-likemethods here. See the StackView Widget sample.
}
二、示例应用
此例子由10堆叠起来的View组成,展示的名称由0!到9!,此例子有下面一些行为:
public void onUpdate(Context context, AppWidgetManager appWidgetManager,
int[] appWidgetIds) {
// update each ofthe app widgets with the remote adapter
for (int i = 0; i < appWidgetIds.length; ++i) {
//Set up the intent that starts the StackViewService, which will
//provide the views for this collection.
Intent intent = new Intent(context, StackWidgetService.class);
//Add the app widget ID to the intent extras.
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
//Instantiate the RemoteViews object for the app widget layout.
RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
//Set up the RemoteViews object to use a RemoteViews adapter.
//This adapter connects
//to a RemoteViewsService through the specified intent.
//This is how you populate the data.
rv.setRemoteAdapter(appWidgetIds[i], R.id.stack_view, intent);
//The empty view is displayed when the collection has no items.
//It should be in the same layout used to instantiate the RemoteViews
//object above.
rv.setEmptyView(R.id.stack_view, R.id.empty_view);
//
//Do additional processing specific to this app widget...
//
appWidgetManager.updateAppWidget(appWidgetIds[i], rv);
}
super.onUpdate(context, appWidgetManager, appWidgetIds);
}
class StackRemoteViewsFactory implements
RemoteViewsService.RemoteViewsFactory {
private staticfinal intmCount =10;
private List mWidgetItems = new ArrayList();
private Context mContext;
private int mAppWidgetId;
public StackRemoteViewsFactory(Context context, Intent intent) {
mContext =context;
mAppWidgetId =intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
}
public void onCreate() {
//In onCreate() you setup any connections / cursors to your data source. Heavylifting,
//for example downloading or creating content etc, should be deferred toonDataSetChanged()
//or getViewAt(). Taking more than 20 seconds in this call will result in an ANR.
for(int i = 0; i < mCount; i++) {
mWidgetItems.add(new WidgetItem(i + "!"));
}
...
}
...
public RemoteViews getViewAt(int position){
// Construct aremote views item based on the app widget item XML file,
// and set thetext based on the position.
RemoteViews rv = new RemoteViews(mContext.getPackageName(), R.layout.widget_item);
rv.setTextViewText(R.id.widget_item, mWidgetItems.get(position).text);
...
// Return theremote views object.
return rv;
}
public class StackWidgetProvider extends AppWidgetProvider {
public staticfinal String TOAST_ACTION = "com.example.android.stackwidget.TOAST_ACTION";
public staticfinal String EXTRA_ITEM = "com.example.android.stackwidget.EXTRA_ITEM";
...
// Called whenthe BroadcastReceiver receives an Intent broadcast.
// Checks to seewhether the intent's action is TOAST_ACTION. If it is, the app widget
// displays aToast message for the current item.
@Override
public void onReceive(Context context, Intent intent) {
AppWidgetManager mgr = AppWidgetManager.getInstance(context);
if(intent.getAction().equals(TOAST_ACTION)) {
int appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
int viewIndex = intent.getIntExtra(EXTRA_ITEM, 0);
Toast.makeText(context, "Touched view " + viewIndex, Toast.LENGTH_SHORT).show();
}
super.onReceive(context, intent);
}
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
//update each of the app widgets with the remote adapter
for(int i = 0; i < appWidgetIds.length; ++i) {
// Sets up the intent that points to the StackViewServicethat will
// provide the views for this collection.
Intent intent = new Intent(context, StackWidgetService.class);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);
// When intents are compared, the extras are ignored, so weneed to embed the extras
// into the data so that the extras will not be ignored.
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
rv.setRemoteAdapter(appWidgetIds[i], R.id.stack_view, intent);
// The empty view is displayed when the collection has noitems. It should be a sibling
// of the collection view.
rv.setEmptyView(R.id.stack_view, R.id.empty_view);
// This section makes it possible for items to haveindividualized behavior.
// It does this by setting up a pending intent template.Individuals items of a collection
// cannot set up their own pending intents. Instead, thecollection as a whole sets
// up a pending intent template, and the individual itemsset a fillInIntent
// to create unique behavior on an item-by-item basis.
Intent toastIntent = new Intent(context, StackWidgetProvider.class);
// Set the action for the intent.
// When the user touches a particular view, it will havethe effect of
// broadcasting TOAST_ACTION.
toastIntent.setAction(StackWidgetProvider.TOAST_ACTION);
toastIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
PendingIntent toastPendingIntent = PendingIntent.getBroadcast(context, 0, toastIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
rv.setPendingIntentTemplate(R.id.stack_view, toastPendingIntent);
appWidgetManager.updateAppWidget(appWidgetIds[i], rv);
}
super.onUpdate(context, appWidgetManager, appWidgetIds);
}
}
class StackRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {
private staticfinal intmCount =10;
private List mWidgetItems = new ArrayList();
private Context mContext;
private int mAppWidgetId;
public StackRemoteViewsFactory(Context context, Intent intent) {
mContext =context;
mAppWidgetId =intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
}
// Initialize thedata set.
publicvoidonCreate(){
// In onCreate() you set up any connections / cursors toyour data source. Heavy lifting,
// for example downloading or creating content etc, shouldbe deferred to onDataSetChanged()
// or getViewAt(). Taking more than 20 seconds in this callwill result in an ANR.
for (int i = 0; i < mCount; i++) {
mWidgetItems.add(new WidgetItem(i + "!"));
}
...
}
...
//Given the position (index) of a WidgetItem in the array, use the item's textvalue in
//combination with the app widget item XML file to construct a RemoteViewsobject.
publicRemoteViews getViewAt(int position) {
// position will always range from 0 to getCount() - 1.
// Construct a RemoteViews item based on the app widgetitem XML file, and set the
// text based on the position.
RemoteViews rv = new RemoteViews(mContext.getPackageName(), R.layout.widget_item);
rv.setTextViewText(R.id.widget_item, mWidgetItems.get(position).text);
// Next, set a fill-intent, which will be used to fill inthe pending intent template
// that is set on the collection view inStackWidgetProvider.
Bundle extras = new Bundle();
extras.putInt(StackWidgetProvider.EXTRA_ITEM, position);
Intent fillInIntent = new Intent();
fillInIntent.putExtras(extras);
// Make it possible to distinguish the individual on-click
// action of a given item
rv.setOnClickFillInIntent(R.id.widget_item, fillInIntent);
...
// Return the RemoteViews object.
return rv;
}
...
}