从 2008 年开始,Widget 就一直是 Android 系统的一个重要组成部分,也是自定义主屏幕的一个重要方面。您可以将 Widget 理解为一个 "一目了然" 的应用视图,让用户在无需从主屏幕打开应用的前提下,就能对应用数据和核心功能一览无余。但是从 Android 推出至今,AppWidget 的 API 基本就没有什么大的变化,从 2012 年到 2021 年更是只有一个 Android 版本包含了对 AppWidget API 的更新。而随着 Android 12 的推出,也带来了 Widget API 一些亟需改进的更新。
△ 在 Android 12 中构建更现代的应用 Widget
Bilibili 视频链接
https://www.bilibili.com/video/BV1Ra411z7dN/
Widget 工作原理
API
在 Android 12 中许多关键的界面元素都开始采用圆角设计,为了使 AppWidget 与其他系统组件样式之间看起来一致,Android 12 引入了 system_app_widget_background_radius 和 system_app_widget_inner_radius 两个新的系统参数实现圆角,前一个参数是用来设置 Widget 的圆角半径,后一个则是设置 Widget 内视图的圆角半径。要使用这些参数,只需要定义一个设置了系统参数 corner 的可绘制对象即可,如代码所示:
// res/drawable/app_widget_background.xml
<shape android:shape="rectangle">
<corners android:radius="@android:dimen/system_app_widget_background_radius">
…
</shape>
// res/drawable/app_widget_inner_view_background.xml
<shape android:shape="rectangle">
<corners android:radius="@android:dimen/system_app_widget_inner_radius">
…
</shape>
system_app_widget_background_radius
https://developer.android.google.cn/reference/android/R.dimen?hl=zh-cn#system_app_widget_background_radius
system_app_widget_inner_radius
https://developer.android.google.cn/reference/android/R.dimen?hl=zh-cn#system_app_widget_inner_radius
然后将可绘制对象应用于 Widget 的外部容器,这样做可将系统参数提供的圆角半径应用于 Widget 背景中。同样,将内部视图的可绘制对象应用于表示 Widget 内部容器的布局,如代码所示:
// res/layout/widget_layout.xml
<LinearLayout
android:background=”@drawable/app_widget_background”
…>
<LinearLayout
android:background=”@drawable/app_widget_inner_view_background”
…>
</LinearLayout>
</LinearLayout>
从效果中我们可以看到 Widget 当前内部容器的圆角半径要小于外部容器,这就是新参数的使用方法。
// res/layout/widget_layout.xml
<LinearLayout
android:theme="@android:style/Theme.DeviceDefault.DayNight"
android:background="?android:attr/colorBackground">
<ImageView
android:tint="?android:attr/colorAccent" />
…
</LinearLayout>
val viewMapping: Map<SizeF, RemoteViews> = mapof(
SizeF(180.0f, 110.0f) to RemoteViews(
context. packageName,
R.layout.widget_small
),
SizeF (270.0f, 110.0f) to RemoteViews(
context.packageName,
R.layout.widget_medium
),
SizeF(270.0f, 280.0f) to RemoteViews(
context.packageName,
R.layout.widget_large
)
)
appWidgetManager.updateAppWidget(appWidgetId, RemoteViews(viewMapping))
Android 12 中还提供了新的 targetCellWidth 和 targetCellHeight 属性,这些属性指定了 Widget 置于主屏幕中时默认的较大单元格尺寸。在 Android 12 之前,可以使用 minWidget 和 minHeight 属性,它们指定了以 dp 为单位的默认 Widget 尺寸,我们建议同时指定这两个属性以保持向后兼容。如果您的 Widget 是可调整尺寸的,那么还可以使用 Android 12 提供的 minResizeWidget/Height 和 maxResizeWidget/Height 属性来限制 Widget 的可调整尺寸范围。
<appwidget-provider
android:targetCellWidth="3"
android: targetCellHeight="2"
android:minWidth="140dp"
android:minHeight="110dp"
android:maxResizeWidth="570dp"
android:maxResizeHeight="450dp"
android:minResizeWidth="140dp"
android:minResizeHeight="110dp"
…>
<appwidget-provider
android:description=
"@string/app_widget_weather_description"
android:previewLayout=
"@layout/widget_weather_forecast_small"
…
/>
Glance
class MyAppWidget: GlanceAppWidget() {
override fun Content() {
// 在这里创建 AppWidget
Column(
modifier = Modifier.expandHeight().expandWidth(),
verticalAlignment = Alignment.Top,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = “Where to”, modifier = Modifier.padding(12.dp))
userDestinations()
}
}
}
class MyAppWidgetReceiver: GlanceAppWidgetReceiver() {
// 告知 MyAppWidgetReceiver 该使用哪个 GlanceAppWidget
override val glanceAppWidget: GlanceAppWidget = MyAppWidget()
}
有一点需要了解,虽然 Glance 使用 Compose Runtime 和 Compose 的语法,但它仍是一个独立的框架,由于受到在远端进行构建的限制,您不可能重用在 Jetpack Compose UI 中定义的组件。但如果您已对 Jetpack Compose 非常熟悉,那么 Glance 将非常易于理解。
另外,由于 Glance 使用用户事件 API 的方式处理交互,我们处理同用户的交互将变得更加轻松。如果您了解 Widget 的工作原理就会知道 Widget 在不同进程上工作,这使得处理简单的用户事件也变得困难,因为不在同一进程就代表您没有这个 Widget 的所有权,只能通过进程回调来处理各种事件。
Glance 将这些复杂性抽象了出来,您只需通过向需要的 composable 对象定义 clickable modifier 即可让其支持处理用户点击事件,Glance 会将其中的注入行为全部抽象出来,用户点击了 composanle,即可回调所定义的操作。我们还定义了一些常用的操作,例如,如何启动 Activity,只要调用 launchActivity 传递 Activity 目标类即可。
Button(
text = “Home”,
modifier = Modifier.clickable(launchActivity<NavigationActivity>)
)
此外,我们还可以提供自定义操作来执行一些自定义代码,例如,我们可能希望每当用户点击此按钮时就会更新地理位置并刷新 Widget,如下列代码所示,Glance 会在背后为您处理一些需要注入的工作,并通过广播接收器处理此次点击,最终调用您定义的操作代码。但请注意,如果该种操作为网络请求或数据库访问等较为耗时的操作,请使用 WorkManager API。
Button(
text = “My Location”,
modifier = Modifier.clickable(customAction<UpdateLocationAction>)
)
在前文中我们也提到,您可以使用可调整尺寸的 Widget,但是处理不同的响应式布局也并非易事,Glance 就试图通过定义三种不同的 SizeMode 选项从而让这种工作变得稍微轻松一些。
SizeMode.Single 是默认选项,该选项指定了我们在此处定义的 Widget 内容不会因为可用尺寸变化而改变,这意味着我们在 Widget 元数据上定义的最小支持尺寸只会通过 Content 方法被调用一次,如果 Widget 的可用尺寸发生更改,例如用户调整了 Widget 尺寸,则不会刷新内容。如下图所示,使用了 SizeMode.Single 选项的 Widget,无论其尺寸如何变化,其输出的尺寸大小永远不会得到变化,这是因为 Content 方法只被调用了一次,内容在尺寸发生变化时并没有得到刷新。
class MyAppWidget: GlanceAppWidget() {
override val sizeMode = SizeMode.Single
override fun Content() {
val size = LocalSize.current
//…
}
}
若在每次尺寸发生变更都对内容进行刷新,则可使用 SizeMode.Exact 选项。此选项会在用户每次调整 Widget 尺寸时,重新创建 Widget 界面并再次调用 Content 方法,并同时提供最大可用尺寸以便让我们能够在空间足够的情况下更改界面,比如添加额外按钮等等。如下图中,Widget 尺寸发生变化时,其内部的输出也会随时发生变化,这是因为每次 Widget 界面都会被重新创建。
class MyAppWidget: GlanceAppWidget() {
override val sizeMode = SizeMode.Exact
override fun Content() {
val size = LocalSize.current
//…
}
}
△ SizeMode.Exact 选项示意图
尽管 SizeMode.Exact 选项看似能够完全满足需求,但是每次都需要重新创建界面,可能会导致用户在调整尺寸时界面的转换因为一些性能问题有点不流畅,此时我们就可以通过 SizeMode.Responsive 选项。例如,此处我们将一些尺寸映射到某些特定形状,每当创建或更新 AppWidget 时 Glance 都会调用每个 Size 定义好的的 Content 方法,每次都将映射到特定尺寸并存储在内存中,系统能够在用户调整 Widget 尺寸时,根据可用尺寸选择最合适的尺寸,而无需重新创建界面从而提供更平稳的转换和更出色的性能。正如下图所展示的那样,当 Widget 尺寸发生变更时,只有当其尺寸能够匹配到所预先定义好的尺寸范围中,其内部输出才会发生变化,更应该注意的是,此时并没有重新创建界面。
△ SizeMode.Responsive 选项示意图
同样,我们还可以在 Content() 方法中定义更加多元化的样式,让 Widget 在不同的尺寸下展示更独特的内容。
class MyAppWidget: GlanceAppWidget() {
companion object {
private val SMALL_SQUARE = DpSize (100.dp, 160. dp)
private val HORIZONTAL_RECTANGLE = DpSize (250.dp, 100.dp)
private val BIG_SQUARE = DpSize (250.dp, 250.dp)
}
override val sizeMode = SizeMode.Responsive(
SMALL_SQUARE, HORIZONTAL_RECTANGLE, BIG_SQUARE
)
override fun Content() {
val size = LocalSize.current
//…
}
}
应用 Widget 概览
https://developer.android.google.cn/guide/topics/appwidgets/overview
推荐阅读
点击屏末 | 阅读原文 | 即刻了解 Widget 更多内容