【android开发】桌面小挂件( APP Widgets )

APP小挂件指的是一个小型的应用View控件,他可以嵌入到其他应用程序中(比如说桌面),并接受定期的更新。你可以通过Widget Provider来自己发布一个。一个可以持有其他App小挂件的应用组件叫做AppWidget host

下图表示一个音乐应用的挂件;


此文章将会描述怎么使用AppWidget provider去发布一个应用挂件。创建挂件牵涉到的类是:

android.appwidget.AppWidgetHost

一、基本的描述

要创建一个App挂件,你需要做到下面的工作。

  • AppWidgetProviderInfo对象

描述App挂件的元数据,比如说:挂件的布局,更新频率,和AppWidgetProvider类。你需在XML文件中定义

  • 继承AppWidgetProvider

定义一个基于广播事件的允许动态改变挂件界面的方法,通过他,当你的应用挂件更新、变得可用、变得不可用、被删除的事件你都可以收到。

  • View Layout

XML文件中定义一个挂件的初始布局文件。

 

此外,你可以实现一个挂件配置 Activity,他是一个可选的Activity,当用户要添加你的挂件时,他会启动用于给用户设置和更改挂件的设置。

 

下面将介绍怎么去设置一个这样的组件

Manifest中申明一个挂件

首先需要在Manifest中声明AppWidgetProvider类。比如说:


    
        
    
    


  元素需要androidname属性,他指的是挂件使用的AppWidgetProvider

 必须要包含一个元素,并且包含androidname属性这个属性表示AppWidgetProvider接受ACTION_APPWIDGET_UPDATE广播。这是你唯一需要明确指定的广播接受,在必要的时候AppWidgetManager将会自动发送广播给所有挂件。

元素定义了AppWidgetProviderInfo资源,他需要有下面几个属性:

  • Android:name----定义元数据的名字,此例中使用  “android.appwidget.provider  ”来定义这个数据,作为AppWidgetProviderInfo的描述信息
  • android:resource---定义AppWidgetProviderInfo资源的地址

 

添加AppWidgetProviderInfo元数据

AppWidgetProviderInfo 定义了一个挂件的基本品质,比如说:最小布局尺寸、他初始化的布局资源、更新挂件的频率、和configurationActivity在创建的时候加载在XML文件中定义AppWidgetProviderInfo 对象只需要使用元素,保存在res/xml文件夹中。



下面是的属性概览:

  • minWidth 和 minHeight 的值表示挂件需要的最小空间。默认情况下,挂件的位置是基于主屏幕中的具有宽高的方格子(可以看做将屏幕划分为很多个矩阵格子)。如果挂件的宽和高与方格子不匹配,挂件的尺寸将计算到与方格子相近的尺寸。

【注意:为了使你的挂件能方便的跨设备,你最好将你的最小尺寸设置为低于4 X 4

 

  • minResizeWidth 和 minResizeHeight 属性指定了挂件的绝对最小尺寸,这个值应该定义为,当你的挂件出现字迹不清与不可用时的最小尺寸。使用这些属性允许用户去重新设置挂件的尺寸,可能会小于你使用minWidthminHigh的值。这个特性从 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中的中的androidpreviewImage是一样的。(具体怎么设置后面将会介绍)
  • autoAdvanceViewId 属性定义了可以被挂件宿主自增加的子Viewid。在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的话。然而,你必须知道的是,挂件的布局使基于RemoteViewsandroid.widget.RemoteViews)的,不是所有的布局和view控件他都支持。

 

他们支持的layout如下:

  • FrameLayout
  • LinearLayout
  • RelativeLayout
  • GridLayout

 

他们支持的控件如下:(他们的衍生品不支持)

  • AnalogClock【时钟控件】
  • Button
  • Chronometer【计时器】
  • ImageButton
  • ImageView
  • ProgressBar
  • TextView
  • ViewFlipper
  • ListView
  • GridView
  • StackView
  • AdapterViewFlipper

(他们都在android.widget包下。)

 

RemoteView也支持使用ViewStubandroid.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类型的对象,包含了下面的内容:

 

  • 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


            上面的APPWidgetProvider就只是定义了onUpdate()方法,此方法用来定义一个启动ActivityPendingIntent,然后使用setOnclickPendingIntentintPendingIntent)绑定到挂件的Button上。

            注意:上面的代码是在一个循环中,他们迭代 appWidgetIds数组的每个实例,每个实例都是一个挂件的ID,通过这种方式,如果用户创建了多个挂件实例(例如在主屏幕创建相同的两个挂件),他们全部都将更新,因为,他们使用的updatePeriodMillis使用的是同一个实例。比如说:如果刷新频率被设置为两个小时,同时,桌面上有两个相同的挂件,第二个挂件在第一个挂件出现的一个小时后添加的。他们最终的结果就是,一旦到了第一个挂件刷新时(也就是第一个挂件添加两个小时后),第二个挂件也一起刷新,但是到了第二个挂件刷新时,将不起任何作用(直接忽视了第二个挂件的周期,以第一个为准)

注意:

由于APPWidgetProvider是扩展自BroadcastReceiver,因此,不能保证在回调方法返回后你的进程将继续保持运行(BroadcastReceiver的生命周期),如果你需要做一些时间比较长的任务,你最好在onUpdate()中开启一个Service,然后在Service中更新界面。例子见右边--

 

接收Widget广播Intents

使用APPWidgetProvider仅仅是个便利的做法,如果你喜欢,你也可以将挂件直接继承BroadcastReceiver,或者复写APPWidgetProvideronRecerveContextIntent)方法。有几个Intent你需要注意的:

  • ACTION_APPWIDGET_UPDATE
  • ACTION_APPWIDGET_DELETED
  • ACTION_APPWIDGET_ENABLED
  • ACTION_APPWIDGET_DISABLED
  • ACTION_APPWIDGET_OPTIONS_CHANGED

 

创建挂件Configuration Activity

      如果你想让用户在新添加你的挂件前先做一些设置。那么你可以创建一个Configuration Activity,此Activity将会在挂件添加前启动,以便于用户为挂件做设置,比如说:挂件颜色、挂件大小、挂件更新频率等。

        Configuration Activity应该像一般的Activity一样在manifest中申明,只是需要加上

android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>以便接受Intent的过滤器而已。比如说:

 

    
        
    

同时,我们需要在APPWidgetProviderInfoXML文件中的android:configure 中添加上此Activity。比如:



记得 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、当设置完毕,通过getInstanceContext)获取到APPWidgetManager对象

AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);

4、通过拥有布局的RemoteViews对象调用updateAPPWidgetintRemoteViews)来更新挂件。

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

      在一个垂直方向滚动显示条目的列表。

  •       GridView

       ListView相似,只是在水平方向展示的条目多于1条(ListView1条)。

  •      StackView

      类似于将很多卡牌堆叠在一起的View(有点像名片盒),

  •      AdapterViewFlipper

     一个由ViewAnimator做后台支撑的adapter,他可以在两个或多个View之间动画。但是同一时间只能有一个child可以被显示。

 

上面有说道:这些collectionView将显示来自其他地方的数据。这就意味着,他们要使用一个Adapter去绑定他们的数据到UI上,一个适配器绑定了一些条目,而这些条目是将一堆数据分别存放到分别的View对象中。

由于这些collectionview是基于adapter的,所有安卓框架必须要包含额外的建筑样式去支持这样的挂件。在挂件的上下文中,adapterRemoteViewsFactory所取代,它仅仅对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.
 
}

二、示例应用

【android开发】桌面小挂件( APP Widgets )_第1张图片

此例子由10堆叠起来的View组成,展示的名称由0!到9!,此例子有下面一些行为:

  • 可以像查看堆叠的相册一样,向下向上翻阅----使用的StackView
  • 如果用户没与之交互,他会自己滚动。因为在xml文件中使用了android:atutoAdvanceViewId = "@id/stack_view"
  • 如果用户点击了其中的一个View,那么他将会吐丝:你点击了 **”。其中**是根据你点击的索引不同而不同。

实现集合类型的小挂件
下面有几个相同的实现步骤,用于实现APP Widgets。下面的实例是针对集合类的挂件(ViewStacks,ListView,GridView这样的视图)

1、在Manifest中创建APP Widgets申明。
除了创建APP Widgets必须要在Manifest申明的内容之外(前文中有介绍),如果我们想实现集合类型的挂件,我们还需要绑定一个RemoteViewsService服务,所以,我们也必须在Manifest中将其Service申明出来,并且需要加上BIND_REMOTEVIEWS权限。申明权限主要是为了阻止其他应用程序访问你的数据。比如:我们可以申明这样一个Service(用于填充集合类挂件的数据的服务)。
android:name="MyWidgetService"  指向你创建的RemoteViewService的子类。

2、为集合类挂件创建一个Layout。
既然我们要使用集合类挂件,那么,我们的布局文件中就要使用一种集合类的View,可以使用的有:ListView、GridView、StackView或者AdapterViewFlipper。下面的widget_layout.xml使用StackView作为实例:

 

    
    
【当我们使用集合类View时,我们最好在集合类View的同级申明一个当该View为空时显示的View。上面实例中的id为empty_view的TextView就是用于当StackView为空时显示的一个View】
值得注意的是:使用ListView等集合类View我们知道,除了创建ListView的布局文件之外,我们还要创建其集合View的itemView。本例子中,我们为StackView创建了两套Itemview,分别是:dark_widget_item.xml和light_widget_item.xml。

3、为集合类挂件创建APPWidgetProvider类。
正常情况下,我们写的一般的AppWidgetProvider子类大部分都会在onUpdate()方法中(主要复写对象)。开发集合类的挂件复写onUpdate()主要不同在于,当创建一个集合类挂件时,我们必须要调用用setRemoteAdapter(),用来告诉集合View去哪里取数据。RemoteViewsService可以返回一个RemoteViewsFactory的实现,这样Widgets就可以获取到数据了,当你调用setRemoteAdapter()方法时,你必须传递一个Intent(指向你实现的RemoteViewsService)、一个widgetID(指向需要更新的widget)。
例如:下面展示了我们的StackView Widget实现onUpdate()的一个例子。
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类】
持久化数据
你不能把数据存储依赖于某个单独的Service实例,你应该将其持久化。因此,你不要将你的任何数据存储在RemoteViewsService中(静态的也不行)。如果你想将你的widget的数据持久化,最好的方式是使用ContentProvider,他可以不受进程的生命周期影响。
正如上面的描述,你的RemoteViewsService实现类提供RemoteViewsFactory来填充远程的集合view 。
特别地:你需要执行下面几个步骤:
1、创建RemoteViewsService的子类。通过RemoteViewsService,远程adapter可以请求RemoteView。
2、在你的RemoteViewsService中,包含一个RemoteViewsFactory接口的实现,RemoteViewsFactory是一个远程集合类View和其adapter之间的接口,并持有其集合类View的数据,这个接口主要用于通过给定的data生成RemoteView的每个Item。这个接口仅仅是对adapter进行了简单的包装。
RemoteViewService最重要的组成部分就是RemoteViewsFactory,下面我们将对其进行介绍。

【RemoteViewsFactory接口】
我们实现RemoteViewsFactory接口就是为了给集合类View的每个Item提供数据。为了实现这样的目的,我们通过他来讲每个item的XML布局文件和相应的数据源结合起来。具体数据源可以是来自数据库的任意的数据。在此例子中:StackView的数据源是一个WidgetItems的数组,RemoteViewsFactory扮演者一个adapter的角色,用于将数据和远程的集合类View绑定起来。
下面有两个非常重要的方法我们需要实现:
onCreate()和getViewAt()
系统在创建你Factory的时候将会调用其onCreate()方法。在这个方法中,你可以进行一些数据库连接或者cursor连接之类的操作。比如说:在我们的例子中StackView Widget使用onCreate()方法去初始化一个WidgetItem的集合。当你的widget处于活动状态是,系统将访问这个集合去根据item的不同position取不同的值来初始化item。
下面有个StackView的RemoteViewsFactory实例代码。
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 + "!"));
       }
       ...
    }
...

RemoteViewsFactory的getViewAt()方法返回一个与data集合中相应position的数据相关的RemoteView对象。下面是StackView的一段摘录代码。
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;
}

【为每个独立的item添加行为(点击效果)】
上面提到的内容告诉我们怎样创建一个集合类View。但是,我们如何为每个item添加不同的行为呢?下面继续介绍:
正如之前介绍的,一般情况下我们为在RemoteView中为一个View添加点击事件时,我们可以使用setOnClickPendingIntent()来为之设置点击事件。但是,如果我们要为集合类View的每个Item都设置点击事件时,这样的做法是不允许的。取而代之的是,先为集合类View使用setPendingIntentTemplate()创建一个PendingIntent模板,然后其每个不同item使用setOnClickFillInIntent()去填充出不同的Intent数据。
这小节将通过StackView为例子来介绍怎样为每个Item添加不同的行为。在这个例子中,如果用户点击了最顶上的view,appWidget将会显示一个Toast消息“touched view n,”其中n为view的position号。思路如下:
  • StackWidgetProvider(APPWidgetProvider的子类)创建一个PendingIntent,并拥有一个自定义action,叫做:TOAST_ACTION。
  • 当用户点击了一个view,我们将会广播这个Intent。
  • 这个广播将会传递给StackWidgetProvider的onReceive()方法,然后小挂件将会显示点击该View的Toast信息。每个item的数据是由RemoteViewsService的RemoteViewsFactory提供。
(提示:在这个例子中,StackView Widgets使用的是广播,但是在很多app中可能会启动相应的Activity。但是实现代码和思路都很类似)

【创建PendingIntent模板】
StackWidgetProvider(APPWidgetProvider的子类)设置PendingIntent,集合中每个独立的item不能设置他们自己的PendingIntent。正确的做法是:为整个集合类View创建一个PendingIntent模板,然后每个item去填充intent。当然,因为,我们的StackView的每个Item点击事件是发送广播,所以,为了方便,我们的StackWidgetsProvider也设置为可以接受这类广播消息(Action是TOAST_ACTION的广播消息),从而onReceive()会受到这样的消息。实例代码片段如下:
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);
    }
}


【创建填充模板PendingIntent的Intent实例】
你的RemoteViewsFactory必须为每个Item创建一个Intent去填充PendingIntent中的内容,以示区分item。下面是个实例代码片段:
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;
       }
    ...
    }


【保持数据的更新】
下面的插图显示了集合类Widgets当有数据更新的情况。他展示了Widgets是如何和RemoteViewsFactory交互的,并且,你如何触发更新操作。

【android开发】桌面小挂件( APP Widgets )_第2张图片

集合类挂件的其中一个特性就是可以为用户提供最新的内容展示。
比如说:如果android3.0有个Gmail的小挂件,他可以给用户提供收件箱的快速浏览。为了将此变为可能,你需要触发你的RemoteViewsFactory和集合类View去获取并展示最新的数据。你可以通过APPWidgetManager调用notifyAPPWidgetViewDataChanged()去实现。这个方法最终回去调用你的RemoteViewsFactory的onDataSetChanged()方法,从而给了你机会去获取新数据。
另外,你可以在onDataSetChanged()方法中执行大量的密集的同步操作(也就是耗时操作),最终也可以保证你的方法在MetaData和ViewData之前。不止如此,你也可以在getViewAt()方法中执行密集的耗时操作,如果用户滑动到此依旧为加载完毕时,该item会显示loadingView(由RemoteViewsFactory的getLoadingView指定),直到该getViewAt()正确的返回了View。






























你可能感兴趣的:(android开发)