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,方便大家查阅。
RemoteViews类
XML属性
结语
通过上面的解读,大家可以感受到Google在小组件的重新设计上耗费了诸多努力,它给这个老旧的功能注入很多新玩法和新花样。
简要回顾一下Android 12里小组件的新特性:
- 更便捷的小组件选择器
- 更美观的圆角边框设计
- 更灵活的小组件预览
- 更完整的控件支持
- 更方便的尺寸调节
- 更精准的布局控制
- 更自由的视图更新
- 更简便的列表数据绑定
如此之多的新特性,在助力小组件高效开发的同时,还能给用户呈现更加优秀的使用体验。
跟随Android 12的脚步,快快尝试起来,让现有的小组件重新绽放光彩。
未决事项
- 小组件内部视图的圆角尺寸如何适配?
- 小组件启动App的流畅过渡效果如何实现,是什么效果?
本文DEMO
https://github.com/ellisonchan/NewAppWidget