【Android学习笔记】App Widget 桌面小插件

转自http://johnsonxu.iteye.com/blog/1763042

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。

<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>标签用于指明App Widget使用的AppWidgetProvider

<intent-filter>标签必须包括一个含有android:name属性的<action>标签。该属性用于指明AppWidgetProvider接收APPWIDGET_UPDATE广播。这是你唯一需要显示声明的广播。当有需要时,AppWidgetManager自动发送AppWidgetProder所需的各种广播。

<meta-data>标签标识了AppWidgetProviderInfo资源,它需要以下属性:

l android:name:使用android.appwidget.provider来标识AppWidgetProviderInfo。

l android:resource:标识AppWidgetProviderInfo的资源位置。

添加AppWidgetProviderInfo元数据

AppWidgetProviderInfo定义了一个App Widget的必要属性,例如最小布局范围、初始布局、更新频率、以及在创建时显示的配置Activity(可选)。

AppWidgetProviderInfo使用<appwidget-provider>标签来定义,并保存在res/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. 创建一个布局:

<FrameLayout
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:layout_margin="@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>

1. 创建两个dimension资源,res/values为老的平台,res/values-v14为4.0平台:

res/values/dimens.xml

<dimen name="widget_margin">15dp</dimen>

res/values-v14/dimens.xml

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

还有一种做法是在9-patch的背景图里设置边距,并且为不同的版本提供不同的背景图。

使用AppWidgetProvider类

AppWidgetProvider类继承了BroadcastReceiver,它只监听与App Widget有关的广播事件,如更新、删除、启用、禁用。当这些事件发生时,AppWidgetProvider将接收到以下事件:

l onUpdate()

由updatePeriodMills定义的时间间隔触发。当然,添加Widget的时候也会,因此,应该在此处执行一些必要的配置,如定义View的事件处理handler,有必要的话,还会启动一个临时的Service。然而,如果你声明了一个配置Activity,该方法将不会在此时被调用。

l onDelete(Context, int[])

当App Widget从host中移除时会被调用。

l onEnabled(Context)

当App Widget的实例被第一次创建时,该方法将被调用。如果你建了两个实例,那该方法也只会被调用一次。

l onDisabled(Context)

当最后一个App Widget的实例被删除时,该方法被调用。你可以在此处清除之前在onEnabled里执行的操作。

l onReceive(Content, Intent)

每次接收到广播都会被调用,而且执行的顺序在上述方法之前。通常,你不需要实现该方法,因为AppWidgetProvider已经对各种不同类型的广播进行了过滤及分发。

注:在Android1.5中,有一个已知的问题,使得在某些情况下,onDeleted不会被调用。这时,你就需要实现onReceive()了。

AppWidgetProvider的最重要的回调方法是onUpdate。如果你的App Widget不需要创建任何临时文件或数据库,或执行任何的清理工作,这应该是你唯一需要定义的。比如说,如果你的App Widget只包含一个按钮,它的功能仅仅是当按钮点击时打开一个Activity,你可以使用以下的实现:

public class ExampleAppWidgetProvider extends AppWidgetProvider {

    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        final int N = appWidgetIds.length;

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

            // Create an Intent to launch ExampleActivity
            Intent intent = new Intent(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 = new RemoteViews(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方法。它使用setOnClickPendingIntent方法设置了按钮点击时要执行的PendingIntent。该PendingIntent的作用就是启动ExampleActivity。注意,此处,我们枚举了appWidgetIds里所有的id,这些id是用来标识该AppWidgetProvider定义的所有App Widget。这样,如果用户创建了多个App Widget的实例,就会在同时被更新。对于同类的App Widget,系统只会维护一个updatePeriodMillis,这样,只有第一个实例创建时的更新周期才会起作用。例如,周期为2小时,第一个widget实例创建后1小时创建第二个实例,这样,在第二个实例创建后一小时就会被更新。

注意,由于AppWidgetProvider继承自BroadcastReceiver,如果你的操作需要耗时较长,最好在onUpdate里启动一个Service。

接收App Widget广播Intent

AppWidgetProvider只是一个方便你编程的类。如果你想直接接收App Widget,你也可以实现你自己的BroadcastReceiver或者复写onReceive方法。你需要关注的是以下四种Intent:

l ACTION_APPWIDGET_UPDATE

l ACTION_APPWIDGET_DELETED

l ACTION_APPWIDGET_ENABLED

l ACTION_APPWIDGET_DISABLED

创建一个App Widget的配置Activity

如果你想让用户在添加新的App Widget时能够进行配置,你可以创建一个配置Activity。当App Widget创建时,这个Activity会被App Widget host调用,你可以在该Activity中允许用户设置一些诸如颜色、大小、更新周期等信息。

配置Activity与别的Activity没什么不同,唯一需要做的就是监听ACTION_APPWIDGET_CONFIGURE的事件。

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

同时,这个Activity还需要在AppWidgetProviderInfo的xml文件里声明。

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

注意:由于这个Activity会在你的包外部被引用,此处必须使用全路径。

有了以上的说明,你就可以开始实现一个配置Activity了。当然,还有两件事你需要记住:

由于是被动调用的,你的配置Activity必须返回一个结果,并且带上App Widget的ID(在传来的Intent里有,int类型的AppWidgetManager.EXTRA_APPWIDGET_ID)。

当配置Activity存在时,onUpdate方法不会被调用。这样,在配置Activity中就需要请求AppWidgetManager执行一次更新。

从配置Activity更新App Widget

1、 获取App Widget的ID

Intent intent = getIntent();
Bundle extras = intent.getExtras();
if (extras != null) {
    mAppWidgetId = extras.getInt(
            AppWidgetManager.EXTRA_APPWIDGET_ID, 
            AppWidgetManager.INVALID_APPWIDGET_ID);
}

2、 执行配置过程

3、 当配置完成后,获取AppWidgetManager的实例

AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);

4、 更新App Widget

RemoteViews views = new RemoteViews(context.getPackageName(),R.layout.example_appwidget);
appWidgetManager.updateAppWidget(mAppWidgetId, views);

5、 创建返回的Intent,设其为结果,并关闭Activity


Intent resultValue = new Intent();
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
setResult(RESULT_OK, resultValue);
finish();

注:当你的配置Activity第一次打开时,请将结果设为RESULT_CANCELED。这样,如果用户中途使用返回键退出,App Widget host就会接收到通知,知道配置过程未完成。这样App Widget就不会被添加。

设置预览图片

Android3.0开始,引入了一个previewImage字段。用户可以在widget picker中看到该预览图。如果没设置该字段,系统将使用应用的图标。

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

为了方便开发者创建预览图,Android模拟器中提供了一个Widget Preview应用,你可以利用该应用创建预览图并将保存后的图片放到你的应用中去。

在App Widget中使用Collection

从Android3.0开始,你可以在App Widget中使用collection。这种widget使用RemoteViewsService来显示远程数据(比如说来自content provider)。由RemoteViewsService提供的数据将使用以下view来显示:

l ListView:以垂直可拉伸列表显示。

l GridView:以二维可拉伸网格显示。

l StackView:栈式卡片视图,用户可以上下拖动以查看前一张或后一张。

l AdapterViewFlipper:一个基于adapter的简单ViewAnimator,它可以在两个或多个视图间切换,且同一时刻只有一个子项被显示。

就像上面所说的,这些collection视图基于远程数据显示。也就是说,它们使用一个Adapter来绑定界面和后台数据。在widget的上下文中,我们使用RemoteViewsFactory代替Adapter,它是Adapter接口的一个简单封装。当请求集合中的特定数据时,RemoteViewsFactory以RemoteViews对象的方式创建并返回集合中的内容。为了在你的app widget中使用collection视图,你必须实现RemoteViewsService与RemoteViewsFactory。

RemoteViewsService是一个允许远程adapter请求RemoteViews对象的服务。

RemoteViewsFactory是一个collection视图与相应数据之间的接口。具体例子可以参照StackWidget。

实现带有collection的App Widget

要实现一个带有collection的App Widget,你需要执行以下基本步骤。

在Manifest中声明

除了以上列出的内容,你还需要将collection绑定到RemoteViewsService上。你需要为RemoteViewsService声明BIND_REMOTEVIEWS权限。这样可以限制其他应用随便访问你的widget的数据。如果你要实现一个RemoteViewsService来创建collection视图,可以按照如下方式:

<service android:name="MyWidgetService"
...
android:permission="android.permission.BIND_REMOTEVIEWS" />

Collection的布局

布局中最主要的部分就是collection视图:ListView,GridView,StackView或AdapterViewFlipper。以下是一个例子:

<?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。

public void onUpdate(Context context, AppWidgetManager appWidgetManager,
int[] appWidgetIds) {
    // update each of the app widgets with the remote adapter
    for (int i = 0; i < appWidgetIds.length; ++i) {
        
        // Set up the intent that starts the StackViewService, which will
        // provide the views for this collection.
        Intent intent = new Intent(context, StackWidgetService.class);
        // Add the app widget ID to the intent extras.
        intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);
        intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
        // Instantiate the RemoteViews object for the App Widget layout.
        RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
        // Set up the RemoteViews object to use a RemoteViews adapter. 
        // This adapter connects
        // to a RemoteViewsService  through the specified intent.
        // This is how you populate the data.
        rv.setRemoteAdapter(appWidgetIds[i], R.id.stack_view, intent);
        
        // The empty view is displayed when the collection has no items. 
        // It should be in the same layout used to instantiate the RemoteViews
        // object above.
        rv.setEmptyView(R.id.stack_view, R.id.empty_view);

        //
        // Do additional processing specific to this app widget...
        //
        
        appWidgetManager.updateAppWidget(appWidgetIds[i], rv);   
    }
    super.onUpdate(context, appWidgetManager, appWidgetIds);
}

RemoteViewsService类

就像上面所说的,你的RemoteViewsService指向了用于创建远程collection视图的RemoteViewsFactory。

你需要执行以下操作:

1、 继承RemoteViewsService

2、 在你的RemoteViewsService子类里,实现一个RemoteViewsFactory的内部类。

注意:你不能依赖一个Service的实力来保存数据。除非是静态数据,否则任何内容都不应该保存在此处。如果你要保存你的App Widget的数据,你可以使用ContentProvider。

RemoteViewsFactory接口

你的实现了RemoteViewsFactory的类为App Widget提供了collection中的各项所需的数据。

其中,最重要的两个需要实现的方法是onCreate和getViewAt。

当系统第一次创建factory对象时,会调用onCreate。在这里面,你可以配置数据源。通过创建内容集合或Cursor等等。例如,在StackView Widget中,onCreate里创建了一个WidgetItem的数组。当你的App Widget处于激活状态时,系统会使用它们的index来获取数据并显示。

class StackRemoteViewsFactory implements
RemoteViewsService.RemoteViewsFactory {
    private static final int mCount = 10;
    private 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对象。

public RemoteViews getViewAt(int position) {
   
    // Construct a remote views item based on the app widget item XML file, 
    // and set the text based on the position.
    RemoteViews rv = new RemoteViews(mContext.getPackageName(), R.layout.widget_item);
    rv.setTextViewText(R.id.widget_item, mWidgetItems.get(position).text);

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

为单独的item设置相应的行为

前文说过,通常来说,你是使用setOnClickPendingIntent方法来设置一个控件的点击事件。但对于collection中的子项,该方法是无效的。你可以先用setPendingIntentTemplate方法为collection整体的点击设置一个处理的PendingIntent,然后通过RemoteViewsFactory使用setOnClickFillInIntent为collection视图中的每一项传入一个与该项相关的Intent。该Intent会被合入处理时接收到Intent中。

在onUpdate中设置pending intent template

            // This section makes it possible for items to have individualized behavior.
            // It does this by setting up a pending intent template. Individuals items of a collection
            // cannot set up their own pending intents. Instead, the collection as a whole sets
            // up a pending intent template, and the individual items set a fillInIntent
            // to create unique behavior on an item-by-item basis.
            Intent toastIntent = new Intent(context, StackWidgetProvider.class);
            // Set the action for the intent.
            // When the user touches a particular view, it will have the effect of
            // broadcasting TOAST_ACTION.
            toastIntent.setAction(StackWidgetProvider.TOAST_ACTION);
            toastIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);
            intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
            PendingIntent toastPendingIntent = PendingIntent.getBroadcast(context, 0, toastIntent,
                PendingIntent.FLAG_UPDATE_CURRENT);
            rv.setPendingIntentTemplate(R.id.stack_view, toastPendingIntent);

在RemoteViewsFactory中设置fill-in intent

        public RemoteViews getViewAt(int position) {
            // position will always range from 0 to getCount() - 1.
    
            // Construct a RemoteViews item based on the app widget item XML file, and set the
            // text based on the position.
            RemoteViews rv = new RemoteViews(mContext.getPackageName(), R.layout.widget_item);
            rv.setTextViewText(R.id.widget_item, mWidgetItems.get(position).text);
    
            // Next, set a fill-intent, which will be used to fill in the pending intent template
            // that is set on the collection view in StackWidgetProvider.
            Bundle extras = new Bundle();
            extras.putInt(StackWidgetProvider.EXTRA_ITEM, position);
            Intent fillInIntent = new Intent();
            fillInIntent.putExtras(extras);
            // Make it possible to distinguish the individual on-click
            // action of a given item
            rv.setOnClickFillInIntent(R.id.widget_item, fillInIntent); 
            ... 
            // Return the RemoteViews object.
            return rv;
        }

保证Collection的数据是最新的

下图给出了当更新发生时,一个带有collection视图的App Widget的工作流。


【Android学习笔记】App Widget 桌面小插件_第1张图片

带有collection视图的App Widget的一个作用就是为用户提供实时的数据。例如,Android 3.0上的Gmail app widget,它为洪湖提供了他们的收件箱的快照。要实现这个功能,你需要能够触发你的RemoteViewsFactory及collection视图去获取以及显示新的数据。你可以通过调用AppWidgetManager的notifyAppWidgetViewDataChanged方法来达到这个目的。

通过调用这个方法,RemoteViewsFactory的onDataSetChanged方法将被调用,你可以在其中进行数据的获取。

onDataSetChanged以及后续的getViewAt方法中,你都可以进行一些处理密集型的操作。也就是说,你不用怕这个操作会占用太长的时间,从而导致UI线程无响应。

如果getViewAt方法耗时太长,加载视图(可由RemoteViewsFactory的getLoadingView获取)将显示在collection视图所在的区域。

你可能感兴趣的:(android,widget)