App Widget是一种可以被放在其他应用中(如Launcher)并接收周期性更新的应用视图。这些视图在UI上就表现为Widget,并且你可以同App Widget Provider一起发布。
对于能够包含其他App Widget的应用程序组件,称为App Widget Host。
基本信息
要创建一个App Widget,你需要完成以下步骤:
lAppWidgetProviderInfo对象:它描述了App Widget的基本元素,比如说布局、更新频率、AppWidgetProvider类等。这些都是在xml文件中定义的。
l AppWidgetProvider类的实现:它定义了一些基本的方法以支持通过广播事件与App Widget交互。通过它,当App Widget被更新、启用、禁用以及删除时,你将收到相应的广播信息。
l View Layout:通过xml文件定义App Widget的初始视图。
另外,你还可以实现一个App Widget的配置Activity。当然,这不是强制的。
在AndroidManifest中声明一个App Widget
首先,声明AppWidgetProvider。
l android:name:使用android.appwidget.provider来标识AppWidgetProviderInfo。
l android:resource:标识AppWidgetProviderInfo的资源位置。
添加AppWidgetProviderInfo元数据
AppWidgetProviderInfo定义了一个App Widget的必要属性,例如最小布局范围、初始布局、更新频率、以及在创建时显示的配置Activity(可选)。
AppWidgetProviderInfo使用
l minWidth与minHeight属性表示了App Widget所需的最小布局区域。
默认的主屏中,App Widget要想确认其位置,需要通过基于网格的具有固定宽度和高度的单元。如果App Widget的最小宽度和高度无法匹配给定的单元,它将会自动扩展到最接近的单元大小。
由于主屏的布局方向是可变的,你应该考虑最坏的情况(每单元的宽和高都是74dp)。然而,为了防止在扩展时产生整数计算错误,你还需要减去2。因此,你可以用以下公式来计算最小宽度和高度(单位dp):(单元数量×74)-2。
同时,为了保证你的App Widget能够在各种设备上正常使用,它们的宽度和高度必须不超过4×4个单元。
lupdatePeriodMillis属性定义了App Widget框架调用AppWidgetProvider的onUpdate方法的频率。对于实际的更新,我们不建议采用该值进行实时处理。最好是越不频繁越好——为了保证电量,一小时不超过一次为好。当然,你也可以允许用户对更新频率进行设置。
注意,如果更新触发时设备正处于休眠状态,设备将唤醒以执行该操作。如果你的更新频率不超过一小时一次,这不会对电池的寿命产生多大的影响。但如果你需要更频繁地更新却又不想要在设备休眠时执行,那你可以使用定时器来执行更新。要达到这种目的,可以在AlarmManager中设置一个AppWidgetProvider能接收的Intent。将类型设为ELAPSED_REALTIME或RTC。由于AlarmManager只有当设备处于唤醒状态时才会被调用,我们只要设updatePeriodMillis为0即可。
linitialLayout属性标识了初始布局文件。
lconfigure属性定义了当用户添加App Widget时调用的Activity。(这是可选的)
lpreviewImage定义了App Widget的缩略图,当用户从widget列表中选择时,显示的就是这张图。如果没设置,用户将看见的是你的应用的默认图标。
lautoAdvanceViewId属性是在Android3.0引入的,用于标识需要被host(launcher)自动更新的widget的子视图。
l resizeMode属性标识了widget重新布局的规则。你可以使用该属性来让widget能够在水平、竖直、或两个方向上均可变化。可用的值包括horizontal、vertical、none。如果是想在两个方向上均能拉伸,可设置为horizontal|vertical,当然,需要Android3.1以上版本。
创建App Widget的布局
要创建你的App Widget的初始布局,你可以使用以下View对象。
创建布局不是很麻烦,重点是,你必须记住,这个布局是基于RemoteViews的,不是所有的布局类型与View都支持。
一个RemoteViews对象可以支持以下布局类:
FrameLayout
LinearLayout
RelativeLayout
以及一下widget类
AnalogClock
Button
Chronometer
ImageButton
ImageView
ProgressBar
TextView
ViewFlipper
注:这些类的子类是不支持的。
为App Widget添加边距
作为一个widget,它不应该扩展到屏幕的边缘,同时在视觉上,不应该与其它widget相混淆。因此,你需要为你的widget在四个方向上添加边距。
在Android4.0中,所有的widget都自动添加了内边距,并且提供了更好的对齐方案。要利用这种优势,建议设置应用的targetSdkVersion到14或更高。
为了支持以前的设备,你也可以为早些的平台写一个包含边距的布局,而对于Android4.0以上的,则不设置:
1. 设置targetSdkVersion到14或更高
2. 创建一个布局:
…
1. 创建两个dimension资源,res/values为老的平台,res/values-v14为4.0平台:
res/values/dimens.xml
15dp
res/values-v14/dimens.xml
0dp
还有一种做法是在9-patch的背景图里设置边距,并且为不同的版本提供不同的背景图。
使用AppWidgetProvider类
AppWidgetProvider类继承了BroadcastReceiver,它只监听与App Widget有关的广播事件,如更新、删除、启用、禁用。当这些事件发生时,AppWidgetProvider将接收到以下事件:
l onUpdate()
由updatePeriodMills定义的时间间隔触发。当然,添加Widget的时候也会,因此,应该在此处执行一些必要的配置,如定义View的事件处理handler,有必要的话,还会启动一个临时的Service。然而,如果你声明了一个配置Activity,该方法将不会在此时被调用。
l onDelete(Context, int[])
当App Widget从host中移除时会被调用。
l onEnabled(Context)
当App Widget的实例被第一次创建时,该方法将被调用。如果你建了两个实例,那该方法也只会被调用一次。
l onDisabled(Context)
当最后一个App Widget的实例被删除时,该方法被调用。你可以在此处清除之前在onEnabled里执行的操作。
l onReceive(Content, Intent)
每次接收到广播都会被调用,而且执行的顺序在上述方法之前。通常,你不需要实现该方法,因为AppWidgetProvider已经对各种不同类型的广播进行了过滤及分发。
注:在Android1.5中,有一个已知的问题,使得在某些情况下,onDeleted不会被调用。这时,你就需要实现onReceive()了。
AppWidgetProvider的最重要的回调方法是onUpdate。如果你的App Widget不需要创建任何临时文件或数据库,或执行任何的清理工作,这应该是你唯一需要定义的。比如说,如果你的App Widget只包含一个按钮,它的功能仅仅是当按钮点击时打开一个Activity,你可以使用以下的实现:
public class ExampleAppWidgetProvider extends AppWidgetProvider { public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { final int N = appWidgetIds.length; // Perform this loop procedure for each App Widget that belongs to this provider for (int i=0; i这个AppWidgetProvider只定义了一个onUpdate方法。它使用setOnClickPendingIntent方法设置了按钮点击时要执行的PendingIntent。该PendingIntent的作用就是启动ExampleActivity。注意,此处,我们枚举了appWidgetIds里所有的id,这些id是用来标识该AppWidgetProvider定义的所有App Widget。这样,如果用户创建了多个App Widget的实例,就会在同时被更新。对于同类的App Widget,系统只会维护一个updatePeriodMillis,这样,只有第一个实例创建时的更新周期才会起作用。例如,周期为2小时,第一个widget实例创建后1小时创建第二个实例,这样,在第二个实例创建后一小时就会被更新。
注意,由于AppWidgetProvider继承自BroadcastReceiver,如果你的操作需要耗时较长,最好在onUpdate里启动一个Service。
接收App Widget广播Intent
AppWidgetProvider只是一个方便你编程的类。如果你想直接接收App Widget,你也可以实现你自己的BroadcastReceiver或者复写onReceive方法。你需要关注的是以下四种Intent:
l ACTION_APPWIDGET_UPDATE
l ACTION_APPWIDGET_DELETED
l ACTION_APPWIDGET_ENABLED
l ACTION_APPWIDGET_DISABLED
创建一个App Widget的配置Activity
如果你想让用户在添加新的App Widget时能够进行配置,你可以创建一个配置Activity。当App Widget创建时,这个Activity会被App Widget host调用,你可以在该Activity中允许用户设置一些诸如颜色、大小、更新周期等信息。
配置Activity与别的Activity没什么不同,唯一需要做的就是监听ACTION_APPWIDGET_CONFIGURE的事件。
同时,这个Activity还需要在AppWidgetProviderInfo的xml文件里声明。
注意:由于这个Activity会在你的包外部被引用,此处必须使用全路径。
有了以上的说明,你就可以开始实现一个配置Activity了。当然,还有两件事你需要记住:
由于是被动调用的,你的配置Activity必须返回一个结果,并且带上App Widget的ID(在传来的Intent里有,int类型的AppWidgetManager.EXTRA_APPWIDGET_ID)。
当配置Activity存在时,onUpdate方法不会被调用。这样,在配置Activity中就需要请求AppWidgetManager执行一次更新。
从配置Activity更新App Widget
1、 获取App Widget的ID
Intent intent = getIntent(); Bundle extras = intent.getExtras(); if (extras != null) { mAppWidgetId = extras.getInt( AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); }2、 执行配置过程
3、 当配置完成后,获取AppWidgetManager的实例
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);4、 更新App Widget
RemoteViews views = new RemoteViews(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();注:当你的配置Activity第一次打开时,请将结果设为RESULT_CANCELED。这样,如果用户中途使用返回键退出,App Widget host就会接收到通知,知道配置过程未完成。这样App Widget就不会被添加。
设置预览图片
Android3.0开始,引入了一个previewImage字段。用户可以在widget picker中看到该预览图。如果没设置该字段,系统将使用应用的图标。
为了方便开发者创建预览图,Android模拟器中提供了一个Widget Preview应用,你可以利用该应用创建预览图并将保存后的图片放到你的应用中去。
在App Widget中使用Collection
从Android3.0开始,你可以在App Widget中使用collection。这种widget使用RemoteViewsService来显示远程数据(比如说来自content provider)。由RemoteViewsService提供的数据将使用以下view来显示:
l ListView:以垂直可拉伸列表显示。
l GridView:以二维可拉伸网格显示。
l StackView:栈式卡片视图,用户可以上下拖动以查看前一张或后一张。
l AdapterViewFlipper:一个基于adapter的简单ViewAnimator,它可以在两个或多个视图间切换,且同一时刻只有一个子项被显示。
就像上面所说的,这些collection视图基于远程数据显示。也就是说,它们使用一个Adapter来绑定界面和后台数据。在widget的上下文中,我们使用RemoteViewsFactory代替Adapter,它是Adapter接口的一个简单封装。当请求集合中的特定数据时,RemoteViewsFactory以RemoteViews对象的方式创建并返回集合中的内容。为了在你的app widget中使用collection视图,你必须实现RemoteViewsService与RemoteViewsFactory。
RemoteViewsService是一个允许远程adapter请求RemoteViews对象的服务。
RemoteViewsFactory是一个collection视图与相应数据之间的接口。具体例子可以参照StackWidget。
实现带有collection的App Widget
要实现一个带有collection的App Widget,你需要执行以下基本步骤。
在Manifest中声明
除了以上列出的内容,你还需要将collection绑定到RemoteViewsService上。你需要为RemoteViewsService声明BIND_REMOTEVIEWS权限。这样可以限制其他应用随便访问你的widget的数据。如果你要实现一个RemoteViewsService来创建collection视图,可以按照如下方式:
Collection的布局
布局中最主要的部分就是collection视图:ListView,GridView,StackView或AdapterViewFlipper。以下是一个例子:
注意:empty view是当没数据时显示的视图。
除了App Widget的整体视图,你还需要为collection中的每一项定义布局。比如说,StackView Widget的例子中,只有一种。而WeatherListWidget的例子却有两种布局文件。
带有collection的AppWidgetProvider
与普通App Widget的唯一区别就是,在带有collection的AppWidgetProvider.onUpdate中,你需要调用setRemoteAdapter方法。通过调用这个方法,collection就知道如何获取它的数据。然后你在RemoteViewsService中返回一个RemoteViewsFactory,这样widget就能获取到相应的数据了。另外,当你调用setRemoteAdapter时,你需要传入一个Intent。这个Intent指向了RemoteViewsService的实现,并指明了App Widget的ID。
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) { // 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); }RemoteViewsService类
就像上面所说的,你的RemoteViewsService指向了用于创建远程collection视图的RemoteViewsFactory。
你需要执行以下操作:
1、 继承RemoteViewsService
2、 在你的RemoteViewsService子类里,实现一个RemoteViewsFactory的内部类。
注意:你不能依赖一个Service的实力来保存数据。除非是静态数据,否则任何内容都不应该保存在此处。如果你要保存你的App Widget的数据,你可以使用ContentProvider。
RemoteViewsFactory接口
你的实现了RemoteViewsFactory的类为App Widget提供了collection中的各项所需的数据。
其中,最重要的两个需要实现的方法是onCreate和getViewAt。
当系统第一次创建factory对象时,会调用onCreate。在这里面,你可以配置数据源。通过创建内容集合或Cursor等等。例如,在StackView Widget中,onCreate里创建了一个WidgetItem的数组。当你的App Widget处于激活状态时,系统会使用它们的index来获取数据并显示。
class StackRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory { private static final int mCount = 10; private ListmWidgetItems = 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. Heavy lifting, // for example downloading or creating content etc, should be deferred to onDataSetChanged() // 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 + "!")); } ... } ... getViewAt用于返回特定位置上的RemoteViews对象。
public RemoteViews getViewAt(int position) { // Construct a remote views item based on the app widget item 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); ... // Return the remote views object. return rv; }为单独的item设置相应的行为
前文说过,通常来说,你是使用setOnClickPendingIntent方法来设置一个控件的点击事件。但对于collection中的子项,该方法是无效的。你可以先用setPendingIntentTemplate方法为collection整体的点击设置一个处理的PendingIntent,然后通过RemoteViewsFactory使用setOnClickFillInIntent为collection视图中的每一项传入一个与该项相关的Intent。该Intent会被合入处理时接收到Intent中。
在onUpdate中设置pending intent template
// This section makes it possible for items to have individualized behavior. // It does this by setting up a pending intent template. Individuals items of a collection // cannot set up their own pending intents. Instead, the collection as a whole sets // up a pending intent template, and the individual items set 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 have the 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);
在RemoteViewsFactory中设置fill-in intent
public RemoteViews getViewAt(int position) { // position will always range from 0 to getCount() - 1. // Construct a RemoteViews item based on the app widget item 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 in the pending intent template // that is set on the collection view in StackWidgetProvider. 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; }保证Collection的数据是最新的
下图给出了当更新发生时,一个带有collection视图的App Widget的工作流。
带有collection视图的App Widget的一个作用就是为用户提供实时的数据。例如,Android 3.0上的Gmail app widget,它为洪湖提供了他们的收件箱的快照。要实现这个功能,你需要能够触发你的RemoteViewsFactory及collection视图去获取以及显示新的数据。你可以通过调用AppWidgetManager的notifyAppWidgetViewDataChanged方法来达到这个目的。
通过调用这个方法,RemoteViewsFactory的onDataSetChanged方法将被调用,你可以在其中进行数据的获取。
onDataSetChanged以及后续的getViewAt方法中,你都可以进行一些处理密集型的操作。也就是说,你不用怕这个操作会占用太长的时间,从而导致UI线程无响应。
如果getViewAt方法耗时太长,加载视图(可由RemoteViewsFactory的getLoadingView获取)将显示在collection视图所在的区域。