原文
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。
Xml代码
<receiver android:name="ExampleAppWidgetProvider" >
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/example_appwidget_info" />
</receiver>
标签用于指明App Widget使用的AppWidgetProvider
标签必须包括一个含有android:name属性的标签。该属性用于指明AppWidgetProvider接收APPWIDGET_UPDATE广播。这是你唯一需要显示声明的广播。当有需要时,AppWidgetManager自动发送AppWidgetProder所需的各种广播。
标签标识了AppWidgetProviderInfo资源,它需要以下属性:
l android:name:使用android.appwidget.provider来标识AppWidgetProviderInfo。
l android:resource:标识AppWidgetProviderInfo的资源位置。
添加AppWidgetProviderInfo元数据
AppWidgetProviderInfo定义了一个App Widget的必要属性,例如最小布局范围、初始布局、更新频率、以及在创建时显示的配置Activity(可选)。
AppWidgetProviderInfo使用标签来定义,并保存在res/xml文件夹中。
Xml代码
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="294dp"
android:minHeight="72dp"
android:updatePeriodMillis="86400000"
android:previewImage="@drawable/preview"
android:initialLayout="@layout/example_appwidget"
android:configure="com.example.android.ExampleAppWidgetConfigure"
android:resizeMode="horizontal|vertical">
</appwidget-provider>
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. 创建一个布局:
Xml代码
<service android:name="MyWidgetService"
...
android:permission="android.permission.BIND_REMOTEVIEWS" />
Collection的布局
布局中最主要的部分就是collection视图:ListView,GridView,StackView或AdapterViewFlipper。以下是一个例子:
Xml代码
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<StackView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/stack_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:loopViews="true" />
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/empty_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:background="@drawable/widget_item_background"
android:textColor="#ffffff"
android:textStyle="bold"
android:text="@string/empty_view_text"
android:textSize="20sp" />
</FrameLayout>
注意: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。
Java代码
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来获取数据并显示。
Java代码
class StackRemoteViewsFactory implements
RemoteViewsService.RemoteViewsFactory {
private static final int mCount = 10;
private List<WidgetItem> mWidgetItems = new ArrayList<WidgetItem>();
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对象。
Java代码
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
Java代码
// 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
Java代码
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视图所在的区域。