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上,则会显示加载失败。
日志
AppWidgetHostView: Error inflating AppWidget AppWidgetProviderInfo(UserHandle{0}/ComponentInfo{com.example.splash/com.example.splash.widget.TodoListAppWidget}): android.view.InflateException:
Binary XML file line #13 in com.example.splash:layout/widget_todo_list: 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() }