APP小挂件指的是一个小型的应用View控件,他可以嵌入到其他应用程序中(比如说桌面),并接受定期的更新。你可以通过Widget Provider来自己发布一个。一个可以持有其他App小挂件的应用组件叫做AppWidget host。
下图表示一个音乐应用的挂件;
此文章将会描述怎么使用AppWidget provider去发布一个应用挂件。创建挂件牵涉到的类是:
Android.appwidget.AppWidgetHost |
一、基本的描述
要创建一个App挂件,你需要做到下面的工作。
描述App挂件的元数据,比如说:挂件的布局,更新频率,和AppWidgetProvider类。你需在XML文件中定义
定义一个基于广播事件的允许动态改变挂件界面的方法,通过他,当你的应用挂件更新、变得可用、变得不可用、被删除 的事件你都可以收到。
View Layout
在XML文件中定义一个挂件的初始布局文件。
此外,你可以实现一个挂件配置 Activity,他是一个可选的Activity,当用户要添加你的挂件时,他会启动用于给用户设置和更改挂件的设置。
下面将介绍怎么去设置一个这样的组件
在Manifest中申明一个挂件
首先需要在Manifest中声明AppWidgetProvider类。比如说:
<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>
<receiver> 元素需要android:name属性,他指的是 挂件使用的AppWidgetProvider
<intent-filter>必须要包含一个<action>元素,并且包含android:name属性。 这个属性表示AppWidgetProvider接受ACTION_APPWIDGET_UPDATE广播。这是你唯一需要明确指定的广播接受,在必要的时候AppWidgetManager将会自动发送广播给所有挂件。
<meta-data> 元素 定义了AppWidgetProviderInfo资源,他需要有下面几个属性:
Android:name----定义元数据的名字,此例中使用 “android.appwidget.provider ”来定义这个数据,作为AppWidgetProviderInfo的描述信息
android:resource --- 定义AppWidgetProviderInfo资源的地址
添加AppWidgetProviderInfo元数据
AppWidgetProviderInfo 定义了一个挂件的基本品质,比如说:最小布局尺寸、他初始化的布局资源、更新挂件的频率、和configurationActivity在创建的时候加载。在XML文件中定义AppWidgetProviderInfo 对象只需要使用<appwidget-provider> 元素,保存在res/xml文件夹中。
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:minWidth="40dp" android:minHeight="40dp" android:updatePeriodMillis="86400000" android:previewImage="@drawable/preview" android:initialLayout="@layout/example_appwidget" android:configure="com.example.android.ExampleAppWidgetConfigure" android:resizeMode="horizontal|vertical" android:widgetCategory="home_screen"> </appwidget-provider>
下面是<appwidget-provider>的属性概览:
minWidth 和 minHeight 的值表示挂件需要的最小空间。默认情况下,挂件的位置是基于主屏幕中的具有宽高的方格子(可以看做将屏幕划分为很多个矩阵格子)。如果挂件的宽和高与方格子不匹配,挂件的尺寸将计算到与方格子相近的尺寸。
【注意:为了使你的挂件能方便的跨设备,你最好将你的最小尺寸设置为低于4 X 4】
minResizeWidth 和 minResizeHeight 属性指定了挂件的绝对最小尺寸,这个值应该定义为,当你的挂件出现字迹不清与不可用时的最小尺寸。使用这些属性允许用户去重新设置挂件的尺寸,可能会小于你使用minWidth与minHigh的值。这个特性从 android 3.1开始
updatePeriodMillis 属性定义了挂件多久被AppWidgetProvider调用onUpdate() 刷新一次,事实上不能保证准时刷新(会有一定的出入),我们建议刷新频率不要太高,以便保持电量。当然你也可以允许用户自己去设置刷新的频率,有的人可能会设置15分钟刷新,当然也有人可能设置为一天只刷新4次。
【如果设备正在休眠,但是到了刷新你挂件的时间,设备将会被激活,然后去执行更新操作。如果你一个多小时的刷新一次可能不会对电池造成什么影响,但是,如果你刷新频率比较高,或者可能不需要挂件在休眠的时候刷新,那么你可以基于 alarm 刷新,他将不会激活设备。实现方式:使用你的AppWidgetProvider需要接受的Intent设置一个alarm(使用AlarmManager类),将alarm的类型设置为 ELAPSED_REALTIME 或者 RTC,这样只有在设备唤醒的情况下才通知你的挂件刷新,最后你需要将你的 updatePeriodMillis 设置为 0 】
initialLayout 属性指向一个定义挂件布局的XML文件。
configure 定义了一个Activity,在用户添加挂件的时候他将会启动,以便用户去设置挂件的配置。这是可选操作。(具体怎么创建后面将会介绍到)
previewImage 属性,用作预览的图片。当用户还未添加挂件的时候通过此图片让用户大致知道你的挂件。此属性与在Manifest中的<receiver>中的android:previewImage是一样的。(具体怎么设置后面将会介绍)
autoAdvanceViewId 属性定义了可以被挂件宿主自增加的子View的id。在android 3.0中被引进。
resizeMode 属性指定了挂件被重定义大小的规则。你可以使用此挂件让你的挂件在主屏幕中可以被调节大小---水平,垂直,同时。用户长按挂件可以去显示调节把柄,然后拖拽把柄可以改变大小(还是以方格子为单元)。resizeMode 的值包括:“horizontal”、“vertical”、“none”。如果你要设置为水平、垂直都可以改变,那么这样设置“horizontal | vertical”。在android 3.1中引进
minResizeHeight 属性定义了那些可以被调节大小的挂件的最小的高度(使用dp做单位)。如果挂件比minHeight大或者挂件调节大小并不可用时,此字段将没有作用。在android 4.0中引进
minResizeWidth 属性定义了最小的宽度(使用dp作单位),与minResizeHeight 类似的作用
widgetCategory 属性表示你的挂件是否可以展示在主屏幕(home_screen)中,锁屏界面(keyguard)中,或者同时。只有在android 5.0以下的才可以使用锁屏界面的挂件(keyguard),在android5.0或以上只能使用 home_screen
参考类android.appwidget.AppWidgetProviderInfo以了解更多
创建挂件的布局文件
你必须在XML文件中定义个初始化的布局文件,保存在你工程的res/layout/目录下。你可以使用下面的View对象来设计你的布局。但是,在你设计你的布局之前,你最好先阅读并理解挂件的设计指导方针 AppWidget Design Guidelines:(见右侧)
创建挂件非常简单,如果你熟悉Layout的话。然而,你必须知道的是,挂件的布局使基于RemoteViews(android.widget.RemoteViews)的,不是所有的布局和view控件他都支持。
他们支持的layout如下:
他们支持的控件如下:(他们的衍生品不支持)
AnalogClock【时钟控件】
Chronometer【计时器】
(他们都在android.widget包下。)
RemoteView也支持使用ViewStub(android.view.ViewStub)进行运行时懒加载。
为挂件添加margin
我们的控件一般不需要延生到边缘,也不要和其他挂件挤在一起了。因此我们需要在边界上加上一个的空白。那就是“Margin”或者“padding”
在android4.0以后,系统将在挂件之间自动加上padding。我们不需要自己手动的添加。如果低于4.0的,我们自己加上就行了。
例子如下:
<FrameLayout android:layout_width="match_parent" android:layout_height="match_parent" android:padding="@dimen/widget_margin"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" android:background="@drawable/my_widget_background"> … </LinearLayout> </FrameLayout>
资源文件如下:
res/values/dimens.xml文件中这样定义:
<dimen name="widget_margin">8dp</dimen>
res/values-v14/dimens.xml文件中这样定义(由于系统默认给我们加了,我们再添加)
<dimen name="widget_margin">0dp</dimen>
另外一个可选操作则是:为有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类型的对象,包含了下面的内容:
AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH
包含了当前挂件的最小宽度尺寸(使用dp为单位)。
AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT
包含了当前挂件的最小高度尺寸(使用dp为单位)。
AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH
包含了当前挂件的最大宽度尺寸(使用dp为单位)。
AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT
包含了当前挂件的最大高度尺寸(使用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<N; i++) { int appWidgetId = appWidgetIds[i]; // Create anIntent to launch ExampleActivity Intent intent = new Intent(context, ExampleActivity.class); PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0); // Get the layoutfor the App Widget and attach an on-click listener // to the button RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.appwidget_provider_layout); views.setOnClickPendingIntent(R.id.button, pendingIntent); // Tell theAppWidgetManager to perform an update on the current app widget appWidgetManager.updateAppWidget(appWidgetId, views); } } }
上面的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中申明,只是需要加上
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>以便接受Intent的过滤器而已。比如说:
<activity android:name=".ExampleAppWidgetConfigure">
<intent-filter> <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/> </intent-filter> </activity>
同时,我们需要在APPWidgetProviderInfo的XML文件中的android:configure 中添加上此Activity。比如:
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" ... android:configure="com.example.android.ExampleAppWidgetConfigure" ... > </appwidget-provider>
记得android:configure的值需要类的全称!
上面的内容就是你启动ConfigurationActivity需要做的所有内容。剩下的需要你做的事情就在这个实际的Activity中,当你在实现Activity的时候,下面有两点重要的事情你需要记住:
调用Configuration Activity后,Configuration Activity应该返回结果,结果中应该包含绑定的挂件的ID,将ID保存到Intent中的 EXTRA_APPWIDGET_ID中。
如果你创建了Configuration Activity,挂件的onUpdate()方法将不会被调用(当Configuration Activity被启动的时候,系统将不会发送ACTION_APPWIDGET_UPDATE 广播 ),请求APPWidgetManager更新挂件就将是Configuration Activity的职责。当然,以后onUpdate()方法会被正常调用。
从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 字段。他可以定义一个挂件的预览图,方便用户直接筛选,如果此字段没定义,那么系统将使用挂件的图片作为预览。
具体定义如下:
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" ... android:previewImage="@drawable/preview"> </appwidget-provider>
【为了帮助你为你的挂件创建挂件预览图,安卓模拟器包含了一个应用,叫做“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!,此例子有下面一些行为:
可以像查看堆叠的相册一样,向下向上翻阅----使用的StackView
如果用户没与之交互,他会自己滚动。因为在xml文件中使用了android:atutoAdvanceViewId = "@id/stack_view"。
如果用户点击了其中的一个View,那么他将会吐丝:“你点击了 **”。其中**是根据你点击的索引不同而不同。
下面没有翻译,具体事例代码自己琢磨
Implementingapp widgets with collections
To implementan app widget with collections, you follow the same basic steps you would useto implement any app widget. The following sections describe the additionalsteps you need to perform to implement an app widget with collections.
Manifest forapp widgets with collections(清单中声明)
In addition to therequirements listed in Declaring an appwidget in the Manifest, to make it possible for app widgets with collections tobind to your RemoteViewsService, you mustdeclare the service in your manifest file with the permission BIND_REMOTEVIEWS. This preventsother applications from freely accessing your app widget's data. For example,when creating an App Widget that uses RemoteViewsService topopulate a collection view, the manifest entry may look like this:
<service android:name="MyWidgetService"
...
android:permission="android.permission.BIND_REMOTEVIEWS" />
The line android:name="MyWidgetService" refers toyour subclass of RemoteViewsService.
Layout forapp widgets with collections(设置布局文件)
The main requirementfor your app widget layout XML file is that it include one of the collectionviews:ListView, GridView, StackView, or AdapterViewFlipper. Here isthe widget_layout.xml for theStackView Widgetsample:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayoutxmlns: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>
Note thatempty views must be siblings of the collection view for which the empty viewrepresents empty state.
In addition to thelayout file for your entire app widget, you must create another layout filethat defines the layout for each item in the collection (for example, a layoutfor each book in a collection of books). For example, theStackView Widgetsample only has one layout file, widget_item.xml, since allitems use the same layout. But the WeatherListWidgetsample has two layout files: dark_widget_item.xml and light_widget_item.xml.
AppWidgetProviderclass for app widgets with collections
As with a regularapp widget, the bulk of your code in your AppWidgetProvider subclasstypically goes inonUpdate(). The majordifference in your implementation for onUpdate() whencreating an app widget with collections is that you must call setRemoteAdapter(). This tells thecollection view where to get its data. TheRemoteViewsService can thenreturn your implementation of RemoteViewsFactory, and the widgetcan serve up the appropriate data. When you call this method, you must pass anintent that points to your implementation of RemoteViewsService and theapp widget ID that specifies the app widget to update.
For example, here'show the StackView Widget sample implements the onUpdate() callbackmethod to set theRemoteViewsService as theremote adapter for the app widget collection:
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);
}
RemoteViewsServiceclass
Persisting data
You can’t rely on a single instance of your service,or any data it contains, to persist. You should therefore not store any data inyourRemoteViewsService (unless it is static). If you want your appwidget’s data to persist, the best approach is to use a ContentProvider whose data persists beyond the processlifecycle.
As described above,your RemoteViewsService subclassprovides the RemoteViewsFactory used topopulate the remote collection view.
Specifically,you need to perform these steps:
Subclass RemoteViewsService. RemoteViewsService is the service through which a remote adapter can requestRemoteViews.
In your RemoteViewsService subclass, include a class that implements the RemoteViewsFactory interface. RemoteViewsFactory is an interface for an adapter between a remote collection view (such as ListView, GridView, and so on) and the underlying data for that view. Your implementation is responsible for making a RemoteViews object for each item in the data set. This interface is a thin wrapper around Adapter.
The primary contentsof the RemoteViewsService implementationis its RemoteViewsFactory, describedbelow.
RemoteViewsFactoryinterface
Your custom classthat implements the RemoteViewsFactory interfaceprovides the app widget with the data for the items in its collection. To dothis, it combines your app widget item XML layout file with a source of data.This source of data could be anything from a database to a simple array. In the StackView Widgetsample, the data source is an array of WidgetItems. The RemoteViewsFactory functionsas an adapter to glue the data to the remote collection view.
The two mostimportant methods you need to implement for your RemoteViewsFactory subclassareonCreate() and getViewAt() .
The systemcalls onCreate() whencreating your factory for the first time. This is where you set up anyconnections and/or cursors to your data source. For example, the StackView Widgetsample uses onCreate()to initialize anarray of WidgetItem objects. When your app widgetis active, the system accesses these objects using their index position in thearray and the text they contain is displayed
Here is an excerptfrom the StackView Widget sample's RemoteViewsFactory implementationthat shows portions of the onCreate() method:
class StackRemoteViewsFactory implements
RemoteViewsService.RemoteViewsFactory {
private staticfinal intmCount =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. 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 + "!"));
}
...
}
...
The RemoteViewsFactory method getViewAt() returnsa RemoteViews objectcorresponding to the data at the specified position in thedata set. Here is an excerpt from the StackView Widget sample'sRemoteViewsFactory implementation:
public RemoteViewsgetViewAt(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;
}
Addingbehavior to individual items
The abovesections show you how to bind your data to your app widget collection. But whatif you want to add dynamic behavior to the individual items in your collectionview?
As describedin Using theAppWidgetProvider Class, you normally use setOnClickPendingIntent() to set anobject's click behavior—such as to cause a button to launch an Activity. But thisapproach is not allowed for child views in an individual collection item (toclarify, you could use setOnClickPendingIntent() to set upa global button in the Gmail app widget that launches the app, for example, butnot on the individual list items). Instead, to add click behavior to individualitems in a collection, you use setOnClickFillInIntent(). This entailssetting up up a pending intent template for your collection view, and thensetting a fill-in intent on each item in the collection via your RemoteViewsFactory.
This section usesthe StackView Widgetsample to describe how to add behavior to individual items. In theStackView Widgetsample, if the user touches the top view, the app widget displays the Toast message"Touched view n," where n is theindex (position) of the touched view. This is how it works:
The StackWidgetProvider (an AppWidgetProvider subclass) creates a pending intent that has a custom action called TOAST_ACTION.
When the user touches a view, the intent is fired and it broadcasts TOAST_ACTION.
This broadcast is intercepted by the StackWidgetProvider's onReceive() method, and the app widget displays the Toast message for the touched view. The data for the collection items is provided by theRemoteViewsFactory, via the RemoteViewsService.
Note: The StackView Widget sample uses a broadcast, but typically an app widgetwould simply launch an activity in a scenario like this one.
Setting up the pending intent template
The StackWidgetProvider (AppWidgetProvider subclass)sets up a pending intent. Individuals items of a collection cannot set up theirown pending intents. Instead, the collection as a whole sets up a pendingintent template, and the individual items set a fill-in intent to create uniquebehavior on an item-by-item basis.
This class alsoreceives the broadcast that is sent when the user touches a view. It processesthis event in itsonReceive() method. Ifthe intent's action is TOAST_ACTION, the app widgetdisplays a Toast messagefor the current view.
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);
}
}
Setting the fill-in Intent
Your RemoteViewsFactory must set afill-in intent on each item in the collection. This makes it possible todistinguish the individual on-click action of a given item. The fill-in intentis then combined with thePendingIntent templatein order to determine the final intent that will be executed when the item isclicked.
public class StackWidgetService extends RemoteViewsService {
@Override
public RemoteViewsFactory onGetViewFactory(Intent intent) {
returnnew StackRemoteViewsFactory(this.getApplicationContext(), intent);
}
}
class StackRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {
private staticfinal intmCount =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);
}
// 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;
}
...
}
KeepingCollection Data Fresh
The following figureillustrates the flow that occurs in an app widget that uses collections whenupdates occur. It shows how the app widget code interacts with the RemoteViewsFactory, and how youcan trigger updates:
One feature of appwidgets that use collections is the ability to provide users with up-to-datecontent. For example, consider the Android 3.0 Gmail app widget, which providesusers with a snapshot of their inbox. To make this possible, you need to be ableto trigger your RemoteViewsFactory andcollection view to fetch and display new data. You achieve this with the AppWidgetManager call notifyAppWidgetViewDataChanged(). This callresults in a callback to your RemoteViewsFactory’s onDataSetChanged() method,which gives you the opportunity to fetch any new data. Note that you canperform processing-intensive operations synchronously within the onDataSetChanged() callback.You are guaranteed that this call will be completed before the metadata or viewdata is fetched from the RemoteViewsFactory. In addition,you can perform processing-intensive operations within the getViewAt() method. Ifthis call takes a long time, the loading view (specified by the RemoteViewsFactory’s getLoadingView() method)will be displayed in the corresponding position of the collection view until itreturns.
作者:LanTingShuXu