乐趣区

关于android:在-Android-12-中构建更现代的应用-Widget

从 2008 年开始,Widget 就始终是 Android 零碎的一个重要组成部分,也是自定义主屏幕的一个重要方面。您能够将 Widget 了解为一个 “ 高深莫测 ” 的利用视图,让用户在无需从主屏幕关上利用的前提下,就能对利用数据和外围性能和盘托出。然而从 Android 推出至今,AppWidget 的 API 根本就没有什么大的变动,从 2012 年到 2021 年更是只有一个 Android 版本蕴含了对 AppWidget API 的更新。而随着 Android 12 的推出,也带来了 Widget API 一些亟需改良的更新。

本文咱们就来介绍一下 Android 12 中带来了哪些对于 Widget API 的更新,以及有哪些好用的工具能够让开发利用 Widget 变得更加杰出。如果您更喜爱通过视频理解此内容,请在此处查看:

https://www.bilibili.com/vide…

△ 在 Android 12 中构建更古代的利用 Widget

Widget 工作原理

Widget 运行在一个名为 AppWidgetHost 的远端过程中,比方 Home Screen Launcher,也正因如此,它的运行受到了一些限度。咱们来看看 Widget 的工作原理。

在前端,利用首先注册 AppWidgetProvider 来定义 Widget 行为,以及注册 AppWidgetProviderInfo 来定义元数据。而后 AndroidManifest 援用这些信息,让操作系统通过 AndroidManifest 读取元数据,例如 Widget 初始的布局和默认尺寸,并提供 Widget 的预览,紧接着,provider 会应用链接账户来更新布局并对 Widget 进行更新。这里须要留神的是,利用于 Widget 的构建次数无限,所以操作系统是通过接管方的播送事件 (蕴含了更新信息) 对 Widget 进行更新,这也意味着 Widget 是定期接管来自利用的信息进行更新的。

API

Android 12 的推出带来了很多对于 AppWidget API 的更新,本文不会对所有的 API 一一介绍,而是重点介绍几个对 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>

而后将可绘制对象利用于 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 圆角;图右: 内视图圆角

从成果中咱们能够看到 Widget 以后外部容器的圆角半径要小于内部容器,这就是新参数的应用办法。

动静色彩

正如咱们之前在 Google I/O 大会上发表的那样,从 Android 12 开始,Widget 能够为按钮、背景及其他组件应用设施主题色彩,包含浅色主题和深色主题。这样可使过渡更晦涩,而且还能在不同的 Widget 之间保持一致。

咱们增加了动静色彩 API,您可间接获取并应用 Pixel 设施零碎上提供的主题背景、色彩等参数,从而让 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>

您能够看到,当设置了主题属性之后,Widget 间接从零碎壁纸中提取了主色,并将其利用于深色和浅色主题背景中。

响应式布局

Android 12 引入了新的 API 来实现响应式布局,能够随着 Widget 的尺寸调整,主动切换到不同的布局。如下图所示,用户能够通过拖动来任意更改 Widget 的尺寸,Widget 也会依据尺寸的不同而动静更新所要显示的内容。

那么如何做到让 Widget 随着尺寸的变动而动静更新显示内容呢,用如下代码举例,咱们定义了三个不同的参数,别离蕴含最小反对宽度和高度,以及在此大小范畴内对应的 RemoteView,零碎会主动依据理论的尺寸而主动对 Widget 进行调整。

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 中还提供了新的 targetCellWidthtargetCellHeight 属性,这些属性指定了 Widget 置于主屏幕中时默认的较大单元格尺寸。在 Android 12 之前,能够应用 minWidgetminHeight 属性,它们指定了以 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"
    …>

Widget 选择器

Android 12 还改良了 Widget 选择器的应用体验,引入了两个新的属性,第一个属性是 description,它对 Widget 选择器的作用进行了形容阐明,通过它能够理解 Widget 的作用;另一个是 previewLayout,它指定了 Widget 选择器中展现的 XML 布局。实际上在 Android 12 之前能够应用 previewImage 属性来指定动态资源达到相似成果,然而 previewLayout 相比拟来说更加准确和不便。另外,因为这些预览都是在运行时构建的,因而也能够动静适配设施的主题。

<appwidget-provider
    android:description=
        "@string/app_widget_weather_description"
    android:previewLayout=
        "@layout/widget_weather_forecast_small"
…
/>

△ description 属性

△ previewLayout 属性

目前曾经介绍了很多 Android 12 引入的新 API,置信不久之后就会看到越来越多的利用采纳新 API 构建出更古代的 Widget 应用体验。

Glance

要构建杰出的 Widget,除了须要用到目前更古代的 API 之外,咱们还须要更古代、更杰出的工具来帮忙咱们,Glance 就是这么一个杰出的工具,它也退出到了 Jetpack 小家庭中。Glance 是由 Compose Runtime 提供反对的 API,通过它就能够应用 Compose 格调的语法来创立 AppWidget,这也意味着您能够通过 Glance 以 composable 构建界面,并将其转换为远端视图显示到 Widget 中,同时还能用到前文中提到的 Android 12 的新 API,并尽可能的让其向后兼容。另外,Glance 还会负责一些 Widget 生命周期以及其余一些常见的操作,听下来是不是感觉十分不便。

△ Glance 构造示意图

接下来咱们介绍如何应用 Glance 构建 Widget,首先仍须要像之前一样申明 AppWidget,并在 AndroidManifest 中将其链接到接收器,当然,咱们在这里应用了 Glance 提供的 GlanceAppWidgetReceiver 和 GlanceAppWidget,Glance 会为您解决大部分的工作,您只须要覆写 MyAppWidget 中的 Content 办法,提供 AppWidget 内容即可。在定义内容时,不再应用 XML 语法,而是应用 Compose 语法,要显示的内容将会被转换为远端视图展现在 AppWidget 中。

class MyAppWidget: GlanceAppWidget() {
    @Composable
    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
 
    @Composable
    override fun Content() {
        val size = LocalSize.current
        //…
    }
}

△ SizeMode.Single 选项示意图

若在每次尺寸产生变更都对内容进行刷新,则可应用 SizeMode.Exact 选项。此选项会在用户每次调整 Widget 尺寸时,从新创立 Widget 界面并再次调用 Content 办法,并同时提供最大可用尺寸以便让咱们可能在空间足够的状况下更改界面,比方增加额定按钮等等。如下图中,Widget 尺寸发生变化时,其外部的输入也会随时发生变化,这是因为每次 Widget 界面都会被从新创立。

class MyAppWidget: GlanceAppWidget() {
    override val sizeMode = SizeMode.Exact
 
    @Composable
    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)
 
    @Composable
    override fun Content() {
        val size = LocalSize.current
        //…
    }
}

除了以上提到的内容外,还有例如对 Widget 状态治理的反对,和即开即用的 Material You 主题背景等更多内容,期待着您的摸索。

如需理解更多内容,欢迎您查阅 Android 开发者网站: 利用 Widget 概览,咱们十分期待您尝试咱们提供的新 API,并期待看到您构建出的 Widget 和您的反馈。

欢迎您 点击这里 向咱们提交反馈,或分享您喜爱的内容、发现的问题。您的反馈对咱们十分重要,感谢您的反对!

退出移动版