一、AppWidgetProvider
首先一个Provider的配置:
<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>元素必须有name属性,用于指定继承了AppWidgetProveder的类。
<intent-filter>元素必须包含拥有name属性的<action>元素,上面的变量值是指定它能接收广播— —ACTION_APPWIDGET_UPDATE,这个广播动作是必须有的!必要时AppWidgetManager会自动把所有其它应用控件广播都发送到这个AppWidgetProvider。
<meta-data>元素指定AppWidgetProviderInfo资源并且要求包含以下两个元素:
android:name 指定metadata名称,使用“android.appwidget.provider“作为AppWidgetProviderInfo的描述符号来标记数据文件 。
android:resource 指定AppWidgetProviderInfo资源路径。
创建AppWidgetProviderInfo文件,一般创建在项目的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>
minWidth&minHeight:Home屏幕将应用控件放置在基于长宽大小固定的单元格所组成的网格窗口上。如果应用控件的最小长宽不符合单元格的尺寸,那么应用控件尺寸会向单元格的大小取整。注意长宽不能大于4个单元格大小。大小参考:
updatePeriodMillis:定义应用控件框架多长时间更新一次,通过调用AppWidgetProvider 的onUpdate()方法。现实是更新不会保证在这个确切的时间发生,而且建议这种周期性的更新一个小时内不要超过1次以保护电池。你也可以允许用户在设置中调节频率。
initialLayout:应用控件的布局文件。
configure:定义一个当用户添加应用控件时会被启动的Activity,方便它去配置应用控件的参数。可选。
previewImage:当用户想要添加应用控件时,在控件表单中所看到的图片。默认是应用的图标。
resizeMode:指定控件能在哪一方面被重新调整大小。你可以设置它来使桌面屏幕在水平(horizontally)、垂直(vertically)、或者双方向可被重新调整大小。
widgetCategory:定义你的控件是否可以被显示在桌面(home_screen)上、锁屏(keyguard)上、或者两者。只有安卓5.0以下(不包括)才能显示在锁屏中!!
AppWidgetProvider
主要方法:
onUpdate():官方解释它会响应AppWidgetManager.ACTION_APPWIDGET_UPDATE和AppWidgetManager.ACTION_APPWIDGET_RESTORED的广播事件,即只有在控件被新建的时候才会调用此方法;而如果你给它绑定了一个configuration Activity,那么此方法就不会在创建控件的时候被调用,但会被随后的更新所调用。当配置被设定时,它负责configuratioin Activity执行第一次更新。
onUpdate()方法是最最重要的方法,当你要设置一些与用户交互的事件,必须在这里设置。如果你的控件不创建临时文件、数据库,或者要执行其它需要清理的工作时,这个方法可能是你唯一需要实现的。多个控件都会被同时更新,但是只有一个updatePeriodMillis时间表。比如一个时间表被定义为2个小时更新一次,第二个控件在第一个控件的一个小时后被创建,那么两个控件的更新都会跟着第一个被创建的控件的时间,而不会说两个控件交替着一个小时更新一个。
onReceive():像广播接受器一样,每次都能接受到它筛选后符合Action的Intent,因为它继承了BroadcastReceiver类,所以在Provider中不能保存任何对象!如果要在Provider中执行类似网络请求数据等,要求对象长期存在的工作,就要在此Provider的onUpdate()方法中去开启Service。它的Intent参数的toString()方法打印:
Intent { act=zyf.test.widget.UP flg=0x10000010 cmp=com.example.admin.mydemo/.widget.AppWidgetProvider bnds=[539,197][639,293] (has extras) }
猜测后面的两个数组是组件的位置信息!
onDeleted():当控件从应用控件host中被删除时调用。
onEnabled():当应用的第一个控件被添加时调用。
onDisabled():当最后一个控件对象从应用控件host中被删除时调用。在这里你应该清理所有已经完成的工作,如删除临时数据库。
在android.appwidget.AppWidgetProvider类的onReceive(Context context, Intent intent)方法中,可以通过AppWidgetManager.getInstance(context)静态方法来获取AppWidgetManager对象,因为它是一个单例,所以它跟onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds)所返回的AppWidgetManager对象是同一个。
二、App Widget Configuration Activity
假如你想让用户在添加新控件的时候可以设置配置,那就创建一个配置应用控件的Activity吧。这个Activity会被应用控件host自动启动,并允许用户去配置一些有效设置,如控件创建后的颜色、大小、更新周期或者其它功能性的设置。这个Activity也应该在Manifest中声明,但是它应该被应用控件host通过ACTION_APPWIDGET_CONFIGURE来启动,所以Activity需要去接收这个Intent,如:
<activity android:name=".ExampleAppWidgetConfigure">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
</intent-filter>
</activity>
而且也必须在AppWidgetProviderInfo XML文件中用android:configure配置。注意,Activity必须写出完整的包路径,因为引用它的不限于此包范围内。通过以上配置后,要开启一个Activity还需要注意两个件事:
1)App Widget host调用配置Activity,而它要返回一个结果。这个结果包含了由启动了Activity的Intent所传进去的控件ID(保存在Intent的Extras中标记为“EXTRA_APPWIDGET_ID”)。
2)当Activity被创建时onUpdate()方法不会被调用(即系统不会发送ACTION_APPWIDGET_UPDATE 广播)。它是在控件第一次创建时,负责请求AppWidgetManager更新的configuration Activity。但是onUpdate()会被随后的更新所调用--只会跳过第一次。
当控件使用了配置Activity,在配置完成后Activity要负责更新控件。你可以直接调用AppWidgetManager来执行更新。下面是一个正确更新控件和关闭Activity的例子:
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)进行控件的配置。(Perform your App Widget configuration.)
3)配置完成后通过调用getInstance(Context)来获取AppWidgetManager对象
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
4)调用updateAppWidget(int, RemoteViews)方法用RemoteViews布局来更新控件
RemoteViews views = new RemoteViews(context.getPackageName(),
R.layout.example_appwidget);
appWidgetManager.updateAppWidget(mAppWidgetId, views);
5)最后创建要返回的Intent,将它设置为Activity的结果Result,然后关闭Activity
Intent resultValue = new Intent();
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
setResult(RESULT_OK, resultValue);
finish();
温馨提示:当你的配置Activity首次开启时,将Activity的Result设置为RESULT_CANCELED。这样,在Activity还没执行完就被用户退出的时候,控件host才能知道取消了配置,并且不会添加控件。
三、集合的方式使用应用控件
安卓3.0以集合的方式介绍了应用控件。这种控件使用RemoteViewsService去展示通过远程数据返回的集合,比如从content provider。这些RemoteViewsService提供的数据是在使用了以下View类型之一的控件中给出,这些View我们都可以用于“collection views”:ListView、GridView、StackView、AdatperViewFlipper。
当请求集合中指定的一项时,RemoteViewsFactory会为集合创建并返回一项作为RemoteViews对象。要在控件中包含集合视图,需要实现RemoteViewsService和RemoteViewsFactory。前者是一个允许远程适配器请求RemoteViews对象的服务,后者是集合视图与视图底层数据之间适配的接口,你的实现是负责为数据集中的每个项创建一个RemoteViews对象。重要实现代码:(例子名为:StackView Widget sample官网找不到,另找途径下载!)
public class StackWidgetService extends RemoteViewsService {
@Override
public RemoteViewsFactory onGetViewFactory(Intent intent) {
return new StackRemoteViewsFactory(this.getApplicationContext(), intent);
}
}
class StackRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {
//... include adapter-like methods here. See the StackView Widget sample.
}
以集合的方式实现应用控件:
1)Manifest配置
为了确保使用集合的控件能够绑定到RemoteViewsService,除了普通的声明Service外还需要使用权限BIND_REMOTEVIEWS。这是为了防止其它应用随意访问你的应用控件的数据。如下:
<service android:name="MyWidgetService"
...
android:permission="android.permission.BIND_REMOTEVIEWS" />
2)布局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>
3)AppWidgetProvider类
一个普通的应用控件,AppWidgetProvider子类的代码量一般在onUpdate()。而当用集合的方式实现控件时实现onUpdate()方法的主要不同点是要调用setRemoteAdapter(),它告诉集合视图去哪里获取它的数据。然后RemoteViewsService会返回RemoteViewsFactory的实现,控件就能准备(装填)数据了。当你调用 方法时必须传递一个Intent,该Intent指向RemoteViewsService的实现和指定要更新的控件的ID。
下面的例子告诉你StackView如何实现onUpdate()回调方法来将RemoteViewsService设置为控件集合的远程适配器:
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);
}
4)RemoteViewsService类
前面介绍过了,在这里提一下持久数据。不能在Service的单独对象或者任何Service所包含的数据中去实现数据的持久化。因此不能将任何数据储存在RemoteViewsService中(除非它是静态的)。如果你需要持久化控件的数据,最好的方式是使用ContentProvider,它的数据不受进程周期影响。
5)RemoteViewsFactory接口
要实现的两个最重要的方法onCreate()和getViewAt()。当第一次创建你自定义的工厂类时系统调用onCreate()方法。在这里设置任何指向数据源的连接和/或者游标。如下面例子在该方法中实例化WidgetItem对象数组。当你的控件开发活动,系统会访问这些使用他们在数组中的指针地址的对象,然后展示它们所包含的文本。
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;
}
6)为每个项添加行为
一般我们都是用setOnClickPendingIntent()来为RemoteViews中的Button等组件添加点击事件,但这方法不能使用在视图集合的子视图上(如ListView的一个Item点击事件)。这里要用setOnClickFillInIntent()来代替。这需要为你的集合视图设置一个PendingIntent模板,然后通过RemoteViewsFactory为集合的每一项设置一个填充(fill-in)intent。
在StackView Widget例子中是如何实现的呢:
a. StackWidgetProvider(AppWidgetProvider子类)创建一个包含名称为“TOAST_ACTION”的自定义action的pending intent。
b. 当用户触摸一个视图时intent就会发射并广播TOAST_ACTION。
c. 这个广播会被StackWidgetProvider的onReceive()方法拦截,然后应用控件显示所触摸的视图的Toast信息。这些数据来自RemoteViewsFactory所提供的集合项,通过RemoteViewsService获取。
6.1)设置pending intent模板
一个集合的独立的项不能自己设置pending intent。而是要集合的整体去设置一个pending intent模板,而单独的项通过一个接着一个的方式来设置一个fill-in intent去创建唯一的行为。看例子:
public class StackWidgetProvider extends AppWidgetProvider {
public static final String TOAST_ACTION = "com.example.android.stackwidget.TOAST_ACTION";
public static final String 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
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 StackViewService that 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 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 = 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 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 = 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);
appWidgetManager.updateAppWidget(appWidgetIds[i], rv);
}
super.onUpdate(context, appWidgetManager, appWidgetIds);
}
}
6.2)设置fill-in intent
RemoteViewsFactory必须在每个集合的项上设置fill-in intent。这样才能区别给定项的独特点击动作。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 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);
}
// 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(new WidgetItem(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 = 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;
}
...
}
7)让集合数据保持最新的流程图
下图阐述了当发生更新时,使用了集合的应用控件中产生的数据流。它演示控件代码如何与RemoteViewsFactory交互,还有如何监听更新: