构建应用微件  

您所在的位置:网站首页 vivo小组件怎么设置大小 构建应用微件  

构建应用微件  

2024-01-01 13:00| 来源: 网络整理| 查看: 265

应用微件是可以嵌入其他应用(如主屏幕)并接收定期更新的微型应用视图。这些视图称为界面中的微件,您可以使用应用微件提供程序发布微件。能够容纳其他应用微件的应用组件称为应用微件托管应用。下面的屏幕截图显示了音乐应用微件。

本文档介绍如何使用应用微件提供程序来发布应用微件。如需了解如何创建您自己的 AppWidgetHost 来托管应用微件,请参阅应用微件托管应用。

注意:如需了解如何设计应用微件,请阅读应用微件概览。

基础知识

要创建应用微件,您需要:

AppWidgetProviderInfo 对象 描述应用微件的元数据,如应用微件的布局、更新频率和 AppWidgetProvider 类。此对象应在 XML 中定义。 AppWidgetProvider 类实现 定义允许您基于广播事件以编程方式与应用微件连接的基本方法。通过它,您会在更新、启用、停用和删除应用微件时收到广播。 视图布局 定义应用微件的初始布局,在 XML 中定义。

此外,您还可以实现应用微件配置 Activity。这是一个可选的 Activity,在用户添加您的应用微件时启动,并允许用户在应用微件创建时修改其设置。

下面几部分介绍如何设置上述各个组件。

在清单中声明应用微件

首先,在应用的 AndroidManifest.xml 文件中声明 AppWidgetProvider 类。例如:

元素需要 android:name 属性,该属性指定应用微件使用的 AppWidgetProvider。

元素必须包含一个具有 android:name 属性的 元素。此属性指定 AppWidgetProvider 接受 ACTION_APPWIDGET_UPDATE 广播。这是您必须明确声明的唯一一项广播。AppWidgetManager 会根据需要自动将其他所有应用微件广播发送到 AppWidgetProvider。

元素指定 AppWidgetProviderInfo 资源,并且需要以下属性:

android:name - 指定元数据名称。使用 android.appwidget.provider 将数据标识为 AppWidgetProviderInfo 描述符。 android:resource - 指定 AppWidgetProviderInfo 资源位置。 添加 AppWidgetProviderInfo 元数据

AppWidgetProviderInfo 定义应用微件的基本特性,如应用微件的最小布局尺寸、应用微件的初始布局资源、应用微件的更新频率,以及(可选)在应用微件创建时启动的配置 Activity。您可以使用单个 元素在 XML 资源中定义 AppWidgetProviderInfo 对象,并将其保存在项目的 res/xml/ 文件夹中。

例如:

下面简要说明了 属性:

minWidth 和 minHeight 属性的值指定应用微件默认情况下占用的最小空间。默认的主屏幕根据定义了高度和宽度的单元格的网格在其窗口中放置应用微件。如果应用微件的最小宽度或高度的值与单元格的尺寸不匹配,则应用微件的尺寸会向上舍入到最接近的单元格大小。

如需详细了解如何设置应用微件的大小,请参阅应用微件设计准则。

注意:为使应用微件能够在设备间移植,应用微件的最小大小不得超过 4 x 4 单元格。

minResizeWidth 和 minResizeHeight 属性指定应用微件的绝对最小大小。这些值应指定应用微件的大小低于多大就会难以辨认或无法使用。使用这些属性,用户可以将微件的大小调整为可能小于由 minWidth 和 minHeight 属性定义的默认微件大小。这些属性是在 Android 3.1 中引入的。

如需详细了解如何设置应用微件的大小,请参阅应用微件设计准则。

updatePeriodMillis 属性定义应用微件框架通过调用 onUpdate() 回调方法来从 AppWidgetProvider 请求更新的频率应该是多大。不能保证实际更新按此值正好准时发生,我们建议尽可能降低更新频率 - 或许不超过每小时一次,以节省电池电量。您也可以允许用户在配置中调整频率 - 有些人可能希望股票行情自动收录器每 15 分钟更新一次,另有一些人也可能希望它一天只更新 4 次。

注意:如果设备在到了该更新的时候(由 updatePeriodMillis 定义)处于休眠状态,则设备会唤醒以执行更新。如果您的更新频率不超过每小时一次,这样或许不会给电池续航时间造成严重问题。不过,如果您需要更频繁地更新和/或不需要在设备处于休眠状态时进行更新,则可以改为基于不会唤醒设备的闹钟来执行更新。为此,请使用 AlarmManager 设置一个具有 AppWidgetProvider 会接收的 Intent 的闹钟。将闹钟类型设为 ELAPSED_REALTIME 或 RTC,这样只有在设备处于唤醒状态时,闹钟才会响起。然后,将 updatePeriodMillis 设为零 ("0")。

initialLayout 属性指向用于定义应用微件布局的布局资源。 configure 属性定义要在用户添加应用微件时启动以便用户配置应用微件属性的 Activity。这是可选的(请阅读下文的创建应用微件配置 Activity)。 previewImage 属性指定预览来描绘应用微件经过配置后是什么样子的,用户在选择应用微件时会看到该预览。如果未提供,则用户会看到应用的启动器图标。此字段对应于 AndroidManifest.xml 文件的 元素中的 android:previewImage 属性。如需详细了解如何使用 previewImage,请参阅设置预览图片。此属性是在 Android 3.0 中引入的。 autoAdvanceViewId 属性指定应由应用微件的托管应用自动跳转的应用微件子视图的视图 ID。此属性是在 Android 3.0 中引入的。 resizeMode 属性指定可以按什么规则来调整微件的大小。您可以使用此属性来让主屏幕微件在横轴上可调整大小、在纵轴上可调整大小,或者在这两个轴上均可调整大小。用户可轻触并按住微件以显示其大小调整手柄,然后拖动水平和/或垂直手柄以更改布局网格上的大小。resizeMode 属性的值包括“horizontal”、“vertical”和“none”。要将微件声明为在水平和垂直方向上均可调整大小,请提供值“horizontal|vertical”。此属性是在 Android 3.1 中引入的。 minResizeHeight 属性指定可将微件大小调整到的最小高度(以 dp 为单位)。如果此字段的值大于 minHeight 或未启用垂直大小调整(请参阅 resizeMode),则此字段不起作用。此属性是在 Android 4.0 中引入的。 minResizeWidth 属性指定可将微件大小调整到的最小宽度(以 dp 为单位)。如果此字段的值大于 minWidth 或未启用水平大小调整(请参阅 resizeMode),则此字段不起作用。此属性是在 Android 4.0 中引入的。 widgetCategory 属性声明应用微件是否可以显示在主屏幕 (home_screen) 和/或锁定屏幕 (keyguard) 上。只有低于 5.0 的 Android 版本才支持锁定屏幕微件。对于 Android 5.0 及更高版本,只有 home_screen 有效。

如需详细了解 元素接受的属性,请参阅 AppWidgetProviderInfo 类。

创建应用微件布局

您必须在 XML 中定义应用微件的初始布局,并将其保存在项目的 res/layout/ 目录中。您可以使用下面列出的视图对象来设计应用微件,但在开始设计应用微件之前,请先阅读并了解应用微件设计准则。

如果您熟悉布局,那么创建应用微件布局非常简单。不过,您必须知道,应用微件布局基于 RemoteViews,并不是每种布局或视图微件都受其支持。

RemoteViews 对象(因而应用微件)可以支持以下布局类:

FrameLayout LinearLayout RelativeLayout GridLayout

以及以下微件类:

AnalogClock Button Chronometer ImageButton ImageView ProgressBar TextView ViewFlipper ListView GridView StackView AdapterViewFlipper

不支持这些类的后代。

RemoteViews 还支持 ViewStub,它是一个大小为零的不可见视图,您可以使用它在运行时以懒散的方式扩充布局资源。

向应用微件添加外边距

微件通常不应扩展到屏幕边缘,也不应在视觉上与其他微件齐平,因此您应在微件框架的四周添加外边距。

从 Android 4.0 开始,系统会自动在微件框架与应用微件的边界框之间为应用微件留出内边距,以使应用微件与用户主屏幕上的其他微件和图标更好地对齐。要利用这种强烈建议的行为,请将应用的 targetSdkVersion 设为 14 或更高版本。

一种简单的做法是编写单个布局,对较低版本的平台上的微件应用自定义外边距,不对 Android 4.0 及更高版本的平台上的微件应用额外的外边距:

将应用的 targetSdkVersion 设为 14 或更高版本。 创建如下所示的布局,为其外边距引用尺寸资源: … 创建两个尺寸资源:一个在 res/values/ 中,该资源为低于 Android 4.0 的平台上的微件提供自定义外边距;一个在 res/values-v14/ 中,该资源不为 Android 4.0 平台上的微件提供额外的内边距:

res/values/dimens.xml:

8dp

res/values-v14/dimens.xml:

0dp

另一种做法是默认情况下直接将额外的外边距内置到九宫格背景资源中,并为 API 级别 14 或更高级别的平台上的微件提供没有外边距的不同九宫格。

使用 AppWidgetProvider 类

AppWidgetProvider 类扩展了 BroadcastReceiver 作为一个辅助类来处理应用微件广播。AppWidgetProvider 仅接收与应用微件有关的事件广播,例如当更新、删除、启用和停用应用微件时发出的广播。当发生这些广播事件时,AppWidgetProvider 会接收以下方法调用:

onUpdate() 调用此方法可以按 AppWidgetProviderInfo 中的 updatePeriodMillis 属性定义的时间间隔来更新应用微件(请参阅上文的添加 AppWidgetProviderInfo 元数据)。当用户添加应用微件时也会调用此方法,所以它应执行基本设置,如定义视图的事件处理脚本以及根据需要启动临时的 Service。不过,如果您已声明配置 Activity,则当用户添加应用微件时不会调用此方法,但会调用它来执行后续更新。由配置 Activity 负责在配置完成后执行首次更新。(请参阅下文的创建应用微件配置 Activity。) onAppWidgetOptionsChanged() 当首次放置微件时以及每当调整微件的大小时,会调用此方法。您可以使用此回调来根据微件的大小范围显示或隐藏内容。您可以通过调用 getAppWidgetOptions() 来获取大小范围,该方法会返回包含以下各项的 Bundle: OPTION_APPWIDGET_MIN_WIDTH - 包含微件实例的当前宽度的下限(以 dp 为单位)。 OPTION_APPWIDGET_MIN_HEIGHT - 包含微件实例的当前高度的下限(以 dp 为单位)。 OPTION_APPWIDGET_MAX_WIDTH - 包含微件实例的当前宽度的上限(以 dp 为单位)。 OPTION_APPWIDGET_MAX_HEIGHT - 包含微件实例的当前高度的上限(以 dp 为单位)。 此回调是在 API 级别 16 (Android 4.1) 中引入的。如果您实现此回调,请确保您的应用不依赖于它,因为在旧款设备上不会调用它。 onDeleted(Context, int[]) 每次从应用微件托管应用中删除应用微件时,都会调用此方法。 onEnabled(Context) 首次创建应用微件的实例时,会调用此方法。例如,如果用户添加应用微件的两个实例,只有首次添加时会调用此方法。如果您需要打开一个新的数据库或执行只需要对所有应用微件实例执行一次的其他设置,则此方法非常合适。 onDisabled(Context) 从应用微件托管应用中删除了应用微件的最后一个实例时,会调用此方法。您应使用此方法来清理在 onEnabled(Context) 中完成的所有工作,如删除临时数据库。 onReceive(Context, Intent) 针对每个广播调用此方法,并且是在上述各个回调方法之前调用。您通常不需要实现此方法,因为默认的 AppWidgetProvider 实现会过滤所有应用微件广播并视情况调用上述方法。

您必须在 AndroidManifest 中使用 元素将 AppWidgetProvider 类实现声明为广播接收器(请参阅上文的在清单中声明应用微件)。

最重要的 AppWidgetProvider 回调是 onUpdate(),因为向托管应用添加每个应用微件时都会调用它(除非您使用配置 Activity)。如果应用微件接受任何用户交互事件,则您需要在此回调中注册事件处理脚本。如果应用微件未创建临时文件或数据库,或者未执行其他需要清理的工作,则 onUpdate() 可能是您需要定义的唯一一个回调方法。例如,如果您希望应用微件具有一个在用户点击时会启动 Activity 的按钮,则可以使用以下 AppWidgetProvider 实现:

Kotlin class ExampleAppWidgetProvider : AppWidgetProvider() { override fun onUpdate( context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray ) { // Perform this loop procedure for each App Widget that belongs to this provider appWidgetIds.forEach { appWidgetId -> // Create an Intent to launch ExampleActivity val pendingIntent: PendingIntent = Intent(context, ExampleActivity::class.java) .let { intent -> PendingIntent.getActivity(context, 0, intent, 0) } // Get the layout for the App Widget and attach an on-click listener // to the button val views: RemoteViews = RemoteViews( context.packageName, R.layout.appwidget_provider_layout ).apply { setOnClickPendingIntent(R.id.button, pendingIntent) } // Tell the AppWidgetManager to perform an update on the current app widget appWidgetManager.updateAppWidget(appWidgetId, views) } } } 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 appWidgetManager.requestPinAppWidget(myProvider, null, pendingIntent) } Java AppWidgetManager appWidgetManager = context.getSystemService(AppWidgetManager.class); ComponentName myProvider = new ComponentName(context, MyAppWidgetProvider.class); if (appWidgetManager.isRequestPinAppWidgetSupported()) { // Create the PendingIntent object only if your app needs to be notified // that the user allowed the widget to be pinned. Note that, if the pinning // operation fails, your app isn't notified. Intent pinnedWidgetCallbackIntent = new Intent( ... ); // Configure the intent so that your app's broadcast receiver gets // the callback successfully. This callback receives the ID of the // newly-pinned widget (EXTRA_APPWIDGET_ID). PendingIntent successCallback = PendingIntent.getBroadcast(context, 0, pinnedWidgetCallbackIntent, PendingIntent.FLAG_UPDATE_CURRENT); appWidgetManager.requestPinAppWidget(myProvider, null, successCallback); }

注意:如果无需通知您的应用系统是否已成功地将微件固定到受支持的启动器上,则您可以将 null 作为 requestPinAppWidget() 的第三个参数传入。

创建应用微件配置 Activity

如果您希望用户在添加新的应用微件时配置设置,您可以创建应用微件配置 Activity。此 Activity 将由应用微件托管应用自动启动,并允许用户在应用微件创建时为其配置可用设置,如应用微件的颜色、大小、更新周期或其他功能设置。

应在 Android 清单文件中将配置 Activity 声明为正常 Activity。不过,该 Activity 将由应用微件托管应用通过 ACTION_APPWIDGET_CONFIGURE 操作来启动,因此它需要接受此 Intent。例如:

此外,还必须在 AppWidgetProviderInfo XML 文件中使用 android:configure 属性声明该 Activity(请参阅上文的添加 AppWidgetProviderInfo 元数据)。例如,可按如下方式声明配置 Activity:

请注意,该 Activity 是使用完全限定的命名空间声明的,因为将从您的软件包范围之外对其进行引用。

这就是您开始使用配置 Activity 所需的全部内容。现在您只需要实际 Activity 了。不过,当您实现该 Activity 时,需要记住下面两个要点:

应用微件托管应用调用配置 Activity,并且配置 Activity 应始终返回结果。结果应包含由启动该 Activity 的 Intent 传递的应用微件 ID(在 Intent extra 中保存为 EXTRA_APPWIDGET_ID)。 创建应用微件时,系统不会调用 onUpdate() 方法(启动配置 Activity 时,系统不会发送 ACTION_APPWIDGET_UPDATE 广播)。首次创建应用微件时,由配置 Activity 负责从 AppWidgetManager 请求更新。不过,系统会调用 onUpdate() 来执行后续更新,只在首次更新时不会调用它。

有关如何从配置返回结果并更新应用微件的示例,请参阅下一部分中的代码段。

通过配置 Activity 更新应用微件

当应用微件使用配置 Activity 时,由该 Activity 负责在配置完成后更新应用微件。为此,您可以直接从 AppWidgetManager 请求更新。

下面简要说明了正确更新应用微件并关闭配置 Activity 的过程:

首先,从启动该 Activity 的 Intent 获取应用微件 ID: Kotlin appWidgetId = intent?.extras?.getInt( AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID ) ?: AppWidgetManager.INVALID_APPWIDGET_ID Java Intent intent = getIntent(); Bundle extras = intent.getExtras(); if (extras != null) { appWidgetId = extras.getInt( AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); } 执行应用微件配置。 配置完成后,通过调用 getInstance(Context) 来获取 AppWidgetManager 的实例: Kotlin val appWidgetManager: AppWidgetManager = AppWidgetManager.getInstance(context) Java AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); 通过调用 updateAppWidget(int, RemoteViews) 来使用 RemoteViews 布局更新应用微件: Kotlin RemoteViews(context.packageName, R.layout.example_appwidget).also { views-> appWidgetManager.updateAppWidget(appWidgetId, views) } Java RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.example_appwidget); appWidgetManager.updateAppWidget(appWidgetId, views); 最后,创建返回 Intent,为其设置 Activity 结果,然后结束该 Activity: Kotlin val resultValue = Intent().apply { putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) } setResult(Activity.RESULT_OK, resultValue) finish() Java Intent resultValue = new Intent(); resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); setResult(RESULT_OK, resultValue); finish();

提示:当配置 Activity 首次打开时,请将 Activity 结果设为 RESULT_CANCELED 并注明 EXTRA_APPWIDGET_ID,如上面的第 5 步所示。这样,如果用户在到达末尾之前退出该 Activity,应用微件托管应用就会收到配置已取消的通知,因此不会添加应用微件。

有关示例,请参阅 ApiDemos 中的 ExampleAppWidgetConfigure.java 示例类。

设置预览图片

Android 3.0 引入了 previewImage 字段,用于指定预览来描绘应用微件是什么样子的。此预览通过微件选择器显示给用户。如果未提供此字段,则应用微件的图标将用于预览。

在 XML 中指定此设置的方式如下:

为了帮助创建应用微件的预览图片(在 previewImage 字段中指定),Android 模拟器包含一个名为“微件预览”的应用。要创建预览图片,请启动此应用,为您的应用选择应用微件并设置您希望如何显示预览图片,然后将其保存并放在您的应用的可绘制资源中。

使用包含集合的应用微件

Android 3.0 引入了包含集合的应用微件。这些类型的应用微件使用 RemoteViewsService 来显示由远程数据(如来自内容提供程序的数据)支持的集合。由 RemoteViewsService 提供的数据将使用以下某种视图类型(我们称之为“集合视图”)呈现在应用微件中。

ListView 一种在垂直滚动列表中显示项目的视图。有关示例,请查看 Gmail 应用微件。 GridView 一种在二维滚动网格中显示项目的视图。有关示例,请查看“书签”应用微件。 StackView 一种堆叠式卡片视图(有点像名片盒),用户可以分别向上/向下翻动前面的卡片来查看上一张/下一张卡片。示例包括 YouTube 和“图书”应用微件。 AdapterViewFlipper 一种由适配器支持的简单 ViewAnimator,可以在两个或更多视图之间呈现动画效果。一次只显示一个子级。

如上所述,这些集合视图显示由远程数据支持的集合。这意味着,它们使用 Adapter 将其界面绑定到其数据。Adapter 将一组数据中的各个项目绑定到各个 View 对象。由于这些集合视图由适配器支持,因此 Android 框架必须包含额外的架构来支持它们在应用微件中的使用。在应用微件的上下文中,Adapter 被 RemoteViewsFactory 取代,后者只是 Adapter 接口的瘦封装容器。请求集合中的特定项目时,RemoteViewsFactory 会为集合创建相应项目并将其作为 RemoteViews 对象返回。要在应用微件中添加集合视图,您必须实现 RemoteViewsService 和 RemoteViewsFactory。

RemoteViewsService 是允许远程适配器请求 RemoteViews 对象的服务。RemoteViewsFactory 是集合视图(ListView、GridView 等等)与该视图的底层数据之间的适配器的接口。下面是您用来实现此服务和接口的样板代码的示例(来自 StackWidget 示例):

Kotlin class StackWidgetService : RemoteViewsService() { override fun onGetViewFactory(intent: Intent): RemoteViewsFactory { return StackRemoteViewsFactory(this.applicationContext, intent) } } class StackRemoteViewsFactory( private val context: Context, intent: Intent ) : RemoteViewsService.RemoteViewsFactory { //... include adapter-like methods here. See the StackView Widget sample. } Java 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. } 示例应用

本部分中的代码段摘录自 StackWidget 示例:

此示例由一个包含 10 个视图的堆栈组成,这些视图显示值 "0!" 到 "9!"。该示例应用微件具有以下主要行为:

用户可以垂直滑动该应用微件中的顶部视图以显示下一个或上一个视图。这是一种内置的 StackView 行为。 如果没有任何用户交互,则该应用微件会自动按顺序显示其视图,就像播放幻灯片一样。这是因为在 res/xml/stackwidgetinfo.xml 文件中设置了 android:autoAdvanceViewId="@id/stack_view"。此设置适用于视图 ID,在本例中为堆栈视图的视图 ID。 如果用户触摸顶部视图,则该应用微件会显示 Toast 消息“Touched view n”,其中“n”是触摸的视图的索引(位置)。如需详细了解如何实现此行为,请参阅向各个项目添加行为。 实现包含集合的应用微件

要实现包含集合的应用微件,您应执行的基本步骤与用来实现任何应用微件的步骤都一样。下面几部分介绍了实现包含集合的应用微件时需要执行的额外步骤。

包含集合的应用微件的清单

除了在清单中声明应用微件中列出的要求之外,要使包含集合的应用微件能够绑定到 RemoteViewsService,您还必须在清单文件中使用 BIND_REMOTEVIEWS 权限来声明该服务。这样可防止其他应用自由访问您的应用微件的数据。例如,在创建使用 RemoteViewsService 填充集合视图的应用微件时,清单条目可能如下所示:

代码行 android:name="MyWidgetService" 引用您的 RemoteViewsService 子类。

包含集合的应用微件的布局

对应用微件布局 XML 文件的主要要求是它必须包含某个集合视图:ListView、GridView、StackView 或 AdapterViewFlipper。下面是 StackWidget 示例的 widget_layout.xml:

请注意,空视图必须是集合视图的同级,其中空视图表示空状态。

除了整个应用微件的布局文件之外,您必须再创建一个布局文件,用来定义集合中每个项目的布局(例如,一套图书中每本图书的布局)。StackWidget 示例只有一个布局文件 widget_item.xml,因为所有项目使用同一布局。

包含集合的应用微件的 AppWidgetProvider 类

与常规应用微件一样,AppWidgetProvider 子类中的大部分代码通常都在 onUpdate() 中。在创建包含集合的应用微件时,您的 onUpdate() 实现的主要区别在于,您必须调用 setRemoteAdapter()。这样将告知集合视图要从何处获取其数据。然后,RemoteViewsService 可以返回您的 RemoteViewsFactory 实现,并且微件可以提供适当的数据。当您调用此方法时,必须传递指向您的 RemoteViewsService 实现的 Intent,以及指定要更新的应用微件的应用微件 ID。

例如,以下代码段说明了 StackWidget 示例如何实现 onUpdate() 回调方法以将 RemoteViewsService 设为应用微件集合的远程适配器:

Kotlin override fun onUpdate( context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray ) { // update each of the app widgets with the remote adapter appWidgetIds.forEach { appWidgetId -> // Set up the intent that starts the StackViewService, which will // provide the views for this collection. val intent = Intent(context, StackWidgetService::class.java).apply { // Add the app widget ID to the intent extras. putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME)) } // Instantiate the RemoteViews object for the app widget layout. val rv = RemoteViews(context.packageName, R.layout.widget_layout).apply { // 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. setRemoteAdapter(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. setEmptyView(R.id.stack_view, R.id.empty_view) } // // Do additional processing specific to this app widget... // appWidgetManager.updateAppWidget(appWidgetId, rv) } super.onUpdate(context, appWidgetManager, appWidgetIds) } 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(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 子类提供用于填充远程集合视图的 RemoteViewsFactory。

具体而言,您需要执行以下步骤:

RemoteViewsService 子类。RemoteViewsService 是一项服务,远程适配器可以通过它来请求 RemoteViews。 在您的 RemoteViewsService 子类中,添加一个实现 RemoteViewsFactory 接口的类。RemoteViewsFactory 是远程集合视图(如 ListView、GridView 等等)与该视图的底层数据之间的适配器的接口。您的实现负责为数据集中的每个项目创建一个 RemoteViews 对象。此接口是 Adapter 的瘦封装容器。

您不能依赖于服务的单个实例,也不能保留它包含的任何数据。因此,您不应将任何数据存储在 RemoteViewsService 中(除非它是静态的)。如果您要保留应用微件的数据,最好的方法是使用 ContentProvider,它的数据在进程生命周期过后持续存在。

RemoteViewsService 实现的主要内容是它的 RemoteViewsFactory,如下所述。

RemoteViewsFactory 接口

实现 RemoteViewsFactory 接口的自定义类可以为应用微件包含的集合中的项目提供数据。为此,它会将应用微件项目 XML 布局文件与数据源相结合。此数据源可以是任何来源,从数据库到简单的数组均可。在 StackWidget 示例中,数据源是 WidgetItems 的数组。RemoteViewsFactory 充当将数据粘附到远程集合视图的适配器。

您需要为 RemoteViewsFactory 子类实现的两个最重要的方法是 onCreate() 和 getViewAt()。

首次创建 RemoteViewsFactory 接口时,系统会调用 onCreate()。您可以在此方法中设置指向数据源的任何连接和/或游标。例如,StackWidget 示例使用 onCreate() 来初始化 WidgetItem 对象的数组。当应用微件处于活动状态时,系统会使用这些对象在数组中的索引位置来对其进行访问,并且会显示它们包含的文本。

以下代码段摘录自 StackWidget 示例的 RemoteViewsFactory 实现,显示了 onCreate() 方法的某些部分:

Kotlin private const val REMOTE_VIEW_COUNT: Int = 10 class StackRemoteViewsFactory( private val context: Context ) : RemoteViewsService.RemoteViewsFactory { private lateinit var widgetItems: List override fun 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. widgetItems = List(REMOTE_VIEW_COUNT) { index -> WidgetItem("$index!") } ... } ... } Java class StackRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory { private static final int count = 10; private List widgetItems = new ArrayList(); private Context context; private int appWidgetId; public StackRemoteViewsFactory(Context context, Intent intent) { this.context = context; appWidgetId = 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 < count; i++) { widgetItems.add(new WidgetItem(i + "!")); } ... } ...

RemoteViewsFactory 方法 getViewAt() 将返回与位于数据集中指定 position 的数据对应的 RemoteViews 对象。以下代码段摘录自 StackWidget 示例的 RemoteViewsFactory 实现:

Kotlin override fun getViewAt(position: Int): RemoteViews { // Construct a remote views item based on the app widget item XML file, // and set the text based on the position. return RemoteViews(context.packageName, R.layout.widget_item).apply { setTextViewText(R.id.widget_item, widgetItems[position].text) } } 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(context.getPackageName(), R.layout.widget_item); rv.setTextViewText(R.id.widget_item, widgetItems.get(position).text); ... // Return the remote views object. return rv; } 向各个项目添加行为

上面几部分介绍了如何将数据绑定到应用微件集合。但是,如果您要向集合视图中的各个项目添加动态行为,该怎么办呢?

如使用 AppWidgetProvider 类中所述,您通常使用 setOnClickPendingIntent() 来设置对象的点击行为 - 例如,让按钮启动 Activity。但是,不允许对各个集合项目中的子视图使用此方法(为了阐明这一点,我们举个例子,您可以使用 setOnClickPendingIntent() 在 Gmail 应用微件中设置一个用来启动应用的全局按钮,但不能在各个列表项上进行设置)。要向集合中的各个项目添加点击行为,应改用 setOnClickFillInIntent()。这需要为集合视图设置待定 Intent 模板,然后通过 RemoteViewsFactory 在集合中的每个项目上设置填充 Intent。

本部分通过 StackWidget 示例来说明如何向各个项目添加行为。在 StackWidget 示例中,如果用户触摸顶部视图,该应用微件会显示 Toast 消息“Touched view n”,其中“n”是触摸的视图的索引(位置)。其工作原理如下:

StackWidgetProvider(AppWidgetProvider 子类)会创建一个待定 Intent,该 Intent 具有一项名为 TOAST_ACTION 的自定义操作。 当用户触摸视图时,会触发该 Intent,并且它会广播 TOAST_ACTION。 此广播会被 StackWidgetProvider 的 onReceive() 方法拦截,并且应用微件会针对触摸的视图显示 Toast 消息。集合项目的数据由 RemoteViewsFactory 通过 RemoteViewsService 提供。

注意:StackWidget 示例使用了广播,但应用微件在这样的情况下通常会直接启动 Activity。

设置待定 Intent 模板

StackWidgetProvider(AppWidgetProvider 子类)会设置一个待定 Intent。集合中的各个项目无法设置它们自己的待定 Intent,而是整个集合设置一个待定 Intent 模板,并且各个项目设置填充 Intent 来逐项创建唯一的行为。

此类还会接收用户触摸视图时发送的广播。它在自己的 onReceive() 方法中处理此事件。如果 Intent 的操作为 TOAST_ACTION,则应用微件会针对当前视图显示 Toast 消息。

Kotlin const val TOAST_ACTION = "com.example.android.stackwidget.TOAST_ACTION" const val EXTRA_ITEM = "com.example.android.stackwidget.EXTRA_ITEM" class StackWidgetProvider : AppWidgetProvider() { ... // 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 fun onReceive(context: Context, intent: Intent) { val mgr: AppWidgetManager = AppWidgetManager.getInstance(context) if (intent.action == TOAST_ACTION) { val appWidgetId: Int = intent.getIntExtra( AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID ) val viewIndex: Int = intent.getIntExtra(EXTRA_ITEM, 0) Toast.makeText(context, "Touched view $viewIndex", Toast.LENGTH_SHORT).show() } super.onReceive(context, intent) } override fun onUpdate( context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray ) { // update each of the app widgets with the remote adapter appWidgetIds.forEach { appWidgetId -> // Sets up the intent that points to the StackViewService that will // provide the views for this collection. val intent = Intent(context, StackWidgetService::class.java).apply { putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) // 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. data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME)) } val rv = RemoteViews(context.packageName, R.layout.widget_layout).apply { setRemoteAdapter(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. 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. val toastPendingIntent: PendingIntent = Intent( context, StackWidgetProvider::class.java ).run { // Set the action for the intent. // When the user touches a particular view, it will have the effect of // broadcasting TOAST_ACTION. action = TOAST_ACTION putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME)) PendingIntent.getBroadcast(context, 0, this, PendingIntent.FLAG_UPDATE_CURRENT) } rv.setPendingIntentTemplate(R.id.stack_view, toastPendingIntent) appWidgetManager.updateAppWidget(appWidgetId, rv) } super.onUpdate(context, appWidgetManager, appWidgetIds) } } Java 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); } } 设置填充 Intent

您的 RemoteViewsFactory 必须在集合中的每个项目上设置一个填充 Intent。这样就可以区分给定项目的点击时的各项操作。填充 Intent 随后与 PendingIntent 模板相结合,以确定在点击相应项目时要执行的最终 Intent。

Kotlin private const val REMOTE_VIEW_COUNT: Int = 10 class StackRemoteViewsFactory( private val context: Context, intent: Intent ) : RemoteViewsService.RemoteViewsFactory { private lateinit var widgetItems: List private val appWidgetId: Int = intent.getIntExtra( AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID ) override fun 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. widgetItems = List(REMOTE_VIEW_COUNT) { index -> WidgetItem("$index!") } ... } ... override fun getViewAt(position: Int): RemoteViews { // Construct a remote views item based on the app widget item XML file, // and set the text based on the position. return RemoteViews(context.packageName, R.layout.widget_item).apply { setTextViewText(R.id.widget_item, widgetItems[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. val fillInIntent = Intent().apply { Bundle().also { extras -> extras.putInt(EXTRA_ITEM, position) putExtras(extras) } } // Make it possible to distinguish the individual on-click // action of a given item setOnClickFillInIntent(R.id.widget_item, fillInIntent) ... } } ... } Java 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 count = 10; private List widgetItems = new ArrayList(); private Context context; private int appWidgetId; public StackRemoteViewsFactory(Context context, Intent intent) { this.context = context; appWidgetId = 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 < count; i++) { widgetItems.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(context.getPackageName(), R.layout.widget_item); rv.setTextViewText(R.id.widget_item, widgetItems.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; } ... } 使集合数据保持最新

下图说明了在发生更新时使用集合的应用微件中发生的流程。图中显示了应用微件代码如何与 RemoteViewsFactory 交互,以及您如何触发更新:

使用集合的应用微件的一项功能是能够为用户提供最新内容。以 Android 3.0 Gmail 应用微件为例,它可以为用户提供收件箱的快照。要做到这一点,您需要能够触发 RemoteViewsFactory 和集合视图以获取和显示新数据。您可以通过 AppWidgetManager 调用 notifyAppWidgetViewDataChanged() 来实现这一目标。此调用会导致对 RemoteViewsFactory 的 onDataSetChanged() 方法进行回调,从而让您有机会获取任何新数据。请注意,您可以在 onDataSetChanged() 回调中同步执行处理密集型操作。可以保证您会在从 RemoteViewsFactory 获取元数据或视图数据之前完成此调用。此外,您还可以在 getViewAt() 方法中执行处理密集型操作。如果此调用需要很长时间,则正在加载的视图(由 RemoteViewsFactory 的 getLoadingView() 方法指定)会显示在集合视图的相应位置,直到它返回结果。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3