应用程序的窗口小部件是微型应用程序视图,可以嵌入到其他应用程序中(如主屏幕),并收接收定期更新。这些视图在用户界面被称为小工具,你可以用一个App的Widget提供者发布一个。一个能够容纳其他应用程序的窗口小部件的应用程序组件被称为一个应用程序窗口小部件的宿主。下面的截图显示了音乐应用程序的Widget。

本文介绍如何使用应用程序的Widgetprovider发布一个应用程序窗口小部件。

小部件设计

有关如何设计你的应用程序窗口小部件的信息,请阅读窗口小部件的设计指南。

基础知识


要创建一个应用程序窗口小部件,您需要满足以下条件:

AppWidgetProviderInfo对象
为App Widget描述元数据( metadata),如App Widget的布局,更新频率和AppWidgetProvider类。这些都应当在XML中定义。
AppWidgetProvider类实现
基于广播事件定义基本方法,允许你与widget界面交互。通过它,您将收到广播对Widget进行更新,启用,禁用和删除操作。
视图布局
在XML中 为widget定义初始的布局。

此外,你可以实现一个App widget可配置activity。当用户添加你的App widget,并允许他或她在创建时修改App widget的设置时,启动这个可选的activity。

以下各节描述了如何设置这些组件。

在manifest中声明App widget


首先,在应用程序的AndroidManifest.xml文件中声明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属性,它指定App Widget需要使用的AppWidgetProvider

<intent-filter>元素必须包括有android:name 属性的<action>元素。该属性指定AppWidgetProvider接受ACTION_APPWIDGET_UPDATE广播。这是你必须显式声明的唯一广播。该AppWidgetManager自动发送所有其他应用程序窗口小部件广播到AppWidgetProvider是必要的。

<meta-data>元素指定AppWidgetProviderInfo资源和需要以下属性:

  • android:name-指定元数据(metadata)名称。使用android.appwidget.provider标识数据作为AppWidgetProviderInfo描述符。
  • android:resource-指定AppWidgetProviderInfo资源位置。

添加AppWidgetProviderInfoMetadata


AppWidgetProviderInfo定义App Widge的基本特征,例如其最小尺寸布局,其初始布局资源,如何经常更新应用程序窗口小部件,以及在创建时启动一个可选的可配置的activity。在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|keyguard"
android:initialKeyguardLayout = "@layout/example_keyguard" >
</appwidget-provider>

以下是<appwidget-provider>属性摘要

  • minWidthminHeight属性的值指定App Widget默认需要占据的最小空间。默认主页屏幕定位应用的窗口小部件在其窗口的位置,是根据其已定义长和宽的子元素来实现的。如果一个App Widget的最小长和宽的值不匹配子元素的尺寸,那么App widget的尺寸就会调整到最接近子元素的尺寸。

    有关调整您的应用程序的窗口小部件的更多信息,请参阅App Widget Design Guidelines

    注:为了使您的应用程序窗口小部件可以跨设备,你的应用程序部件的最小尺寸不应该大于4×4的子元素(cells)尺寸。

  • minResizeWidthminResizeHeight属性指定App Widget的绝对最小尺寸。这些值应指定低于该应用程序的Widget会模糊不清或以其他方式不可用的大小。在Android3.1中,允许用户使用这些属性调整控件的尺寸,可能小于默认尺寸界定的minWidthminHeight属性。
  • updatePeriodMillis属性定义App widget多久请求一次来自通过调用onUpdate()回调方法的AppWidgetProvider的一次更新。实际的更新不能保证发生在确切的时间上,并带有需要的值,我们建议尽可能少的更新—为了节省电池,也许不超过一小时一次。您可能还允许用户在配置中调整频率,有些人可能希望股票行情每隔15分钟更新,或者仅仅一天四次。

    注意:如果设备处于睡眠状态,而又到了更新时间(通过updatePeriodMillis定义),那么设备将为了执行更新而被唤醒。如果你不超过每小时更新一次,这可能不会造成电池寿命严重的问题。但是,如果你需要频繁更新和/或在设备处于休眠状态时不需要更新,那么你可以根据警代替执行,则不会唤醒设备执行更新。要做到这一点,用一个你的AppWidgetProvider将接收的Intent来设置一个警报,这个警报用AlarmManager来设置。设置报警类型为ELAPSED_REALTIMERTC,在设备被唤醒时,这将仅传递警报。然后设置updatePeriodMillis为零(“0”)。

  • initialLayout属性点指向定义App widget布局的布局资源。
  • configure属性定义了当用户添加应用程序窗口小部件时要启动的activity,为了让他或她配置应用程序控件属性。这是可选的(阅读下文的Creating an App Widget Configuration Activity)。
  • previewImage属性指定App widget在被配置以后的预览界面,在用户选择这个App widget时,用户将看到它。如果没有提供,用户看到的是你应用程序的启动图标。此字段对应于AndroidManifest.xml文件中的<receiver>元素里的android:previewImage属性。对于使用previewImage的详细讨论,请参阅设置预览图像。在Android 3.0已介绍。
  • autoAdvanceViewId属性指定应用程序部件子视图的ID,应该是自动优先接近App widget宿主。在Android 3.0已介绍。
  • resizeMode属性指定App widget可以被调整的模式。您可以使用此属性调整主屏幕的小部件的大小,水平,垂直,或两轴。用户触摸并持有(touch-hold)一个小部件,以显示其缩放控点,然后拖动水平和/或垂直控点来改变布局网格的大小。resizeMode属性的值包括“水平”,“垂直”和“无”。为声明一个widget能调整水平和垂直方向的位置,提供“水平|垂直”值。在android3.1中介绍。
  • minResizeHeight属性指定小部件可以调整的最低高度(DPS)。如果它是大于minHeight,或者如果没有启用垂直大小调整(见resizeMode),此字段没有效果。在Android 4.0介绍。
  • minResizeWidth属性指定小部件可以调整最小宽度(DPS)。如果它是大于了minWidth,或者如果没有启用水平调整大小(见resizeMode),此字段没有效果。在Android 4.0介绍。
  • widgetCategory属性声明是否你的​​App小部件可以在主屏幕上或者在锁屏(键盘保护)上显示出来,或两者兼而有之。这个属性的值包括“home_screen”和“keyguard”。请参阅在锁屏启用应用程序的窗口小部件。默认值是“home_screen”。在Android4.2介绍。
  • initialKeyguardLayout属性指向定义锁屏应用的Widget布局的布局资源。这种工作方式与android:initialLayout相同,因为它提供了一个可以立即出现的布局,直到您的App widget被初始化,并能够更新布局。在安卓4.2介绍。

更多信息请阅读AppWidgetProviderInfo类。

创建App widget布局(Creating the App Widget Layout)


在XML中你必须为你的App widget定义一个初始的布局,并将其保存在工程的RES /layout/目录下,使用下列的视图对象可以设计你的App widget。但在你开始设计App Widget之前,请仔细阅读并理解App Widget设计准则。

如果您熟悉Layouts,那么创建一个App widget是简单的。但是,你必须意识到,App Widget布局是基于RemoteViews,这并不支持所有类型的布局或视图控件的。

一个RemoteViews对象(因而,一个App Widget)可以支持以下布局类:

  • FrameLayout
  • LinearLayout
  • RelativeLayout的
  • GridLayout

以及下面的窗口小部件类:

  • AnalogClock
  • Button
  • Chronometer
  • ImageButton
  • ImageView
  • ProgressBar
  • TextView
  • ViewFlipper
  • ListView
  • GridView
  • StackView
  • AdapterViewFlipper

不支持这些类的子类。

RemoteViews还支持ViewStub,这是一种无形的,零大小的视图,你可以在运行时用它来填充布局资源。

Adding margins to App Widgets

窗口小部件一般不应延伸到屏幕边缘,不应该在视觉上与其他小部件齐平,所以你应该围绕你的widget框架四周增加边距(margins)。

从Android 4.0起,应用程序窗口小部件自动给出构件框架应用程序构件的界框提供更好协调用户主屏幕上其他小部件图标之间边距利用强烈推荐行为请将应用程序的targetSdkVersion设置14更高版本

这很容易编译一个单独的布局,它适用于平台的早期版本,具有自定义边距,在Android4.0或者更高版本并没有额外的边距:

  1. 设置应用程序的targetSdkVersion到14或更高。
  2. 创建布局,如下,为它的边距引用dimension resource:
    <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>
  3. 创建两个维度(尺寸)的资源,一个在RES /values/,提供pre-Android 4.0自定义边距,一个在RES /values-V14 /没有提供额外的空间给Android 4.0的小部件:

    res/values/dimens.xml:

    <dimenname="widget_margin">8dp</dimen>

    res/values-v14/dimens.xml:

    <dimenname="widget_margin">0dp</dimen>

另一种选择是简单地在默认情况下构建额外边距到nine-patch(9.png图片)背景的assets,在Android API level 14或更高版本中,提供没有边距的、不同的nine-patch(9.png图片)

使用AppWidgetProvider类

AppWidgetProvider类扩展自BroadcastReceiver,作为一个便利类来处理App Widget广播。AppWidgetProvider接收事情广播,诸如App widget更新,删除,启用和禁用。当这些广播事件发生时,AppWidgetProvider会调用以下方法:

OnUpdate()
这个方法被调用以在被定义的时间间隔内更新App Widget,这个间隔被定义在AppWidgetProviderInfo中的 updatePeriodMillis 属性中(见 Adding the AppWidgetProviderInfo Metadata)。当用户添加App widget时这种方法也被调用,所以, 如果有必要,要进行必要的设置,如定义视图的事件处理程序和启动临时服务。但是,如果你已经声明了一个配置Activity,当用户添加应用程序窗口小部件时,这个方法不会被调用,但在后续更新时会被调用。当配置完成后,执行第一次更新是配置Activity的责任。(请参见下文的 Creating an App Widget Configuration Activity。)
onAppWidgetOptionsChanged()
在widget第一次被放置在宿主视图上时,以及在widget调整大小的任何时间,这个方法会被调用。您可以使用此回调方法以显示或隐藏基于widget尺寸范围的内容。你可以通过调用 getAppWidgetOptions()得到范围大小,它一个包含以下内容的 Bundle

  • OPTION_APPWIDGET_MIN_WIDTH-包含一个widget实例当前的宽度下限,以DP为单位。
  • OPTION_APPWIDGET_MIN_HEIGHT-包含一个widget实例当前的高度下限,以DP为单位。
  • OPTION_APPWIDGET_MAX_WIDTH-包含一个widget实例当前的宽度上限,以DP为单位。
  • OPTION_APPWIDGET_MAX_HEIGHT-包含一个widget实例当前的高度上限,以DP为单位。
在API level 16(Android 4.1)版本上介绍了此回调。如果你要实现此回调,请确保你的App不依赖于它,因为它在比较旧的设备上不会被调用。
onDeleted(Context,
int[])
在App Widget每次从它的宿主视图中被删除时,这个方法会被调用。
onEnabled(Context)
当一个App widget的实例被首次创建时该方法被调用。例如,如果用户添加的应用程序的widget的两个实例,这只是第一次调用。如果你需要打开一个新的数据库或执行其他设置只需要发生一次为所有应用的Widget实例,那么这是一个很好的地方,做到这一点。
onDisabled(Context)
当一个App widget的最后一个实例从它的宿主视图中被删除时,此方法被调用。这时,你i应该在 onEnabled(Context)方法中清理那些已经完成的工作,如删除临时数据库。
onReceive(Context,Intent)
这个方法被调用是为了每个广播和前面上述的回调方法。你通常不需要实现这个方法,因为AppWidgetProvider 默认执行过滤所有App widget广播和调用适当的上述方法

AppWidgetProvider最重要的回调是OnUpdate(),因为当每个应用程序窗口小部件被添加到其宿主视图(除非你使用一个配置活动)时,这个方法都会被调用。如果您的App Widget接受任何用户交互事件,那么你需要在这个回调中注册事件处理程序。如果您的App Widget不创建临时文件或数据库,或执行其他需要清理的工作,那么OnUpdate()可能是你唯一需要定义的回调方法。例如,如果你想要App Widget与启动activity时单击一个按钮,你可以使用如下AppWidgetProvider的实现

publicclassExampleAppWidgetProviderextendsAppWidgetProvider{

publicvoid onUpdate(Context context,AppWidgetManager appWidgetManager,int[] appWidgetIds){
finalint N = appWidgetIds.length;

// Perform this loop procedure for each App Widget that belongs to this provider
for(int i=0; i<N; i++){
int appWidgetId = appWidgetIds[i];

// Create an Intent to launch ExampleActivity
Intent intent =newIntent(context,ExampleActivity.class);
PendingIntent pendingIntent =PendingIntent.getActivity(context,0, intent,0);

// Get the layout for the App Widget and attach an on-click listener
// to the button
RemoteViews views =newRemoteViews(context.getPackageName(), R.layout.appwidget_provider_layout);
views
.setOnClickPendingIntent(R.id.button, pendingIntent);

// Tell the AppWidgetManager to perform an update on the current app widget
appWidgetManager
.updateAppWidget(appWidgetId, views);
}
}
}

这AppWidgetProvider仅定义OnUpdate(),其目的是定义一个PendingIntent启动一个activity,并用setOnClickPendingIntent(INT,PendingIntent)连接到App Widget的按钮。注意,在appWidgetIds中包括一个循环遍历每个条目,这是一个数组的id标识,确定每个App widget的创建。这样,如果用户创建多个实例的App widget,然后他们都同时更新。然而,只有一个updateperiodmillis时间表将管理所有的App widget。例如,如果更新计划被定义为每两个小时,在第一个后添加第二个实例的App widget并每小时更新一次,那么它们都将被定义为第一个而第二个更新周期会被忽略(他们会每两小时更新,而不是每隔一小时)

注:由于AppWidgetProvider扩展自BroadcastReceiver,你的进程在回调方法返回后不能保证继续运行(有关广播生命周期的信息BroadcastReceiver)。如果您的App Widget安装过程可能需要几秒钟(也许在执行网页请求时),你要求你的进程继续运行,可以考虑在OnUpdate()方法中启动一个服务。在服务中,不可以执行更新App widget,而不用担心AppWidgetProvider会由于Application Not Responding(ANR)而关闭。一个App Widget运行服务的例子请参阅Wiktionary sample's AppWidgetProvider

也请看ExampleAppWidgetProvider.java示例类。

接收App Widget广播(Receiving App Widget broadcast Intents)

AppWidgetProvider仅仅是一个便利类。如果您想直接接收App Widget广播,你可以实现自己BroadcastReceiver或重写onReceive(Context,Intent)回调。你需要关心的意图如下:

  • ACTION_APPWIDGET_UPDATE
  • ACTION_APPWIDGET_DELETED
  • ACTION_APPWIDGET_ENABLED
  • ACTION_APPWIDGET_DISABLED
  • ACTION_APPWIDGET_OPTIONS_CHANGED

创建应用程序窗口小部件配置Activity(Creating an App Widget Configuration Activity)


如果你想用户配置设置,当他或她增加了一个新的App Widget时,你可以创建一个应用程序窗口小部件配置activity。这个activity将由App widget宿主视图自动启动,并允许在创建时为App Widget配置可用设置,如应用程序的widget颜色,大小,更新周期或其他功能设置。

配置activity应该在Android清单文件中被声明为一个普通的activity。然而,它将被App widget的宿主视图用ACTION_APPWIDGET_CONFIGUREaciton来启动,所以activity需要接受这种意图。例如:

<activity android:name = ".ExampleAppWidgetConfigure" > 
<intent-filter>
<action android:name = "android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>

此外,activity必须在AppWidgetProviderInfo XML文件中声明,并有android:configure属性(见上述的Adding the AppWidgetProviderInfo Metadata)。例如,配置activity可以象这样声明:

<appwidget-provider xmlns:android = "http://schemas.android.com/apk/res/android" 
...
android:configure = "com.example.android.ExampleAppWidgetConfigure"
...
>
</appwidget-provider>

请注意,activity被声明要用其全名,因为它会被你的外部包所引用。

这就是你需要启动的配置activity。现在,所有你需要的是实际的activity。但是,当你实现这个activity时,要记住两个重要的事情:

  • App Widget宿主视图调用配置Activity,并且配置Activity总是返回一个结果。其结果应包括App widget ID,这个ID通过启动activity的Intent被传递过来(作为EXTRA_APPWIDGET_ID被保存在Intent的附加数据-extras中)。
  • 在App
    widget被创建时,
    OnUpdate()
    方法将不会被调用(当一个配置activity被启动时,系统不会发送ACTION_APPWIDGET_UPDATE广播)。在App widget首次被创建时,配置activity从AppWidgetManager中请求更新,这是配置activity的责任。但是,OnUpdate()将为后续更新而被调用-它仅跳过第一次。

从配置activity和更新App widget的代码片段中如何返回结果,请参阅下面部分中的例子。

从配置activity更新App Widget(Updating the App Widget from the configuration Activity)

当一个App widget使用配置activity时,在配置完成时更新App widget,是这个activity的责任。你也可以直接从AppWidgetManager请求一次更新,也能做到这些。

下面步骤是正确地更新应用程序窗口小部件并关闭配置activity的概要:

  1. 首先,从那启动activity的Intent中获得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. 执行您App widget配置。
  3. 当配置完成后,调用调用getInstance(Context)获得AppWidgetManager的一个实例:
    AppWidgetManager appWidgetManager =AppWidgetManager.getInstance(context);
  4. 通过调用updateAppWidget(INT,RemoteViews)方法用一个RemoteViews布局更新App Widget
    RemoteViews views =newRemoteViews(context.getPackageName(),
    R
    .layout.example_appwidget);
    appWidgetManager
    .updateAppWidget(mAppWidgetId, views);
  5. 最后,创建需要返回的Intent,设置Activity的返回结果,并销毁activity:
    Intent resultValue =newIntent();
    resultValue
    .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
    setResult
    (RESULT_OK, resultValue);
    finish
    ();

提示:当您的配置activity第一次启动,将activity结果为RESULT_CANCELED。这样一来,如果用户选择退出之前能够达到目的,App widget通知配置被取消,并不会再添加。

ApiDemos中的ExampleAppWidgetConfigure.java示例类。

设置预览图像(Setting a Preview Image)


Android 3.0介绍了previewImage,它指定了App widget的预览图。此预览从widget选择器(picker)展示给用户。如果该字段没有提供,App Widget的图标用于预览。

下面是你如何在XML中指定这个设置:

<appwidget-provider xmlns:android = "http://schemas.android.com/apk/res/android" 
...
android:previewImage = "@drawable/preview" >
</appwidget-provider>

为了App widget,帮助创建一个预览图像,Android模拟器包括一个称为“App widget预览”的应用。要创建一个预览图像,启动这个应用程序,为您的应用程序选择App widget,并设置你想要你的预览图像如何出现,然后保存它,并将其放置在你的应用程序可绘制的资源中(drawable resources)。

Enabling App Widgets on the Lockscreen


Android4.2引入了小部件添加到锁屏的功能。为了指示你的App widget可以在锁屏上使用,在指定你的AppWidgetProviderInfo的XML文件中声明android:widgetCategory属性。此属性有两个值:“home_screen”和“Keyguard”。一个应用程序窗口小部件可以声明一个或两个都支持。

默认情况下,每一个App widget支持在主屏幕上使用,所以对于android:widgetCategory属性,“home_screen”是默认值。如果你希望你的App widget可用于锁屏,添加了“keyguard”值:

<appwidget-provider xmlns:android = "http://schemas.android.com/apk/res/android" 
...
android:widgetCategory = "keyguard|home_screen" >
</appwidget-provider>

如果声明一个小部件显示在锁屏和桌面上,很可能你会想要根据显示的位置自定义widget。例如,你可能会为锁屏与桌面(Launcher)创建一个单独的布局文件。下一步是在运行时检测的窗口小部件类别,并相应地作出响应。通过通过调用getAppWidgetOptions(),您可以检测你的widget是否是在锁屏或主屏幕上,用以获取widget的选择并作为Bundle。返回的Bundle将包括键值:OPTION_APPWIDGET_HOST_CATEGORY,其值将是WIDGET_CATEGORY_HOME_SCREENWIDGET_CATEGORY_KEYGUARD其中之一。此值取决于widget所绑定的宿主视图。在AppWidgetProvider,你可以再检查widget的类别,例如:

AppWidgetManager appWidgetManager;
int widgetId;
Bundle myOptions = appWidgetManager.getAppWidgetOptions (widgetId);

// Get the value of OPTION_APPWIDGET_HOST_CATEGORY
int category = myOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY,-1);

// If the value is WIDGET_CATEGORY_KEYGUARD, it's a lockscreen widget
boolean isKeyguard = category ==AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD
一旦你知道了widget的类别,你可以选择性的加载不同的基础布局,设置不懂的特征,等等。例如:
int baseLayout = isKeyguard ? R.layout.keyguard_widget_layout : R.layout.widget_layout;

当widget在锁屏上,有Android:initialKeyguardLayout属性,你应该为你的app widget指定一个初始的布局。这种工作方式与android:initialLayout相同,因为它提供了一个可以立即显示的布局,直到您的应用程序widget被初始化,并能够更新布局的布局。

大小调整原则(Sinzing guidelines)

当widget被托管在锁屏上时,框架忽略minWidthminHeightminResizeWidthminResizeHeight字段。如果一个部件也是一个主屏幕的小部件,仍需要这些参数,因为他们还要被用在主屏幕的widget上,但它们会为锁屏的目的而被忽略。

一个锁屏小工具的宽度始终充满已提供空间。对于锁屏小工具的高度,您有以下选择:

  • 如果小部件不标记本身是垂直可调整大小的(android:resizeMode="vertical"),那么部件的高度将永远是“小”:
    • 纵向模式,当解锁UI正在显示,”小“被作为剩下的空间。
    • 在平板电脑和landscape phones,“小”设置在每个设备的基础上。
  • 如果小部件标记本身​​作为垂直可调整大小,那么窗口小部件高度作为”small“x显示在整显示一个解锁UI的纵向上。在其他所有情况下,小部件的尺寸以填充可用的高度。

使用App Widgets集合(Using App Widgets with Collections)


Android 3.0引入了App Widget集合。这些种类的应用程序的窗口小部件使用RemoteViewsService来显示支持远程数据的集合,如从一个content provider。它所提供的RemoteViewsService提出App widget通过使用以下视图类型,我们称之为”集合视图“

ListView控件
一个用来显示项目在一个垂直滚动列表项。举一个例子,看看Gmail App widget。
GridView控件
一个视图用来显示项目在双向滚动的网格。 举一个例子,查看书签的App widget
StackView
堆叠式卡片视图(有点像一盒子名片),在哪里用户可以上/下移动当前的卡片,分别可见到前一张/下一张卡片,举一个例子YouTube和书籍的App widget
AdapterViewFlipper
适配器支持简单的 ViewAnimator,它处理两个或多个视图之间动画。在一时间只有它的一个子视图可以显示出来。

如上所述,这些集合视图的显示支持远程数据集合。这意味着,他们使用一个适配器来将其用户界面绑定到他们的数据上。适配器将单个项目从一个数据集合绑定到单个视图对象。由于这些集合视图由适配器支持,Android框架必须包括额外的架构来支持它们在应用程序窗口小部件中使用。在App widget的上下文中,适配器被替换为RemoteViewsFactory,它是适配器接口的一个简单包装。当为集合中特定项请时,RemoteViewsFactory创建并返回该项目的集合作为一个RemoteViews对象。为了在您的应用程序窗口小部件包含集合视图,您必须实现RemoteViewsServiceRemoteViewsFactory

RemoteViewsService是一个服务,它允许一个远程适配器请求服务RemoteViews对象。

RemoteViewsFactory是集合视图之间的适配器的接口(如ListViewGridView等等)。从StackView Widget sample,这里是用来实现这个服务和接口的样板代码的例子:

publicclassStackWidgetServiceextendsRemoteViewsService{
@Override
publicRemoteViewsFactory onGetViewFactory(Intent intent){
returnnewStackRemoteViewsFactory(this.getApplicationContext(), intent);
}
}

classStackRemoteViewsFactoryimplementsRemoteViewsService.RemoteViewsFactory{

//... include adapter-like methods here. See the StackView Widget sample.

}

示例应用程序(Sample Application)

本节中的代码摘录来自StackView Widget sample示例:

此示例包括10层视图,显示的值“0!”“9!”的应用程序部件有这些主要行为:

  • 用户可以移去App widget中最上面的视图,以显示下一个或上一个视图。这是一个内置的StackView行为。
  • 无需任何用户交互,App widget通过其视图顺序自动展示,像幻灯片放映。这是由于在res/xml/stackwidgetinfo.xml文件中设置了android:autoAdvanceViewId="@id/stack_view"。此设置适用于该视图的ID,在这种情况下,它堆叠视图的视图ID。
  • 如果用户触摸的最上面视图,App widget Toast信息:"Touched viewn,",其中Ñ是触摸视图的索引(位置)。对于这是如何实现的更多讨论,请参阅Adding behavior to individual items。

实现App widget集合(Implementing app widgets with collections)

要实现App widget集合,你按照你会使用的相同的基本步骤来实现任何App widget。以下各节介绍您需要执行的实现一个App widget集合的其他步骤。

Manifest for app widgets with collections

除了在Declaring an app widget in the Manifest中所列的要求,要使App widget集合绑定到你的RemoteViewsService上,

除了 ​​列出的要求声明在清单中的应用程序插件,使人们有可能对应用程序的窗口小部件集合绑定到你的RemoteViewsService,你必须在你的清单文件中声明具有BIND_REMOTEVIEWS权限的服务。这可以防止其他App widget随意访问你的App widget的数据。例如,当创建App widget,使用RemoteViewsService来填充集合视图时,清档文件可能如下所示:

<serviceandroid:name="MyWidgetService"
...
android:permission="android.permission.BIND_REMOTEVIEWS"/>
android:name="MyWidgetService"指的是RemoteViewsService的子类

为App widget集合布局(Layout for app widgets with collections)

您的App widget的布局XML文件主要要求是,它包括集合视图之一:ListView中GridView控件StackView,或AdapterViewFlipper。这里是StackView Widget sample的widget_layout.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>

请注意,空视图必须是该空视图表示空状态的集合视图兄妹视图 。

除了整个应用程序小部件布局文件,你必须创建另一个布局文件定义布局集合中的每个项目(例如,每本书的藏书布局)。例如,StackView Widget sample仅具有一个布局文件,widget_item.xml,因为所有的项目使用相同的布局。但WeatherListWidget sample有两个布局文件:dark_widget_item.xmllight_widget_item.xml

关于App widget集合的AppWidgetProvider类(AppWidgetProvider class for app widgets with collections)

作为常规应用程序窗口小部件AppWidgetProvider子类代码通常大部份OnUpdate()中。集合创建一个App widget时,实现OnUpdate()的主要区别是你必须调用setRemoteAdapter()告诉集合视图在哪里获取数据。那是RemoteViewsService可以返回实现RemoteViewsFactory,并且widget可以提供适当数据调用方法必须传递指向您实现的RemoteViewsService指定更新的App widget的App widgetID意图

例如,下面是StackView Widget如何实现onUpdate()回调方法,将RemoteViewsService设置为App widget集合的远程适配器:

publicvoid 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 =newIntent(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 =newRemoteViews(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(除非它是静态的)。如果你希望你的App widget的数据持久化,最好的方法是使用一个ContentProvider,其数据持续超过流程生命周期。


如上所述,你RemoteViewsService子类提供了RemoteViewsFactory用于填充远程集合视图。

具体来说,您需要执行以下步骤:

  1. 子类RemoteViewsServiceRemoteViewsService是通过一个远程适配器,可以请求RemoteViews的服务
  2. RemoteViewsService子类,包括实现RemoteViewsFactory接口的类RemoteViewsFactory是用于远程集合视图(如ListViewGridView,等等)和该视图的基础数据之间的适配器的接口。您的实现是负责为数据集中的每一项创建一个RemoteViews对象。这个接口是围绕一个适配器的包装类

RemoteViewsService实现的主要内容是其RemoteViewsFactory类,如下所述。

RemoteViewsFactory接口(RemoteViewsFactory interface)

你自定义的类实现RemoteViewsFactory接口,为在其集合中的项目提供有数据的App Widget。要做到这一点,它结合了您的App widget项目布局的XML文件的数据源。这种数据源可以是从数据库到简单数组中的任何一个。在StackView Widget sample中,数据源是一个WidgetItems数组。RemoteViewsFactory作为一个适配器来粘附到远程集合视图数据。

为你的RemoteViewsFactory子类,你有必要实现两个最重要的方法:onCreate()和getViewAt()。

当你第一次创建你的factory时,系统会调用onCreate()。这里是你设置任何连接和/或游标到数据源的地方。例如,StackView Widget sample使用onCreate()来初始化WidgetItem数组对象。当你的App widget处于活动的,系统使用其在数组中的索引和它们包含并被现实的文本来访问这些对象。

下面是从摘录StackView的Widget样品的RemoteViewsFactory实施,显示部分的onCreate()方法:

class StackRemoteViewsFactory implements
RemoteViewsService.RemoteViewsFactory{
privatestaticfinalint mCount =10;
privateList<WidgetItem> mWidgetItems =newArrayList<WidgetItem>();
privateContext mContext;
privateint mAppWidgetId;

publicStackRemoteViewsFactory(Context context,Intent intent){
mContext
= context;
mAppWidgetId
= intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
}

publicvoid 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(newWidgetItem(i +"!"));
}
...
}
...

RemoteViewsFactory的方法getViewAt()返回一个对应于数据集中的在指定位置数据的RemoteViews对象。下面是从StackView Widget样品摘录RemoteViewsFactory实现:

publicRemoteViews 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 =newRemoteViews(mContext.getPackageName(), R.layout.widget_item);
rv
.setTextViewText(R.id.widget_item, mWidgetItems.get(position).text);

...
// Return the remote views object.
return rv;
}

将行为添加到单个项目(Adding behavior to individual items)

上述部分介绍如何将数据绑定到你的应用程序窗口小部件集合。但如果你想要向你的集合视图中的单个项目添加动态行为?

Using the AppWidgetProvider Class所述,您通常使用setOnClickPendingIntent()来设置对象的点击行为—如让一个按钮来启动一个Activity。但是,对于单个集合项目的子视图这个方法不被允许(要澄清这一点,例如,你可以使用setOnClickPendingIntent()在启动Gmail App的widget中建立一个全局的按钮,例如一个全球性的按钮,而不是在单个列表项)。相反,要将单击行为添加到集合中的单个项目,请使用setOnClickFillInIntent()。这就需要为您集合视图建立一个pending Intent模板,然后再通过你的RemoteViewsFactory在集合中的每一项上设置一个fill-in Intent。

本节使用StackView Widget sample来描述如何添加行为到单个项目。在StackView Widget sample中,如果用户触摸顶层视图,该App widget显示Toast消息“已触摸视图Ñ“,其中Ñ是触摸视图的索引(位置)。这是如何工作的:

  • StackWidgetProviderAppWidgetProvider子类)创建了pending Intent,自定义了一个action 称为TOAST_ACTION
  • 当用户触摸视图,Intent被触发并广播TOAST_ACTION
  • 这个广播被StackWidgetProvideronReceive()方法接收,App widget为被触摸的视图显示Toast消息。对于该集合中的项目的数据被RemoteViewsFactory通过RemoteViewsService提供。

注:StackView Widget sample使用了一个广播,像在这样的情况下,App widget通常简单的启动activity。

Setting up the pending intent template

StackWidgetProviderAppWidgetProvider子类)设置pending Intent。一个集合中单独的一项不能建立它自己的Pending Intent。相反,集合作为一个整体设置pending Intent模板,以及个别项目设置一个fill-in Intent来在按项目顺序的基础上创建独特的行为。

这个类还接广播,这个广播在用户触摸一个视图的时候被发送,它在onReceive()方法中处理此事件。如果Intent的action是TOAST_ACTION,App widget为当前视图显示一个Toast消息。

public class StackWidgetProvider extends AppWidgetProvider{
publicstaticfinalString TOAST_ACTION ="com.example.android.stackwidget.TOAST_ACTION";
publicstaticfinalString EXTRA_ITEM ="com.example.android.stackwidget.EXTRA_ITEM";

...

// Called when the BroadcastReceiver receives an Intent broadcast.
// Checks to see whether the intent's action is TOAST_ACTION. If it is, the app widget
// displays a Toast message for the current item.
@Override
publicvoid 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 StackViewService that will
// provide the views for this collection.
Intent intent =newIntent(context,StackWidgetService.class);
intent
.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);
// When intents are compared, the extras are ignored, so we need 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 =newRemoteViews(context.getPackageName(), R.layout.widget_layout);
rv
.setRemoteAdapter(appWidgetIds[i], R.id.stack_view, intent);

// The empty view is displayed when the collection has no items. 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 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 =newIntent(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);

appWidgetManager
.updateAppWidget(appWidgetIds[i], rv);
}
super.onUpdate(context, appWidgetManager, appWidgetIds);
}
}
设置fill-in Intent(Setting the fill-in Intent)

你的RemoteViewsFactory必须在集合中的每个项目上设定一个fill-in Intent。这使得它区分已给定项目的 on-click动作变的可能。然后fill-in Intent与PendingIntent结合,以便决定在子项目被点击以后这最后的Intent将会被执行。

public class StackWidgetService extends RemoteViewsService{
@Override
public RemoteViewsFactory onGetViewFactory(Intent intent){
return new StackRemoteViewsFactory(this.getApplicationContext(), intent);
}
}

class StackRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory{
private static finalint mCount =10;
private List<WidgetItem> mWidgetItems =newArrayList<WidgetItem>();
private Context mContext;
privateint mAppWidgetId;

public StackRemoteViewsFactory(Context context,Intent intent){
mContext
= context;
mAppWidgetId
= intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
}

// Initialize the data set.
public void onCreate(){
// In onCreate() you set up 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(newWidgetItem(i +"!"));
}
...
}
...

// Given the position (index) of a WidgetItem in the array, use the item's text value in
// combination with the app widget item XML file to construct a RemoteViews object.
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 =newRemoteViews(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 =newBundle();
extras
.putInt(StackWidgetProvider.EXTRA_ITEM, position);
Intent fillInIntent =newIntent();
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;
}
...
}

Keeping Collection Data Fresh

下图说明了:在更新触发时,发生在使用集合的App widget中的流程。它展示了App widget代码如何与RemoteViewsFactory交互,以及你如何能够触发更新:

使用App widget 集合的一个特点是,能够用户提供最新的内容。例如,鉴于Android 3.0的Gmail App widget,它为用户提供了收件箱的快照。要做到这一点,你需要能够触发RemoteViewsFactory和集合视图,以获取和显示新数据。你能达到AppWidgetManager调用notifyAppWidgetViewDataChanged()的目的。这个调用导致回调到您的RemoteViewsFactoryonDataSetChanged()方法,使你有机会获取任何新数据。请注意,您可以在onDataSetChanged()回调内执行同步操作。你要保证,在从RemoteViewsFactory获取metedata和views的数据之前,就将此调用完成。此外,您可以在getViewAt()方法中执行处理密集型操作。如果此调用需要很长时间,加载的视图(由RemoteViewsFactorygetLoadingView()方法指定)将显示在相应位置的集合视图中,,直到它返回。

更多相关文章

  1. android listview多视图嵌套多视图
  2. Android - Espresso -滚动到非列表视图项。
  3. 导航架构组件 - 具有CollapsingToolbar的详细信息视图
  4. 仪表测试自定义视图
  5. 无法将视图添加到相对布局
  6. ListView的上拉弹簧、下拉弹簧,下拉弹簧时动态带刷新和切换换刷新
  7. 在android上滚动时,列表视图的位置会发生变化
  8. 如何在android中的recycler视图中显示第一项的选择?
  9. springMVC使用html视图配置详解

随机推荐

  1. Android IOS WebRTC 音视频开发总结(十五)-
  2. Android异步加载图像小结
  3. Android Ant更新项目
  4. 系出名门Android(8) - 控件(View)之TextS
  5. android中 google map计算两GPS点间距离
  6. Android实现TextView字符串波浪式跳动
  7. 页面未随软键盘上升及android隐藏软键盘
  8. Android异步更新UI-线程池-Future-Handle
  9. Android(安卓)wifi设计原理(源码分析)
  10. Android应用模块基本配置元素