您的位置:首页 > 移动开发

App Widget详解

2016-01-27 21:02 369 查看
在网上看到的,翻译的官方文档,先收藏了http://my.oschina.net/bintojojo/blog/209356

目录[-]

基本信息

在AndroidManifest中声明一个App
Widget

添加AppWidgetProviderInfo元数据

创建App
Widget的布局

使用AppWidgetProvider类

创建一个App
Widget的配置Activity

设置预览图片

在App
Widget中使用Collection

实现带有collection的App
Widget

在Manifest中声明

Collection的布局

带有collection的AppWidgetProvider



RemoteViewsFactory接口



在onUpdate中设置pending
intent template

在RemoteViewsFactory中设置fill-in
intent



App Widget是一种可以被放在其他应用中(如Launcher)并接收周期性更新的应用视图。这些视图在UI上就表现为Widget,并且你可以同App
Widget Provider一起发布。

对于能够包含其他App Widget的应用程序组件,称为App
Widget Host。


基本信息

要创建一个App Widget,你需要完成以下步骤:

lAppWidgetProviderInfo对象:它描述了App
Widget的基本元素,比如说布局、更新频率、AppWidgetProvider类等。这些都是在xml文件中定义的。

l AppWidgetProvider类的实现:它定义了一些基本的方法以支持通过广播事件与App
Widget交互。通过它,当App Widget被更新、启用、禁用以及删除时,你将收到相应的广播信息。

l View
Layout:通过xml文件定义App
Widget的初始视图。

另外,你还可以实现一个App Widget的配置Activity。当然,这不是强制的。


在AndroidManifest中声明一个App
Widget

首先,声明AppWidgetProvider。

Xml代码



<receiver android:name="ExampleAppWidgetProvider" >

<intent-filter>

<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />

</intent-filter>

<meta-data android:name="android.appwidget.provider"

android:resource="@xml/example_appwidget_info" />

</receiver>

<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文件夹中。

Xml代码



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

android:minWidth="294dp"

android:minHeight="72dp"

android:updatePeriodMillis="86400000"

android:previewImage="@drawable/preview"

android:initialLayout="@layout/example_appwidget"

android:configure="com.example.android.ExampleAppWidgetConfigure"

android:resizeMode="horizontal|vertical">

</appwidget-provider>

l minWidth与minHeight属性表示了App
Widget所需的最小布局区域。

默认的主屏中,App Widget要想确认其位置,需要通过基于网格的具有固定宽度和高度的单元。如果App
Widget的最小宽度和高度无法匹配给定的单元,它将会自动扩展到最接近的单元大小。

由于主屏的布局方向是可变的,你应该考虑最坏的情况(每单元的宽和高都是74dp)。然而,为了防止在扩展时产生整数计算错误,你还需要减去2。因此,你可以用以下公式来计算最小宽度和高度(单位dp):(单元数量×74)-2。

同时,为了保证你的App Widget能够在各种设备上正常使用,它们的宽度和高度必须不超过4×4个单元。

lupdatePeriodMillis属性定义了App
Widget框架调用AppWidgetProvider的onUpdate方法的频率。对于实际的更新,我们不建议采用该值进行实时处理。最好是越不频繁越好——为了保证电量,一小时不超过一次为好。当然,你也可以允许用户对更新频率进行设置。

注意,如果更新触发时设备正处于休眠状态,设备将唤醒以执行该操作。如果你的更新频率不超过一小时一次,这不会对电池的寿命产生多大的影响。但如果你需要更频繁地更新却又不想要在设备休眠时执行,那你可以使用定时器来执行更新。要达到这种目的,可以在AlarmManager中设置一个AppWidgetProvider能接收的Intent。将类型设为ELAPSED_REALTIME或RTC。由于AlarmManager只有当设备处于唤醒状态时才会被调用,我们只要设updatePeriodMillis为0即可。

linitialLayout属性标识了初始布局文件。

lconfigure属性定义了当用户添加App
Widget时调用的Activity。(这是可选的)

lpreviewImage定义了App
Widget的缩略图,当用户从widget列表中选择时,显示的就是这张图。如果没设置,用户将看见的是你的应用的默认图标。

lautoAdvanceViewId属性是在Android3.0引入的,用于标识需要被host(launcher)自动更新的widget的子视图。

l resizeMode属性标识了widget重新布局的规则。你可以使用该属性来让widget能够在水平、竖直、或两个方向上均可变化。可用的值包括horizontal、vertical、none。如果是想在两个方向上均能拉伸,可设置为horizontal|vertical,当然,需要Android3.1以上版本。


创建App Widget的布局

要创建你的App Widget的初始布局,你可以使用以下View对象。

创建布局不是很麻烦,重点是,你必须记住,这个布局是基于RemoteViews的,不是所有的布局类型与View都支持。

一个RemoteViews对象可以支持以下布局类:

FrameLayout

LinearLayout

RelativeLayout

以及一下widget类

AnalogClock

Button

Chronometer

ImageButton

ImageView

ProgressBar

TextView

ViewFlipper

注:这些类的子类是不支持的。

为App
Widget添加边距

作为一个widget,它不应该扩展到屏幕的边缘,同时在视觉上,不应该与其它widget相混淆。因此,你需要为你的widget在四个方向上添加边距。

在Android4.0中,所有的widget都自动添加了内边距,并且提供了更好的对齐方案。要利用这种优势,建议设置应用的targetSdkVersion到14或更高。

为了支持以前的设备,你也可以为早些的平台写一个包含边距的布局,而对于Android4.0以上的,则不设置:

1. 设置targetSdkVersion到14或更高

2. 创建一个布局:

Xml代码



<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

Xml代码



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

res/values-v14/dimens.xml

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,你可以使用以下的实现:

Java代码



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的事件。

Xml代码



<activity android:name=".ExampleAppWidgetConfigure">

<intent-filter>

<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>

</intent-filter>

</activity>

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

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

Java代码



Intent intent = getIntent();

Bundle extras = intent.getExtras();

if (extras != null) {

mAppWidgetId = extras.getInt(

AppWidgetManager.EXTRA_APPWIDGET_ID,

AppWidgetManager.INVALID_APPWIDGET_ID);

}

2、 执行配置过程

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

Java代码



AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);

4、 更新App
Widget

Java代码



RemoteViews views = new RemoteViews(context.getPackageName(),

R.layout.example_appwidget);

appWidgetManager.updateAppWidget(mAppWidgetId, views);

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

Java代码



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中看到该预览图。如果没设置该字段,系统将使用应用的图标。

Xml代码



<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视图,可以按照如下方式:

Xml代码



<service android:name="MyWidgetService"

...

android:permission="android.permission.BIND_REMOTEVIEWS" />


Collection的布局

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

Xml代码



<?xml version="1.0" encoding="utf-8"?>

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"

android:layout_width="match_parent"

android:layout_height="match_parent">

<StackView xmlns:android="http://schemas.android.com/apk/res/android"

android:id="@+id/stack_view"

android:layout_width="match_parent"

android:layout_height="match_parent"

android:gravity="center"

android:loopViews="true" />

<TextView xmlns:android="http://schemas.android.com/apk/res/android"

android:id="@+id/empty_view"

android:layout_width="match_parent"

android:layout_height="match_parent"

android:gravity="center"

android:background="@drawable/widget_item_background"

android:textColor="#ffffff"

android:textStyle="bold"

android:text="@string/empty_view_text"

android:textSize="20sp" />

</FrameLayout>

注意:empty view是当没数据时显示的视图。

除了App Widget的整体视图,你还需要为collection中的每一项定义布局。比如说,StackView
Widget的例子中,只有一种。而WeatherListWidget的例子却有两种布局文件。


带有collection的AppWidgetProvider

与普通App Widget的唯一区别就是,在带有collection的AppWidgetProvider.onUpdate中,你需要调用setRemoteAdapter方法。通过调用这个方法,collection就知道如何获取它的数据。然后你在RemoteViewsService中返回一个RemoteViewsFactory,这样widget就能获取到相应的数据了。另外,当你调用setRemoteAdapter时,你需要传入一个Intent。这个Intent指向了RemoteViewsService的实现,并指明了App
Widget的ID。

Java代码



public void onUpdate(Context context, AppWidgetManager appWidgetManager,

int[] appWidgetIds) {

// update each of the app widgets with the remote adapter

for (int i = 0; i < appWidgetIds.length; ++i) {

// Set up the intent that starts the StackViewService, which will

// provide the views for this collection.

Intent intent = new Intent(context, StackWidgetService.class);

// Add the app widget ID to the intent extras.

intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);

intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));

// Instantiate the RemoteViews object for the App Widget layout.

RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.widget_layout);

// Set up the RemoteViews object to use a RemoteViews adapter.

// This adapter connects

// to a RemoteViewsService through the specified intent.

// This is how you populate the data.

rv.setRemoteAdapter(appWidgetIds[i], R.id.stack_view, intent);

// The empty view is displayed when the collection has no items.

// It should be in the same layout used to instantiate the RemoteViews

// object above.

rv.setEmptyView(R.id.stack_view, R.id.empty_view);

//

// Do additional processing specific to this app widget...

//

appWidgetManager.updateAppWidget(appWidgetIds[i], rv);

}

super.onUpdate(context, appWidgetManager, appWidgetIds);

}

RemoteViewsService类


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

你需要执行以下操作:

1、 继承RemoteViewsService

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

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


RemoteViewsFactory接口

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

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

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

Java代码



class StackRemoteViewsFactory implements

RemoteViewsService.RemoteViewsFactory {

private static final int mCount = 10;

private List<WidgetItem> mWidgetItems = new ArrayList<WidgetItem>();

private Context mContext;

private int mAppWidgetId;

public StackRemoteViewsFactory(Context context, Intent intent) {

mContext = context;

mAppWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,

AppWidgetManager.INVALID_APPWIDGET_ID);

}

public void onCreate() {

// In onCreate() you setup any connections / cursors to your data source. Heavy lifting,

// for example downloading or creating content etc, should be deferred to onDataSetChanged()

// or getViewAt(). Taking more than 20 seconds in this call will result in an ANR.

for (int i = 0; i < mCount; i++) {

mWidgetItems.add(new WidgetItem(i + "!"));

}

...

}

...

getViewAt用于返回特定位置上的RemoteViews对象。

Java代码



public RemoteViews getViewAt(int position) {

// Construct a remote views item based on the app widget item XML file,

// and set the text based on the position.

RemoteViews rv = new RemoteViews(mContext.getPackageName(), R.layout.widget_item);

rv.setTextViewText(R.id.widget_item, mWidgetItems.get(position).text);

...

// Return the remote views object.

return rv;

}

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


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


在onUpdate中设置pending
intent template

Java代码



// This section makes it possible for items to have individualized behavior.

// It does this by setting up a pending intent template. Individuals items of a collection

// cannot set up their own pending intents. Instead, the collection as a whole sets

// up a pending intent template, and the individual items set a fillInIntent

// to create unique behavior on an item-by-item basis.

Intent toastIntent = new Intent(context, StackWidgetProvider.class);

// Set the action for the intent.

// When the user touches a particular view, it will have the effect of

// broadcasting TOAST_ACTION.

toastIntent.setAction(StackWidgetProvider.TOAST_ACTION);

toastIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);

intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));

PendingIntent toastPendingIntent = PendingIntent.getBroadcast(context, 0, toastIntent,

PendingIntent.FLAG_UPDATE_CURRENT);

rv.setPendingIntentTemplate(R.id.stack_view, toastPendingIntent);


在RemoteViewsFactory中设置fill-in
intent

Java代码



public RemoteViews getViewAt(int position) {

// position will always range from 0 to getCount() - 1.

// Construct a RemoteViews item based on the app widget item XML file, and set the

// text based on the position.

RemoteViews rv = new RemoteViews(mContext.getPackageName(), R.layout.widget_item);

rv.setTextViewText(R.id.widget_item, mWidgetItems.get(position).text);

// Next, set a fill-intent, which will be used to fill in the pending intent template

// that is set on the collection view in StackWidgetProvider.

Bundle extras = new Bundle();

extras.putInt(StackWidgetProvider.EXTRA_ITEM, position);

Intent fillInIntent = new Intent();

fillInIntent.putExtras(extras);

// Make it possible to distinguish the individual on-click

// action of a given item

rv.setOnClickFillInIntent(R.id.widget_item, fillInIntent);

...

// Return the RemoteViews object.

return rv;

}

保证Collection的数据是最新的


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





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

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

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

如果getViewAt方法耗时太长,加载视图(可由RemoteViewsFactory的getLoadingView获取)将显示在collection视图所在的区域。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: