3.5 App Widgets
App Widgets是指能够嵌入到其他应用程序中的小组件,并且能够周期性地进行更新。App Widgets并不是Android应用程序的核心组件,但应用程序开发确实是不可或缺的部分。我们可以通过App Widgets使UI界面更多样化,也可以通过App Widget provider发布我们自己开发的App Widgets组件。一个能够用于容纳App Widgets组件的应用程序组件被称为App Widgets Host(App Widgets宿主),例如下图所示的音乐播放程序。
图3.3 App Widgets Host
Android5中涉及的部分App Widgets类的使用方法会在第四章进行详细介绍,本部分主要对如何使用App Widget Provider发布自己的App Widget组件的方法进行简单介绍。
3.5.1 基础知识
为了创建一个自己的App Widget,需要完成以下工作:
●AppWidgetProviderInfo元数据
定义在XML文件中的用于描述App Widget的元数据对象,比如App Widget的布局,更新频率,以及相关的AppWidgetProvider类。
●实现AppWidgetProvider类
在AppWidgetProvider类中定义了一系列方法,这些方法允许开发者以编程的方式和自己的App Widget进行交互,这种交互基于广播事件。当App Widget的状态发生改变,例如更新、启用、禁用和删除的时候,你都会接收到相应的广播通知。
●视图布局
在XML文件中为App Widget定义初始布局。
●实现App Widget配置Activity
这是一个可选的Activity,当用户添加App Widget时该Activtity会被启动,并允许用户在创建App Widget时修改相关设置。
下面进行详细介绍。
3.5.2 在Manifest文件中声明App Widget
首先,在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_UPDATE广播。这是唯一一个必须被显式声明的广播。当有必要的时候,AppWidgetManager会自动发送所有其他App Widget广播给AppWidgetProvider。
<meta-data>元素指定了AppWidgetProviderInfo资源并需要以下属性:
●android:name–指定元数据名称。
●android:resource–指定AppWidgetProviderInfo资源路径。
3.5.3 增加AppWidgetProviderInfo元数据
AppWidgetProviderInfo用于定义App Widget的一系列基本特性,例如最小布局的尺寸,初始的布局资源、刷新频率以及创建时要加载的配置Activity等。使用<appwidget-provider>元素标签在XML中定义AppWidgetProviderInfo对象并保存到项目的res/xml/目录下。例如:
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:minWidth="294dp" <!-- density-independent pixels --> android:minHeight="72dp" android:updatePeriodMillis="86400000" <!-- once per day --> android:initialLayout="@layout/example_appwidget" android:configure="com.example.android.ExampleAppWidgetConfigure" > </appwidget-provider>
其中:
●minWidth 和minHeight属性的值指定了这个App Widget布局需要的最小区域。
●updatePerdiodMillis属性定义了App Widget框架调用onUpdate()方法来从AppWidgetProvider请求一次更新的频率。实际上更新的时间并不精准。建议更新频率越低越好,比如一小时更新一次,这样可以节省电力,或者根据用户的配置调整更新频率,比如有个人每15分钟想查看一下股票的报价,这样可以将频率设置为一小时更新四次。
●initialLayout属性指向App Widget使用的布局的资源
●configure属性定义了该App Widget被加载时使用的配置Activity。
3.5.4 创建App Widget布局
必须在res/layout目录下以xml文件方式为App Widget定义一个布局文件。App Widget的布局是基于RemoteViews对象,而RemoteViews对象可以支持以下布局:
Framelayout LinearLayout RelativeLayout GridLayout
和以下的小组件类:
AnalogClock Button Chronometer ImageButton ImageView ProgressBar TextView ViewFlipper ListView GridView StackView AdapterViewFlipper
但是并不支持它们的派生类。
此外,RemoteView还支持ViewStub,该组件不可见,自身无尺寸,可用于对布局资源进行支撑。
3.5.5 为App Widget添加边界
若没有为自定义的Widget定义边界,它就会自动扩展到屏幕大小。因此,我们需要为自定义的App Widget定义边界。
自Android4.0开始,App Widget会自动在Widget的边界环绕盒之间添加空隙,以便为Widget和其他小组件以及屏幕上的图标提供更好的排列组合方式。为实现这一个行为,我们需要将应用程序中的“targetSdkVersion”属性设置为大于14。
实际上,我们可以自己定义一个带有自定义边界的布局,并且使该布局在应用于早期平台版本时正常显示边界,而在Android4.0以后版本的平台上不显示额外边界。定义过程如下:
(1)设置targetSdkVersion为大于14的值
(2)创建一个布局,并为其设置dimension资源,其边界信息由dimension资源设定。代码如下:
<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)创建两个dimension资源,一个在res/values/目录下,用于提供早于Android4.0版本的系统的边界信息,另外一个在res/values-v14下,用于提供高于Android4.0版本的操作系统的边界信息。
例如,res/values/dimens.xml定义如下:
<dimen name="widget_margin">8dp</dimen>
而res/values-v14/dimens.xml定义如下:
<dimen name="widget_margin">0dp</dimen>
3.5.6 使用AppWidgetProvider类
首先,AppWidgetProvider类是BroadcastReceiver类的子类,可以方便地处理App Widget发出的广播,因此,其必须被声明在清单文件中的<receiver>元素中。AppWidgetProvider只接受和相应的App Widget相关的广播消息,例如这个App Widget被更新、被删除、被启用或者被禁用的时候。当这些广播事件发生的时候,AppWidgetProvider会接收到以下一系列方法的调用请求:
●onUpdate():每间隔一定时间该方法就会被调用用于对App Widget进行更新。间隔时间由AppWidgetProviderInfo 元数据中的updatePeriodMillis 属性指定。当用户添加App Widget时该方法也会被调用。因此该方法中应该执行必要的操作,例如为视图定义事件处理器或者启动一个临时的服务等。如果你为App Widget定义了配置Activity,则应该由配置Activity负责进行第一次更新,而onUpdate()方法不会在用户执行添加操作的时候被调用,而只会在后期的更新时被调用。
●onAppWidgetOptionsChanged():该方法在当Widget被首次放置到应用程序中或者Widget的尺寸被更改时被调用。
●onDeleted(Context, int[]):该方法在App Widget被从App Widget宿主中删除的时候被调用。
●onEnabled(Context):该方法在App Widget的第一个实例被创建时被调用。如果用户添加了两个App Widget的实例,则该方法只会在第一次添加时被调用。如果你需要进行打开数据库或者进行其他只需要进行一次的设置,那么将代码放在这个方法中是个不错的主意。
●onDisabled(Context):该方法在最后一个App Widget实例从App Widget宿主中被删除的时候调用。在该方法中你应该对在onEnabled()方法中的操作进行善后,例如删除一个临时的数据库。
●onReceive(Context, Intent):每当接收到一个广播,该方法都会被调用。并且,该方法会在上述各个方法之前被调用。通常我们不需要重写该方法,因为默认的AppWidgetProvider类已经很好地实现了对所有广播的过滤和处理方法的调用。
可见onUpdate()方法是最重要的回调方法,如果你创建的App Widget不需要进行创建临时文件等操作的话,那么你可能只需要定义onUpdate()方法就可以了。例如,当你创建了一个带有Button的App Widget,当单击按键时会启动一个Activity,那么你的AppWidgetProvider类应该像下面这样定义:
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); } } }
其中appWidgetIds是一个存放ID的数组,其中的每一个ID值都标识一个AppWidgetProvider创建的App Widget。如果该数组中存放了多个App Widget的ID,那么这些App Widget会被同步更新。
3.5.7 接收App Widget的广播
如果你想直接用自己的类接收并处理App Widget的广播,那么你需要实现自己的BroadcastReceiver,重写onReceiver()方法,并处理以下四个intent:
●ACTION_APPWIDGET_UPDATE
●ACTION_APPWIDGET_DELETED
●ACTION_APPWIDGET_ENABLED
●ACTION_APPWIDGET_DISABLED
3.5.8 创建App Widget的配置Activity
如果想让用户在添加新的App Widget的时候对颜色、尺寸、更新周期等属性进行配置,那么就需要创建一个配置Activity。配置Activity会在App Widget被创建时由其宿主启动。
该配置Activity需要在Manifest文件中进行声明,通过ACTION_APPWIDGET_CONFIGURE活动被宿主启动。代码如下:
<activity android:name=".ExampleAppWidgetConfigure"> <intent-filter> <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" /> </intent-filter> </activity>
此外,该Activity还需要在AppWidgetProviderInfo XML中通过android:configure属性被声明。例如:
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" ... android:configure="com.example.android.ExampleAppWidgetConfigure" ... > </appwidget-provider>
当为App Widget定义了配置Activity后,Widget在被创建时不会再调用onUpdate方法。
3.5.9 使用配置Activity对App Widget进行更新
当Widget使用了配置Activity后,配置Activity会在用户完成设置后对Widget进行更新。通过配置Activity对Widget进行更新并关闭配置Activity的过程如下:
(1)首先,从启动Activity的Intent中获取到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)执行App Widget配置
(3)完成配置后,获取AppWidgetManager类的实例
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
(4)通过RomoteViews布局对App Widget进行更新
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.example_appwidget); appWidgetManager.updateAppWidget(mAppWidgetId, views);
(5)最后,创建返回Intent,设置Activity返回值,并关闭Activity。
Intent resultValue = new Intent(); resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId); setResult(RESULT_OK, resultValue); finish();