App Widgets
App Widgets是一类视图较小的应用程序,它们可以内嵌在其它应用程序中(比如主屏)并 接收定时更新。在用户接口中,这类widget是以一些view视图呈现的,我们可以使用App Widget provider表述一个这种widget。可以内嵌App Widgets的应用程序组件称作App Widget host。下图是一个Music App Widget的截屏。
本篇介绍如何使用App Widget provider编写一个App Widget。有关创建自己的AppWidgetHost的讨论,参见App Widget Host部分。
Widget Design
更多有关app widget设计的信息,请阅读Widgets设计向导部分。
The Basics
创建一个App Widget,需要如下内容:
AppWidgetProviderInfo对象——描述App Widget的metadata,例如App Widget's的layout,更新频率和AppWidgetProvider类。这需要定义在XML文件中。
AppWidgetProvider类实现——定义了我们和App Widget交互的基于广播事件的接口方法。通过它,接收App Widget更新,enabled,disabled和删除时的广播。
View layout——定义了App Widget的初始layout。定义在XML文件中。
除此之外,我们还可以定义一个App Widget的配置Activity。这是一个可选的Acitivity提供给用户在创建App Widget的时候修改App Widget的设置。
Declaring an App Widget in the Manifest
首先,在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_UPDATEG广播。这是我们唯一需要明确指明的广播。AppWidgetManager自动在需要的时候向AppWidgetProvider发送App Widget相关广播。
<meta-data>标签指定AppWidgetProviderInfo资源,它要求下列属性:
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="40dp"
android:minHeight="40dp"
android:updatePeroidMillis="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>
<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/文件夹中提供Android4.0之前的自定义margin。res/values-v14文件夹中为Android4.0 widgets定义无额外padding.
<dimen name="widget_margin">8dp</dimen>
<dimen name="widget_margin">0dp</dimen>
public class ExampleAppWidgetProvider extends AppWidgetProvider{ public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds){ final int N = appWidgetIds.length; for(int i = 0; i < N; i++){ int appWidgetId = appWidgetIds[i]; Intent intent = new Intent(context, ExampleActivity.class); PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0); RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.appwidget_provider_layout); views.setOnClickPendingIntent(R.id.button, pendingIntent); appWidgetManager.updateAppWidget(appWidgetId, views); } } }这个AppWidgetProvider定义了onUpdate()方法,其中定义了PendingIntent启动一个Activity,并且根据setOnClickPendingIntent(int, PendingIntent)附着于App Widget按钮。注意到它包括一个循环。这个循环处理所有添加AppWidget的ID操作。这样,用户添加了多个App Widget实例同时进行更新。例如一个app widget两小时更新一次。而用户在一个小时之后添加了另外一个实例,那么这两个实例都会在第一个实例的两小时之后更新,而不再是第二个实例的两小时之后。
<activity android:name=".ExampleAppWidgetConfigure"> <intent-filter> <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/> </intent-filter> </activity>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" ... android:configure="com.example.android.ExampleAppWidgetConfigure" ... > </appwidget-provider>
Intent intent = getIntent(); Bundle extras = intent.getExtras(); if (extras != null) { mAppWidgetId = extras.getInt( AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); }
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
RemoteViews views = new RemoteViews(context.getPackageName(),
R.layout.example_appwidget);
appWidgetManager.updateAppWidget(mAppWidgetId, views);
Intent resultValue = new Intent();
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
setResult(RESULT_OK, resultValue);
finish();
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" ... android:previewImage="@drawable/preview"> </appwidget-provider>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" ... android:widgetCategory="keyguard|home_screen"> </appwidget-provider>
AppWidgetManager appWidgetManager; int widgetId; Bundle myOptions = appWidgetManager.getAppWidgetOptions(widgetId); int category = myOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY, -1); boolean isKeyguard = category == AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD;一旦知道了widget的category,就可以有选择的装载baselayout,设置不同的属性等。例如
int baseLayout = isKeyguard ? R.layout.keyguard_widget_layout :R.layout_widget_layout;
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. }
<service android:name="MyWidgetService" ... android:permission="android.permission.BIND_REMOTEVIEWS" />
<?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>
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds){ for(int i = 0; i < appWidgetIds.length; ++i){ Intent intent = new Intent(context, StackWidgetService.class); 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); rv.setEmptyView(R.id.stack_view, R.id.empty_view); appWidgetManager.updateAppWidget(appWidgetIds[i], rv); } super.onUpdate(context, appWidgetManager, appWidgetIds); }
如上所述,RemoteViewsService子类提供RemoteViewsFactory用来放置远程容器视图。特别的,需要做以下两步:
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(){ for(int i = 0; i < mCount; i++){ mWidgetItems.add(new WidgetItem(i + "!")); } ... } ... }
public RemoteViews getViewAt(int position){ RemoteViews rv = new RemoteViews(mContext.getPackageName(), R.layout.widget_item); rv.setTextViewText(R.id.widget_item, mWidgetItems.get(position).text); ... return rv; }
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"; @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) { for (int i = 0; i < appWidgetIds.length; ++i) { Intent intent = new Intent(context, StackWidgetService.class); intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]); 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); rv.setEmptyView(R.id.stack_view, R.id.empty_view); Intent toastIntent = new Intent(context, StackWidgetProvider.class); 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); } }Setting the fill-in 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; } ... }