赞
踩
本文原作者: 小虾米君,原文发布于: TechMerger
https://mp.weixin.qq.com/s/w_289RIXnb7wkT3c0b8nfQ
Google IO 2021 重磅介绍的 Android 12,号称历代设计变化最大的版本。其全新的 Material You 设计语言、流畅的动画特效再到焕然一新的小组件,都令人印象深刻。本文将聚焦小组件环节,谈谈它在重新设计之后的各种新特性和适配方法。
小组件在 Android 平台上命名为 AppWidget,有的时候还被翻译成小部件、小插件和微件。说的都是一个东西: 显示在 Launcher 上,能在 Logo 以外提供更多信息的特别设计。它方便用户免于打开 App 即可直接查看信息和进行简单的交互,在 PC 上、早前的 Symbian 上都有类似的设计。
前言
简要回顾下移动平台在小组件设计上的持续探索:
早期的 Android 版本缺乏美观,小组件更是常年未改。似乎除了天气、时钟等常用小组件以外鲜少使用,逐渐被人遗忘;
Windows Phone 的动态磁贴在自由尺寸的 Logo 上灵活展示信息的设计非常超前,奈何生态构建困难,早已退场;
Apple 向来稳重 (保守),直到 iOS 10 才引入小组件,但负一屏限制着它的发展。直到 iOS 14 的全面支持才大获成功,大有后来居上的态势;
VIVO 紧随其后重磅推出的 OriginOS 则将 Logo 和小组件完美融合,试图一统磁贴和小组件的概念,非常值得称赞;
也许是受到了友商们的持续刺激,Google 终于开始重新审视小组件这个元老级功能,并在 Android 12 里进行了重新设计、重新出发。
下面将结合代码实战,带领大家逐步感受 Android 12 里小组件的各项新特性和对应的适配方法。
1. 选择和展示的统一变化
事实上即使未做任何适配,在 12 上直接运行的小组件与 11 就有明显不同,主要表现在选择器和展示的效果。
以 Chrome 和 Youtube Music 的小组件为例:
可以看到 12 上的一些变化:
选择器
顶部悬浮搜索框,可以更加快速地找到目标小组件
小组件按照 App 自动折叠,避免无关的小组件占用屏幕空间
App 标题还对包含的小组件数目进行了提示
拖拽到桌面上之后小组件默认拥有圆角设计
11 上的小组件选择器不支持搜索而且无法折叠,拖拽到桌面上也是初始的直角效果。
2. 美观的圆角设计
健康信息越发重要,手撸一个展示今日步数的小组件,搭配 androidplot 开源框架展示详细的步数图表。
- override fun onUpdate(...) {
- for (appWidgetId in appWidgetIds) {
- showBarChartToWidget(context, appWidgetManager, appWidgetId)
- }
- }
-
-
- private fun showBarChartToWidget(...) {
- // Create plot view.
- val plot = XYPlot(context, "Pedometers chart")
- ...
- // Set graph shape
- plot.setBorderStyle(Plot.BorderStyle.ROUNDED, 12f, 12f)
- plot.isDrawingCacheEnabled = true
-
-
- // Reflect chart's bitmap to widget.
- val bmp = plot.drawingCache
- val remoteViews = RemoteViews(context.packageName, R.layout.widget_pedometer)
- remoteViews.setBitmap(R.id.bar_chart, "setImageBitmap", bmp)
- appWidgetManager.updateAppWidget(appWidgetId, remoteViews)
- }
不用特别适配,直接运行到 12 上,就能有圆角效果。
但布局需要遵从如下两点建议:
四周的边角不要放置内容,防止被切掉
背景不要采用透明的、空的视图或布局,避免系统无法探测边界去进行裁切
事实上,系统预设了如下 dimension 以设置默认的圆角表现。
system_app_widget_background_radius: 小组件背景的圆角尺寸,默认 16dp,上限 28dp
system_app_widget_inner_radius: 小组件内部视图的圆角尺寸,默认 8dp,上限 20dp
system_app_widget_internal_padding: 内部视图的 padding 值,默认 16dp
看下官方的对于内外圆角尺寸的示意图。
注意:
这些 dimension 可以被 ROM 厂商或 3rd Launcher 修改,不一定能保证一致性的尺寸
官方没有说明小组件的内部视图如何才能应用上内部圆角尺寸,DEMO 确实也没有适配上,不知道是 ROM 的问题还是 App 的问题,有待后续的进一步研究
当然 12 以前的系统想要支持圆角设计也很简单: 自定义 radius 的 attribute,应用在shape drawable 上,手动将 drawable 应用到 background。具体可参考官方 Sample:
https://github.com/android/user-interface-samples/tree/main/AppWidget
3. 动态的色彩效果
给小组件添加暗黑主题支持即可自动适配动态色彩。
- <!-- values/themes.xml -->
- <resources xmlns:tools="http://schemas.android.com/tools">
- <style name="Theme.AppWidget" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
- <item name="colorPrimary">@color/purple_500</item>
- <item name="colorPrimaryVariant">@color/purple_700</item>
- <item name="colorOnPrimary">@color/white</item>
- ...
- </style>
- </resources>
-
-
- <!-- values-night/themes.xml -->
- <resources xmlns:tools="http://schemas.android.com/tools">
- <style name="Theme.AppWidget" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
- <item name="colorPrimary">@color/purple_200</item>
- <item name="colorPrimaryVariant">@color/purple_700</item>
- <item name="colorOnPrimary">@color/black</item>
- ...
- </style>
- </resources>
4. 改进的小组件预览
12 针对小组件选择时的预览界面进行了改进,方便展示更加精准的预览效果。
4.1 动态预览
之前只能使用 previewImage 属性展示一张预览图,功能迭代的过程中忘记更新它的话,可能导致预览和实际效果发生偏差。
12 新引入了 previewLayout 属性用以配置小组件的实际布局,使得用户能够在小组件的选择器里看到更加接近实际效果的视图,而不再是一层不变的静态图片。
这样一来在保证效果一致的同时免去了额外维护预览图的麻烦。
- <appwidget-provider
- <!-- 既存的图片属性指定UI提供的设计图 -->
- android:previewImage="@drawable/app_widget_pedometer_preview_2"
-
-
- <!-- 新的预览API里指定实际的布局 -->
- android:previewLayout="@layout/widget_pedometer"
- </appwidget-provider>
左边是步数小组件一开始的设计图,右边是最后的实际效果。
如果忘记说服 UI 重新作图的话,在 11 上的预览图会和实际效果有较大偏差。而 12 上不用在乎设计图是否更新,借助新的 API 即可直接预览实际效果,所见即所得。
一般来说 previewLayout 属性最好指定小组件的实际布局。但如果预览的测试数据和实际的默认值有冲突的话,可以指定专用的预览布局,只需要确保布局的一致。
4.2 添加预览说明
description 属性则可以在小组件预览的下方展示额外的说明,便于用户更好地了解其功能定位。
- <appwidget-provider
- android:description="@string/app_widget_pedometer_description">
- </appwidget-provider>
需要提醒的是 description 属性并非 12 新增,但 12 之前的选择器不支持展示这个说明。
5. 支持新的交互控件
之前的小组件不支持 CheckBox 等控件,从 12 开始全面支持 CheckBox、Switch 和RadioButton 三种状态控件。
下面是采用这三种控件的简单效果。
再做个简单的待办事项以更好地说明状态小组件的使用。
- // 小组件件布局里指定CheckBox控件即可
- <LinearLayout ...
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical"
- android:theme="@style/Theme.AppWidget.AppWidgetContainer">
-
-
- <include layout="@layout/widget_todo_list_title_region" />
-
-
- <CheckBox
- android:id="@+id/checkbox_first"
- style="@style/Widget.AppWidget.Checkbox"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="@string/todo_list_sample_1"
- Tools:text="@string/todo_list_tool" />
- ...
- </LinearLayout>
如果将同样的代码运行到 11 上,则会显示加载失败。
日志
Binary XML file line #13 in com.example.splash:layout/widget_todo_list: Error inflating class android.widget.CheckBox
文本内容不确定的话,可以通过代码动态地控制文本,同时还可以监听用户的选择事件。
比如我们要展示 Android 开发者如今要学习的三座大山,选中的时候弹出 Toast。
- private fun updateAppWidget(...) {
- val viewId1 = R.id.checkbox_first
- val pendingIntent = PendingIntent.getBroadcast(...)
-
-
- val rv = RemoteViews(context.packageName, R.layout.widget_todo_list)
- rv.apply {
- // 设置文本
- setTextViewText(viewId1, context.resources.getString(R.string.todo_list_android))
- ...
-
-
- // 设置CheckBox的默认选中状态
- setCompoundButtonChecked(viewId1, true)
-
-
- // 监听相应的CheckBox的选中事件
- setOnCheckedChangeResponse(
- viewId1,
- RemoteViews.RemoteResponse.fromPendingIntent(pendingIntent)
- )
- }
- appWidgetManager.updateAppWidget(appWidgetId, remoteViews)
- }
-
-
- override fun onReceive(context: Context?, intent: Intent?) {
- ...
- val checked = intent.extras?.getBoolean(RemoteViews.EXTRA_CHECKED, false) ?: false
- val viewId = intent.extras?.getInt(EXTRA_VIEW_ID) ?: -1
-
-
- Toast.makeText(
- context,
- "ViewId : $viewId's checked status is now : $checked",
- Toast.LENGTH_SHORT
- ).show()
- }
6. 便捷地配置尺寸
12 针对小组件的尺寸配置环节也进行了改进,更加便捷。
6.1 精确的尺寸
在已有的 minWidth、minResizeWidth 等属性以外,新增了几个属性以更便捷地配置小组件的尺寸。
targetCellWidth 和 targetCellHeight: 占据 Launcher 上 Cell 的宽高格数,用以替代 minWidth 和 minHeight。事实上 Launcher 是以 Cell 的单位来展示小组件的,所以直接指定 Cell 数显然更合理
maxResizeWidth 和 maxResizeHeight: 配置 Launcher 上允许配置的最大尺寸,弥补 minResizeWidth 和 minResizeHeight 的不足
- <appwidget-provider
- ...
- android:targetCellWidth="3"
- android:targetCellHeight="2"
- android:maxResizeWidth="250dp"
- android:maxResizeHeight="110dp">
- </appwidget-provider>
6.2 灵活调节尺寸
iOS 上添加小组件后尺寸就固定了,不支持调节。而 Android 12 上小组件在长按后即可灵活调节。
想要支持这个特性只需要给 widgetFeatures 性指定 reconfigurable 值即可。
- <appwidget-provider
- android:widgetFeatures="reconfigurable">
- </appwidget-provider>
The reconfigurable flag was introduced in Android 9 (API level 28), but it was not widely supported in launchers until Android 12.
事实上这个属性早在 Android 9 的时候就引入了,但官方说从 S 开始才全面支持。我在 11 版本的 Pixel Launcher 上发现已经可以直接调节尺寸了,不知道官方的意思是不是别的 Launcher 并不支持。
6.3 采用默认配置
configure 属性可以在小组件展示之前启动一个配置画面,供用户选择小组件所需的内容、主题和风格等。
如果想让用户快速看到效果,即不想展示这个画面的话,只要在 widgetFeatures 里指定新的 configuration_optional 值即可。
- <appwidget-provider
- ...
- android:configure="com.example.appwidget.activity.WidgetConfigureActivity"
- android:widgetFeatures="reconfigurable|configuration_optional">
- </appwidget-provider>
后面改主意了又想替换配置的话,可以长按小组件找到配置的入口。
一是小组件右下方的编辑按钮,二是上方出现的 Setup 菜单,这在以前的版本上是没有的。
7. 高效地控制布局
小组件内容较多的时候,为了展示的完整往往会给它限定 Size,这意味着只有 Launcher 空间足够大小组件才能成功放置。当 Launcher 空间捉急的时候就尴尬了,用户只能在移除别的小组件和放弃您的小组件之间做个抉择。
免除这种困扰的最佳做法是在不同的 Size 下采用不同的布局,对展示的内容做出取舍。即 Size 充足的情况下提供更多丰富的内容,反之只呈现最基本、最常用的信息。
7.1 响应式布局
之前是如何做到这一需求呢?除了预设各种尺寸的小组件的一般思路以外,通过 onAppWidgetOptionsChanged 回调也可以控制布局的变化,但往往非常繁琐。
而 12 上借助新增的 RemoteViews (Map<SizeF, RemoteViews> map) API 可以大大简化实现过程。在小组件放置的时候就将 Size 和布局的映射关系告知系统,当 Size 变化了 AppWidgetManager 将自动响应更新对应的布局。
比如待办事项小组件在 Size 为 3x2 的时候额外展示添加按钮,2x2 的时候只展示事项列表的相应式布局。
代码的实现也简单清晰:
- private fun updateAppWidgetWithResponsiveLayouts(...) {
- ...
- // 尺寸够宽的情况下Button才显示
- val wideView = RemoteViews(rv)
- wideView.setViewVisibility(button, View.VISIBLE)
-
-
- val viewMapping: Map<SizeF, RemoteViews> = mapOf(
- SizeF(100f, 100f) to rv,
- SizeF(200f, 100f) to wideView
- )
-
-
- // 将Size和RemoteViews布局的映射关系告知AppWidgetManager
- val remoteViews = RemoteViews(viewMapping)
- appWidgetManager.updateAppWidget(appWidgetId, remoteViews)
- }
好处:
免于同一功能提供一堆尺寸小组件的繁琐,减轻选择器的负担
实现简单,自动响应
7.2 精确布局
如今移动设备的尺寸、形态丰富多样,尤其是折叠屏愈加成熟。如果响应式布局仍不能满足更精细的需求,可以在 Size 变化的回调里,获取目标 Size 对布局进一步的精确把控。
利用 AppWidgetManager 新增的 OPTION_APPWIDGET_SIZES KEY 可以从AppWidgetManager 里拿到目标 Size。
- // 监听目标尺寸
- override fun onAppWidgetOptionsChanged(...) {
- ...
- // Get the new sizes.
- val sizes = newOptions?.getParcelableArrayList<SizeF>(
- AppWidgetManager.OPTION_APPWIDGET_SIZES
- )
-
-
- // Do nothing if sizes is not provided by the launcher.
- if (sizes.isNullOrEmpty()) {
- return
- }
- Log.d("Widget", "PedometerAppWidget#onAppWidgetOptionsChanged() size:${sizes}")
-
-
- // Get exact layout
- if (BuildCompat.isAtLeastS()) {
- val remoteViews = RemoteViews(sizes.associateWith(::createRemoteViews))
- appWidgetManager?.updateAppWidget(appWidgetId, remoteViews)
- }
- }
如下的日志显示 Size 变化的时候会将目标 Size 回传。
Widget : PedometerAppWidget#onAppWidgetOptionsChanged() size:[377.42856x132.0, 214.57143x216.57143]
之后从预设的精细布局里匹配相应的视图。
- private fun createRemoteViews(size: SizeF): RemoteViews {
- val smallView: RemoteViews = ...
- val tallView: RemoteViews = ...
- val wideView: RemoteViews = ...
- ...
-
-
- return when (size) {
- SizeF(100f, 100f) -> smallView
- SizeF(100f, 200f) -> tallView
- SizeF(200f, 100f) -> wideView
- ...
- }
- }
注意: 实际上 Size 列表由 Launcher 提供,如果 3rd Launcher 没有适配这一特性的话,回传的 Size 可能为空。
8. 自由地更新视图
RemoteViews 作为小组件视图的重要管理类,本次 OSV 也添加了诸多 API,以便更加自由地控制视图的展示。
更改颜色的 setColorStateList()
更改边距的 setViewLayoutMargin()
更改宽高的 setViewLayoutWidth() 等
这些新 API 可以助力我们实很多方便的功能,比如 CheckBox 选中之后更新文本颜色,思路很简单:
监听小组件的点击事件并传递目标视图
根据 CheckBox 的状态获得预设的文本颜色
使用 setColorStateList() 更新
- override fun onReceive(context: Context?, intent: Intent?) {
- ...
- // Get target widget.
- val appWidgetManager = AppWidgetManager.getInstance(context)
- val thisAppWidget = ComponentName(context!!.packageName, TodoListAppWidget::class.java.name)
- val appWidgetIds = appWidgetManager.getAppWidgetIds(thisAppWidget)
-
-
- // Update widget color parameters dynamically.
- for (appWidgetId in appWidgetIds) {
- val remoteViews = RemoteViews(context.packageName, R.layout.widget_todo_list)
- remoteViews.setColorStateList(
- viewId,
- "setTextColor",
- getColorStateList(context, checked)
- )
- appWidgetManager.updateAppWidget(appWidgetId, remoteViews)
- }
- }
-
-
- private fun getColorStateList(context: Context, checkStatus: Boolean): ColorStateList =
- if (checkStatus)
- ColorStateList.valueOf(context.getColor(R.color.widget_checked_text_color))
- else
- ColorStateList.valueOf(context.getColor(R.color.widget_unchecked_text_color))
再比如 Chart 线图太小,看不清楚。可以让它在点击之后放大,再点击之后恢复原样。
- // 根据记录的缩放状态获得预设的宽高
- // 通过setViewLayoutWidth和setViewLayoutHeight更新宽高
- override fun onReceive(context: Context?, intent: Intent?) {
- ...
- val widthScaleSize = if (scaleOutStatus) 200f else 260f
- val heightScaleSize = if (scaleOutStatus) 130f else 160f
-
-
- // Update widget layout parameters dynamically.
- for (appWidgetId in appWidgetIds) {
- val remoteViews = RemoteViews(context.packageName, R.layout.widget_pedometer)
- remoteViews.setViewLayoutWidth(viewId, widthScaleSize, TypedValue.COMPLEX_UNIT_DIP)
- remoteViews.setViewLayoutHeight(viewId, heightScaleSize, TypedValue.COMPLEX_UNIT_DIP)
- appWidgetManager.updateAppWidget(appWidgetId, remoteViews)
- }
- }
9. 流畅的启动效果
12 版本上点击 Widget 启动 App 的时候可以呈现更流畅的过渡效果,适配也很简单。官方指示只需给小组件的根布局指定 android 的 backgoround id 即可。
- <LinearLayout
- ...
- android:id="@android:id/background">
- </LinearLayout>
实际的动作显示添加这个 ID 后 App 启动没有什么变化,个中原因需要继续研究。
12 开始对从 Broadcast Receiver 或 Serivce 启动 Activity 做了更严格的限制,但不包括 Widget 发起的场合。但为了避免视觉上的突兀,这种后台启动的情况下不展示迁移动画。
10. 简化的数据绑定
小组件里展示 ListView 的需求也很常见,提供数据的话需要声明一个 RemoteViewsService 以返回 RemoteViewsFactory,比较绕。
而 12 里新增的 setRemoteAdapter (int , RemoteCollectionItems) API 则可以大大简化这个绑定过程。
比如制作一个即将到来的事件列表小组件,通过这个 API 便可以高效注入数据。
- private fun updateCountDownList(...) {
- ...
- // 创建用于构建Remote集合数据的Builder
- val builder = RemoteViews.RemoteCollectionItems.Builder()
- val menuResources = context.resources.obtainTypedArray(R.array.count_down_list_titles)
-
-
- // 往Builder里添加各Item对应的RemoteViews
- for (index in 0 until menuResources.length()) {
- ...
- builder.addItem(index.toLong(), constructRemoteViews(context, resId))
- }
-
-
- // 构建Remote集合数据
- // 并通过setRemoteAdapter直接放入到ListView里
- val collectionItems = builder.setHasStableIds(true).build()
- remoteViews.setRemoteAdapter(R.id.count_down_list, collectionItems)
- ...
- }
-
-
- // 创建ListView各Item对应的RemoteViews
- private fun constructRemoteViews(...): RemoteViews {
- val remoteViews = RemoteViews(context.packageName, R.layout.item_count_down)
- val itemData = context.resources.getStringArray(stringArrayId)
-
-
- // 遍历Item数据行设置对应的文本
- itemData.forEachIndexed { index, value ->
- val viewId = when (index) {
- 0 -> R.id.item_title
- 1 -> R.id.item_time
- ...
- }
- remoteViews.setTextViewText(viewId, value)
- }
- return remoteViews
- }
如果 Item 的布局不固定不止一种,可以使用 setViewTypeCount 指定布局类型的数目,告知 ListView 需要提供的 ViewHolder 种类。如果不指定也可以,系统将自动识别布局的种类,需要系统额外处理而已。
但要注意: 如果指定的数目和实际的不一致会引发异常。
IllegalArgumentException: View type count is set to 2, but the collection contains 3 different layout ids
另外,需要补充一下,支持该 API 的 View 必须是 AdapterView 的子类,比如常见的ListView、GridView 等。RecyclerView 是不支持的,毕竟小组件里数据量不多,不能使用也没关系。
11. 新增 API 总结
简要罗列一下 12 针对小组件新增的 API,方便大家查阅。
11.1 RemoteViews 类
方法 | 作用 |
RemoteViews(Map<SizeF, RemoteViews>) | 根据响应式布局映射表创建目标 RemoteViews |
addStableView() | 向 RemoteViews 动态添加子 View,类似 ViewGroup#addView() |
setCompoundButtonChecked() | 针对 CheckBox 或 Switch 控件更新选中状态 |
setRadioGroupChecked() | 针对 RadioButton 控件更新选中状态 |
setRemoteAdapter(int , RemoteCollectionItems) | 直接将数据填充进小组件的 ListView |
setColorStateList() | 动态更新小组件视图的颜色 |
setViewLayoutMargin() | 动态更新小组件视图的边距 |
setViewLayoutWidth()、setViewLayoutHeight() | 动态更新小组件视图的宽高 |
setOnCheckedChangeResponse() | 监听 CheckBox 等三种状态小组件的状态变化 |
11.2 XML 属性
属性 | 作用 |
description | 配置小组件在选择器里的补充描述 |
previewLayout | 配置小组件的预览布局 |
reconfigurable | 指定小组件的尺寸支持直接调节 |
configuration_optional | 指定小组件的内容可以采用默认设计,无需启动配置画面 |
targetCellWidth、targetCellHeight | 限定小组件所占的 Launcher 单元格 |
maxResizeWidth、maxResizeHeight | 配置小组件所能支持的最大高宽尺寸 |
结语
通过上面的解读,大家可以感受到 Google 在小组件的重新设计上耗费了诸多努力,它给这个老旧的功能注入很多新玩法和新花样。
简要回顾一下 Android 12 里小组件的新特性:
更便捷的小组件选择器
更美观的圆角边框设计
更灵活的小组件预览
更完整的控件支持
更方便的尺寸调节
更精准的布局控制
更自由的视图更新
更简便的列表数据绑定
如此之多的新特性,在助力小组件高效开发的同时,还能给用户呈现更加优秀的使用体验。
跟随 Android 12 的脚步,快快尝试起来,让现有的小组件重新绽放光彩。
未决事项
1. 小组件内部视图的圆角尺寸如何适配?
2. 小组件启动 App 的流畅过渡效果如何实现,是什么效果?
本文 DEMO
https://github.com/ellisonchan/NewAppWidget
参考资料
https://zhuanlan.zhihu.com/p/373609438
https://developer.android.google.cn/about/versions/12/features/widgets
https://github.com/android/user-interface-samples/tree/main/AppWidget
长按右侧二维码
查看更多开发者精彩分享
"开发者说·DTalk" 面向中国开发者们征集 Google 移动应用 (apps & games) 相关的产品/技术内容。欢迎大家前来分享您对移动应用的行业洞察或见解、移动开发过程中的心得或新发现、以及应用出海的实战经验总结和相关产品的使用反馈等。我们由衷地希望可以给这些出众的中国开发者们提供更好展现自己、充分发挥自己特长的平台。我们将通过大家的技术内容着重选出优秀案例进行谷歌开发技术专家 (GDE) 的推荐。
点击屏末 | 阅读原文 | 即刻报名参与 "开发者说·DTalk"
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。