Android车载-小组件Widget
Widget,小部件是放置在主屏幕(Launcher)上的Android应用程序的小工具或控件。通过小部件可以将自己喜欢的应用程序放在主屏幕上,以便快速访问它们或是显示一些重点信息。
小部件可以是多种类型,例如信息小部件、集合小部件、控件小部件和混合小部件。Android为我们提供了一个完整的框架来开发我们自己的小部件。在手机上我们已经看过一些常见的小部件,例如音乐小部件,天气小部件,时钟小部件等。
官方对widget的描述:https://developer.android.google.cn/guide/topics/appwidgets/overview?hl=zh-cn
由于车载系统需要我们额外开发天气、音乐、时钟等应用,所以Widget在车载应用开发中,也算是必修课了。不仅如此,开发车载Launcher时还需要做额外开发,使Launcher具有摆放Widget的能力。
创建一个Widget
创建Widget的布局
simple_widget.xml
1 | <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" |
定义Widget的属性
在res/xml
下创建一个新的XML,XML文件的资源类型应设置为appwidget-provider
用于定义Widget的基本属性。在XML文件中,定义一些属性,如下所示:
1 | <? xml version="1.0" encoding="utf-8" ?> |
实现AppWidgetProvider
重写AppWidgetProvider
的Updae
方法,并在其中调用AppWidgetManager.updateAppWidget()
将数据更新到布局RemoteViews
中,完整的代码如下:
1 | class SimpleWidget : AppWidgetProvider() { |
声明AppWidgetProvider
1 | <receiver |
运行这个程序,并在Launcher上添加这个Widget,就可以看到一个最简单的Widget了。
到这一步,我们就完成了Widget的helloworld。总体来说Widget的架构组成如下所示,接下来我们逐个介绍每个组件的作用。
AppWidgetProviderInfo
AppWidgetProviderInfo
用于描述这个Widget的各种基本信息,包括layout布局,刷新频率以及AppWidgetProvider
。这些信息都会定义在xml中,tag标记是<appwidget-provider>
常用属性与说明
属性 | 说明 |
---|---|
updatePeriodMillis | 定义小部件通过调用onUpdate()回调方法从AppWidgetProvider请求更新的频率。实际更新不能保证使用此值准时进行,尽可能不频繁地更新。updatePeriodMillis不支持小于30分钟的值。如果要禁用定期更新,可以指定为0小部件的其他更新方式,请参考后面的 《小部件进阶用法 - 优化更新频率》 |
initialLayout | 指向定义小部件布局的布局资源。 |
initialKeyguardLayout | 指向定义小部件布局的布局资源。 |
configure | 定义用户添加小部件时启动的Activity,允许他们配置小部件属性。 |
description | 指定要为小部件显示的小部件选择器的描述。 Android 12中引入。 |
previewLayout (Android 12)previewImage (Android 11 and lower) | 从Android 12开始,previewLayout属性指定了一个可扩展的预览,您将提供一个设置为小部件默认大小的XML布局。理想情况下,指定为该属性的布局XML应该与具有实际默认值的实际小部件相同。 在Android 11或更低版本中,previewImage属性指定了小部件配置后的预览,用户在选择应用程序小部件时会看到该预览。如果未提供,则用户会看到应用程序的启动器图标。该字段对应于AndroidManifest中 |
autoAdvanceViewId | 指定小部件主机应自动推进的小部件子视图的视图ID。 Android 3.0中引入。 |
widgetCategory | 声明小部件是否可以显示在主屏幕(home_screen)、锁屏(keyguard)或两者上。只有低于5.0的Android版本支持锁屏小部件。对于Android 5.0及更高版本,只有home_screen有效。 |
widgetFeatures | 声明小部件支持的功能。例如,如果您希望小部件在用户添加时使用其默认配置,请指定configuration_optional和reconfigurable 。这绕过了在用户添加小部件后启动配置活动。(之后用户仍然可以重新配置小部件。) |
targetCellWidth、targetCellHeight (Android 12)minWidth、minHeight | 从Android 12开始,targetCellWidth和targetCellHeight属性指定小部件的默认大小(以网格单元为单位)。 在Android 11及更低版本中,这些属性将被忽略,如果主屏幕不支持基于网格的布局,则这些属性可能会被忽略。minWidth和minHeight属性指定dp中小部件的默认大小。如果小部件的最小宽度或高度的值与单元格的尺寸不匹配,则将这些值四舍五入到最接近的单元格大小。 注意:建议同时指定targetCellWidth/targetCellHeight和minWidth/minHeight属性集,以便在用户的设备不支持targetCellWidth和targetCellHeight的情况下,应用程序可以使用minWidth和minHeight。如果支持,targetCellWidth和targetCellHeight属性优先于minWidth和minHeight属性。 |
minResizeWidthminResizeHeight | 指定小部件的绝对最小大小。这些值应指定小部件无法辨认或无法使用的大小。使用这些属性,用户可以将小部件的大小调整为可能小于默认小部件大小的大小。如果minResizeWidth属性大于minWidth或未启用水平调整大小,则忽略该属性(请参见resizeMode)。 同样,如果minResizeHeight属性大于minHeight或未启用垂直调整大小,则忽略该属性。 Android 4.0中引入。 |
maxResizeWidthmaxResizeHeight | 指定小部件的建议最大大小。如果值不是网格单元尺寸的倍数,则会将其四舍五入到最近的单元尺寸。如果maxResizeWidth属性小于minWidth或未启用水平调整大小,则忽略该属性(请参见resizeMode)。 同样,如果maxResizeHeight属性大于minHeight或未启用垂直调整大小,则忽略该属性。 Android 12中引入。 |
resizeMode | 指定可以调整小部件大小的规则。可以使用此属性使主屏幕小部件可以水平、垂直或在两个轴上调整大小。用户长按小部件以显示其大小调整手柄,然后拖动水平和/或垂直手柄以更改其在布局网格上的大小。resizeMode属性的值包括horizontal、vertical和none。 要将小部件声明为可水平和垂直调整大小,请使用horizontal vertical。 在Android 3.1中引入。 |
关于小部件尺寸的计算问题请参考 : Provide flexible widget layouts
使用方法
AppWidgetProviderInfo
需要在res/xml中使用<appwidget-provider/>
标记将需要的属性定义出来即可。
1 | <? xml version="1.0" encoding="utf-8" ?> |
AppWidgetProvider
AppWidgetProvider是Widget的功能提供者,继承自BroadcastReceiver,本质上就是一个广播接收器,AppWidgetProvider也只是在onReceive中解析接收到的intent,并使用接收到的数据调用其他扩展方法。
1 | public void onReceive(Context context, Intent intent) { |
源码不复杂主要就是完成以下事件的分发逻辑
ACTION_APPWIDGET_UPDATE -> onUpdate
ACTION_APPWIDGET_DELETED -> onDeleted
ACTION_APPWIDGET_OPTIONS_CHANGED -> onAppWidgetOptionsChanged
ACTION_APPWIDGET_ENABLED -> onEnabled
ACTION_APPWIDGET_DISABLED -> onDisabled
ACTION_APPWIDGET_RESTORED -> onRestored
基本属性与说明
该类将BroadcastReceiver扩展为一个方便的类来处理小部件广播。它只接收与小部件相关的事件广播,例如当小部件被更新、删除、启用和禁用时。当这些广播事件发生时,将调用以下方法:
- onUpdate
1 | public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { |
如果在前面的AppWidgetProviderInfo
中定义了updatePeriodMillis
,系统会根据这个时间周期性的产生ACTION_APPWIDGET_UPDATE事件。当用户添加widget时也会产生这一事件。
此方法在用户添加小部件时也会调用,因此它应执行基本设置,例如为 View
对象定义事件处理程序或启动作业以加载要在小部件中显示的数据。但是,如果您声明了一个没有标志的配置活动,则在用户添加小部件时不会调用此方法,而是为后续更新调用此方法。配置活动负责在配置完成后执行第一次更新。
- onAppWidgetOptionsChanged
1 | public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, |
在第一次放置小部件或调整小部件的大小时产生这一事件。使用此回调可以根据小部件的大小范围显示或隐藏内容或者获取大小范围。
通过AppWidgetManager.getAppWidgetOptions(appWidgetId)
可以获取对应WidgetId的Bundle,其中包括以下内容:
OPTION_APPWIDGET_MIN_WIDTH:包含小部件实例的宽度下限(单位dp)。
OPTION_APPWIDGET_MIN_HEIGHT:包含小部件实例高度的下限(单位:dp)。
OPTION_APPWIDGET_MAX_WIDTH:包含小部件实例的宽度上限(单位:dp)。
OPTION_APPWIDGET_MAX_HEIGHT:包含小部件实例高度的上限(单位:dp)。
- onDeleted
1 | public void onDeleted(Context context, int[] appWidgetIds) { |
每次从窗口小部件主机中删除窗口小部件时,都会调用该函数。
- onEnabled
1 | public void onEnabled(Context context) { |
这在第一次创建小部件的实例时调用。
例如,如果用户添加了两个小部件实例,则这只是第一次调用。如果您需要打开一个新的数据库或执行另一个只需要对所有小部件实例执行一次的设置,那么这是一个很好的地方。
- onDisabled
1 | public void onDisabled(Context context) { |
当创建的小部件的最后一个实例从AppWidgetHost中删除时,将调用此函数。
- onRestored
1 | public void onRestored(Context context, int[] oldWidgetIds, int[] newWidgetIds) { |
当AppWidget提供的实例从备份中恢复使调用。此方法调用后,会立即调用onUpdate。
当需要从持久化数据中恢复Widget时,需要重写此方法将旧的AppWidgetID重新映射到新值,并更新任何其他可能相关的状态。
- onReceive
这是为每个广播调用的,通常不需要实现此方法。
RemoteViews
RemoteViews
是一个用于描述可在另一个进程中显示的视图层次结构的类。主要用于通知栏和Widget上。
在定义AppWidgetProviderInfo时需要把Widget的布局文件引入,Widget的布局与传统的Android布局文件一样,保存在项目的res/layout/
下。
但是需要注意的是,Widget的布局基于RemoteViews,与传统的布局方式不同,并不是每种布局或视图Widget都支持。RemoteViews 仅支持以下布局类型:
1 | FrameLayout |
以及以下控件类:
1 | AnalogClock |
Android 12 之后,支持的控件类增加了三个
1 | CheckBox |
RemoteViews 也支持 ViewStub
,它是一个大小为零的不可见视图,我们在使用传统布局,进行性能优化时也会经常使用。
常用方法与说明
- 创建
RemoteViews
RemoteViews(String packageName, int layoutId)创建一个新的 RemoteViews 对象,该对象将显示指定布局文件中包含的视图。 |
RemoteViews(String packageName, int layoutId, int viewId)创建一个新的 RemoteViews 对象,该对象将显示指定布局文件中包含的视图,并将根视图的 ID 更改为指定的 id。 |
RemoteViews(RemoteViews landscape, RemoteViews portrait)创建一个新的 RemoteViews 对象,该对象将填充为指定的横向或纵向 RemoteViews,具体取决于当前配置。 |
RemoteViews(Map<SizeF, RemoteViews> remoteViews)创建一个新的 RemoteViews 对象,该对象将使用最接近的大小规范来膨胀布局。 |
RemoteViews(RemoteViews src)基于RemoteViews创建一个副本。 |
- 设定文字
1 | void setTextViewText(int viewId, CharSequence text) |
相当于TextVIew.setText()
,setTextViewText
内部使用了setCharSequence
,所以其实也可以调用setCharSequence
来完成设定文字的操作。
1 | public void setTextViewText(int viewId, CharSequence text) { |
- 设定字体颜色
1 | void setTextColor(int viewId, int color) |
- 设定字体大小
1 | void setTextViewTextSize(int viewId, int units, float size) |
- 设定图片
1 | void setImageViewResource(int viewId, int srcId) |
1 | void setImageViewUri(int viewId, Uri uri) |
1 | void setImageViewBitmap(int viewId, Bitmap bitmap) |
1 | void setImageViewIcon(int viewId, Icon icon) |
- 设定单个控件的点击事件
1 | void setOnClickPendingIntent(int viewId, PendingIntent pendingIntent) |
1 | val url = "http://www.baidu.com" |
- 设定ProgressBar
1 | void setProgressBar(int viewId, int max, int progress, |
或者使用
1 | setBoolean(viewId, "setIndeterminate", indeterminate); |
- 调整RemoteViews的布局属性
1 | void setViewLayoutMargin(int viewId, int type, float value, int units) |
以上就是常用的一些方法,更多API,请参考官方文档:RemoteViews | Android Developers
Widget进阶用法
优化更新方法
在AppWidgetProvider
中更新RemoteViews有以下三种不同方式可供选择:
完整更新
调用AppWidgetManager.updateAppWidget
可以完整更新整个 widget。性能成本最大。
1 | val appWidgetManager = AppWidgetManager.getInstance(context) |
部分更新
调用AppWidgetManager.partialupdateAppWidget
可以只更新小部件指定的部分。此更新与updateAppWidget
的不同之处在于,传递的RemoteViews对象被理解为小部件的不完整表示,因此AppWidgetService不会缓存它。
注意,由于这些更新没有缓存,因此在使用AppWidgetService中的缓存版本还原Widget的情况下,它们修改的任何未由restoreInstanceState还原的状态都不会持久。
1 | val appWidgetManager = AppWidgetManager.getInstance(context) |
集合数据的更新
在RemoteViews中使用StackView、ListView、GridView时,需要使用
AppWidgetManager.notifyAppWidgetViewDataChanged
来更新视图的集合数据,这将触发RemoteViewsFactory.onDataSetChanged
。在此期间,旧数据将显示在Widget中。
1 | val appWidgetManager = AppWidgetManager.getInstance(context) |
集合Widget专门用于显示许多相同类型的元素,例如来自图库应用程序的图片集合、来自新闻应用程序的文章集合或来自通信应用程序的消息集合。
优化更新频率
定期更新
定期更新Widget很常见,但是updatePeriodMillis
不能设定小于30分钟的数值,如果需要小于30分钟定时更新事件,建议搭配WorkManger
使用,同时要把updatePeriodMillis
设为0,禁用Widget的定期更新。
依据广播的更新
在车载HMI的开发中,有时候需要依据广播更新Widget,比较常见的是地图Widget,可选的做法是根据Location广播更新Widget。
根据广播更新Widget有以下注意事项:
更新持续时间
通常,系统允许广播接收器(通常在应用程序的主线程中运行)运行10 秒,然后再将其视为无响应并触发ANR错误。如果更新小组件需要更多时间,需要考虑以下替代方法:
- 使用 WorkManager
- 使用
BroadcastReceiver.``goAsync
方法为接收方提供更多时间。这允许接收器执行 30 秒。但是,在此处执行的任何工作都会阻止进一步的广播,直到它完成为止,因此过度利用这一点可能会适得其反,并导致以后的事件接收速度更慢
更新优先级
默认情况下,广播作为后台进程运行,这意味着当系统资源紧张时可能会导致广播接收器调用延迟。可以通过将广播设定为前台广播Intent.FLAG_RECEIVER_FOREGROUND
,提高广播的优先级。
总结
最后我们再总结一下Widget的使用方法,<appwidget-provider>
用于定义widget的基本属性和初始布局。AppWidgetProvider
本质上就是一个广播接收器,我们在AppWidgetProvider
中使用RemoteViews
显示UI并填充数据,最后使用AppWidgetManger
刷新UI。
在车载Android系统中,虽然Widget的宿主也是Launcher,但是由于Launcher一般是我们自己重新开发的,所以,如何容纳Widget也是需要Launcher的开发者额外开发的,这块的内容比较复杂,建议阅读构建应用Widget宿主,并参考AOSP-Launcher3的源码实现。