1. Widget 概述
Widget,又叫“微件”、“小部件”。小部件是放置在主屏幕(Launcher)上的Android应用步伐的小工具或控件。通过小部件可以将自己喜欢的应用步伐放在主屏幕上,以便快速访问它们或是表现一些重点信息。
小部件可以是多种范例,比方信息小部件、聚集小部件、控件小部件和肴杂小部件。Android为我们提供了一个完备的框架来开辟我们自己的小部件。在手机上我们已经看过一些常见的小部件,比方音乐小部件,气候小部件,时钟小部件等。
由于车载体系须要我们额外开辟气候、音乐、时钟等应用,以是Widget在车载应用开辟中,也算是必修课了。不光云云,开辟车载Launcher时还须要做额外开辟,使Launcher具有摆放Widget的本领。
本文参考资料:https://developer.android.google.cn/guide/topics/appwidgets/overview
2. 创建一个最简单的Widget
1.创建Widget的布局,simple_widget.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" style="@style/Widget.CarWidget.AppWidget.Container" android:layout_width="match_parent" android:layout_height="match_parent" android:theme="@style/Theme.CarWidget.AppWidgetContainer"> <TextView android:id="@+id/appwidget_text" style="@style/Widget.CarWidget.AppWidget.InnerView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_centerVertical="true" android:layout_margin="8dp" android:contentDescription="@string/appwidget_text" android:text="@string/appwidget_text" android:textSize="24sp" android:textStyle="bold|italic" /></RelativeLayout>2.在res/xml下创建一个新的XML
XML文件的资源范例应设置为appwidget-provider用于界说Widget的根本属性。在XML文件中,界说一些属性,如下所示:
<? xml version="1.0" encoding="utf-8" ?><appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:initialLayout="@layout/simple_widget" android:minWidth="100dp" android:minHeight="100dp" android:updatePeriodMillis="0" />各个属性的详细含义,下一节会详细介绍。
3.扩展AppWidgetProvider的实现
重写AppWidgetProvider的Updae方法,并在此中调用AppWidgetManager.updateAppWidget()将数据更新到布局RemoteViews中,完备的代码如下:
class SimpleWidget : AppWidgetProvider() { override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray ) { for (appWidgetId in appWidgetIds) { updateAppWidget(context, appWidgetManager, appWidgetId) } Log.e(TAG, "onUpdate: $appWidgetIds") }}internal fun updateAppWidget(context: Context,appWidgetManager: AppWidgetManager, appWidgetId: Int) { val widgetText = "林栩" val views = RemoteViews(context.packageName, R.layout.simple_widget) views.setTextViewText(R.id.appwidget_text, widgetText) // 更新整个widget appWidgetManager.updateAppWidget(appWidgetId, views)}4.末了,在AndroidManifes.xml中声明AppWidgetProvider
<receiver android:name=".SimpleWidget" android:exported="false"> <intent-filter> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> </intent-filter> <meta-data android:name="android.appwidget.provider" android:resource="@xml/simple_widget_info" /></receiver>运行这个步伐,并在Launcher上添加这个Widget,就可以看到一个最简单的Widget了。
到这一步,我们就完成了Widget的helloworld。总体来说Widget的架构构成如下所示,接下来我们逐个介绍每个组件的作用。
3. 界说小部件的基础属性 - AppWidgetProviderInfo
AppWidgetProviderInfo用于形貌这个Widget的各种根本信息,包罗layout布局,革新频率以及AppWidgetProvider。这些信息都会界说在xml中,tag标志是<appwidget-provider>
3.1. AppWidgetProviderInfo 常用属性与说明
属性说明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中<receiver>元素中的android:previewImage属性。
注意:发起同时指定previewImage和previewLayout属性,以便在用户的装备不支持previewLayout的环境下,应用步伐可以使用previewImage。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
3.2. AppWidgetProviderInfo 使用方法
AppWidgetProviderInfo须要在res/xml中使用<appwidget-provider/>标志将须要的属性界说出来即可。
<? xml version="1.0" encoding="utf-8" ?><appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:configure="com.android.car.carwidget.SimpleWidgetConfigureActivity" android:description="@string/app_widget_description" android:initialKeyguardLayout="@layout/simple_widget" android:initialLayout="@layout/simple_widget" android:minWidth="50dp" android:minHeight="50dp" android:previewImage="@drawable/example_appwidget_preview" android:previewLayout="@layout/simple_widget" android:resizeMode="horizontal|vertical" android:targetCellWidth="2" android:targetCellHeight="2" android:updatePeriodMillis="86400000" android:widgetCategory="home_screen|keyguard" />4.Widget功能提供者 - AppWidgetProvider
AppWidgetProvider继续自BroadcastReceiver,本质上就是一个广播吸收器,AppWidgetProvider也只是在onReceive中剖析吸收到的intent,并使用吸收到的数据调用其他扩展方法。
public void onReceive(Context context, Intent intent) { //防止恶意更新广播(不是真正的安全标题,只是过滤出坏的Broacast,如许子类就不太大概瓦解)。String action = intent.getAction(); if (AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)) { Bundle extras = intent.getExtras(); if (extras != null) { int[] appWidgetIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS); if (appWidgetIds != null && appWidgetIds.length > 0) { this.onUpdate(context, AppWidgetManager.getInstance(context), appWidgetIds); } } } else if (AppWidgetManager.ACTION_APPWIDGET_DELETED.equals(action)) { Bundle extras = intent.getExtras(); if (extras != null && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_ID)) { final int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID); this.onDeleted(context, new int[] { appWidgetId }); } } else if (AppWidgetManager.ACTION_APPWIDGET_OPTIONS_CHANGED.equals(action)) { Bundle extras = intent.getExtras(); if (extras != null && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_ID) && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS)) { int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID); Bundle widgetExtras = extras.getBundle(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS); this.onAppWidgetOptionsChanged(context, AppWidgetManager.getInstance(context), appWidgetId, widgetExtras); } } else if (AppWidgetManager.ACTION_APPWIDGET_ENABLED.equals(action)) { this.onEnabled(context); } else if (AppWidgetManager.ACTION_APPWIDGET_DISABLED.equals(action)) { this.onDisabled(context); } else if (AppWidgetManager.ACTION_APPWIDGET_RESTORED.equals(action)) { Bundle extras = intent.getExtras(); if (extras != null) { int[] oldIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_OLD_IDS); int[] newIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS); if (oldIds != null && oldIds.length > 0) { this.onRestored(context, oldIds, newIds); this.onUpdate(context, AppWidgetManager.getInstance(context), newIds); } } }}源码不复杂重要就是完成以下事件的分发逻辑
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
4.1. AppWidgetProvider 根本属性与说明
该类将BroadcastReceiver扩展为一个方便的类来处理处罚小部件广播。它只吸收与小部件相关的事件广播,比方当小部件被更新、删除、启用和禁用时。当这些广播事件发生时,将调用以下方法:
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {}假如在前面的AppWidgetProviderInfo中界说了updatePeriodMillis,体系会根据这个时间周期性的产生ACTION_APPWIDGET_UPDATE事件。当用户添加widget时也会产生这一事件。
此方法在用户添加小部件时也会调用,因此它应实行根本设置,比方为 View 对象界说事件处理处罚步伐或启动作业以加载要在小部件中表现的数据。但是,假如您声明白一个没有标志的设置运动,则在用户添加小部件时不会调用此方法,而是为后续更新调用此方法。设置运动负责在设置完成后实行第一次更新。
- onAppWidgetOptionsChanged
public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions) {}在第一次放置小部件或调解小部件的巨细时产生这一事件。使用此回调可以根据小部件的巨细范围表现或隐蔽内容大概获取巨细范围。
通过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)。
public void onDeleted(Context context, int[] appWidgetIds) {}每次从窗口小部件主机中删除窗口小部件时,都会调用该函数。
public void onEnabled(Context context) {}这在第一次创建小部件的实例时调用。
比方,假如用户添加了两个小部件实例,则这只是第一次调用。假如您须要打开一个新的数据库或实行另一个只须要对全部小部件实例实行一次的设置,那么这是一个很好的地方。
public void onDisabled(Context context) {}当创建的小部件的末了一个实例从AppWidgetHost中删除时,将调用此函数。
public void onRestored(Context context, int[] oldWidgetIds, int[] newWidgetIds) {}当AppWidget提供的实例从备份中规复使调用。此方法调用后,会立即调用onUpdate。
当须要从长期化数据中规复Widget时,须要重写此方法将旧的AppWidgetID重新映射到新值,并更新任何其他大概相关的状态。
这是为每个广播调用的,通常不须要实现此方法。
5. Widget 的布局 - RemoteViews
RemoteViews是一个用于形貌可在另一个历程中表现的视图条理布局的类。重要用于关照栏和Widget上。
在界说AppWidgetProviderInfo时须要把Widget的布局文件引入,Widget的布局与传统的Android布局文件一样,生存在项目标res/layout/下。
但是须要注意的是,Widget的布局基于RemoteViews,与传统的布局方式差异,并不是每种布局或视图Widget都支持。RemoteViews 仅支持以下布局范例:
FrameLayoutLinearLayoutRelativeLayoutGridLayout以及以下控件类:
AnalogClockButtonChronometerImageButtonImageViewProgressBarTextViewViewFlipperListViewGridViewStackViewAdapterViewFlipperAndroid 12 之后,支持的控件类增长了三个
CheckBoxSwitchRadioButtonRadioGroupRemoteViews 也支持 ViewStub,它是一个巨细为零的不可见视图,我们在使用传统布局,举行性能优化时也会经常使用。
5.1. 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创建一个副本。
void setTextViewText(@IdRes int viewId, CharSequence text)相称于TextVIew.setText(),setTextViewText内部使用了setCharSequence,以是实在也可以调用setCharSequence来完成设定笔墨的操作。
public void setTextViewText(@IdRes int viewId, CharSequence text) { setCharSequence(viewId, "setText", text);}
void setTextColor(@IdRes int viewId, @ColorInt int color)void setInt(viewId, "setTextColor", color);
void setTextViewTextSize(@IdRes int viewId, int units, float size)
void setImageViewResource(@IdRes int viewId, @DrawableRes int srcId)void setInt(viewId, "setImageResource", srcId);void setImageViewUri(@IdRes int viewId, Uri uri)void setUri(viewId, "setImageURI", uri);void setImageViewBitmap(@IdRes int viewId, Bitmap bitmap)void setBitmap(viewId, "setImageBitmap", bitmap);void setImageViewIcon(@IdRes int viewId, Icon icon)void setIcon(viewId, "setImageIcon", icon);
void setOnClickPendingIntent(@IdRes int viewId, PendingIntent pendingIntent)void setOnClickResponse(@IdRes int viewId, @NonNull RemoteResponse response) val url = "http://www.baidu.com"val intent = Intent(Intent.ACTION_VIEW)intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)intent.data = Uri.parse(url)val pending = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_MUTABLE)views.setOnClickPendingIntent(R.id.appwidget_text, pending)appWidgetManager.updateAppWidget(appWidgetId, views)
void setProgressBar(@IdRes int viewId, int max, int progress, boolean indeterminate)大概使用
setBoolean(viewId, "setIndeterminate", indeterminate);if (!indeterminate) { setInt(viewId, "setMax", max); setInt(viewId, "setProgress", progress);}
void setViewLayoutMargin(@IdRes int viewId, @MarginType int type, float value, @ComplexDimensionUnit int units)void setViewLayoutHeight(@IdRes int viewId, float height, @ComplexDimensionUnit int units)void setViewLayoutWidth(@IdRes int viewId, float width, @ComplexDimensionUnit int units)以上就是常用的一些方法,更多API,请参考官方文档:RemoteViews | Android Developers
6. Widget 进阶用法
6.1. 优化更新方式
在AppWidgetProvider中更新RemoteViews有以下三种差异方式可供选择:
完备更新
调用AppWidgetManager.updateAppWidget可以完备更新整个 widget。性能资本最大。
val appWidgetManager = AppWidgetManager.getInstance(context)val views = RemoteViews(context.packageName, R.layout.simple_widget)views.setTextViewText(R.id.appwidget_text, widgetText)appWidgetManager.updateAppWidget(appWidgetId, views)部门更新
调用AppWidgetManager.partialupdateAppWidget可以只更新小部件指定的部门。此更新与updateAppWidget的差异之处在于,通报的RemoteViews对象被明白为小部件的不完备表现,因此AppWidgetService不会缓存它。
注意,由于这些更新没有缓存,因此在使用AppWidgetService中的缓存版本还原Widget的环境下,它们修改的任何未由restoreInstanceState还原的状态都不会长期。
val appWidgetManager = AppWidgetManager.getInstance(context)val views = RemoteViews(context.packageName, R.layout.simple_widget)views.setTextViewText(R.id.appwidget_text, widgetText)appWidgetManager.partiallyUpdateAppWidget(appWidgetId, views)聚集数据的更新
在RemoteViews中使用StackView、ListView、GridView时,须要使用
AppWidgetManager.notifyAppWidgetViewDataChanged来更新视图的聚集数据,这将触发RemoteViewsFactory.onDataSetChanged。在此期间,旧数据将表现在Widget中。
val appWidgetManager = AppWidgetManager.getInstance(context)appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.widget_listview)聚集Widget专门用于表现许多雷同范例的元素,比方来自图库应用步伐的图片聚集、来自消息应用步伐的文章聚集或来自通信应用步伐的消息聚集。
关于怎样开辟Widget聚集,请参考官方文档:https://developer.android.google.cn/guide/topics/appwidgets/collections
2. 优化更新频率
定期更新
定期更新Widget很常见,但是updatePeriodMillis不能设定小于30分钟的数值,假如须要小于30分钟定时更新事件,发起搭配WorkManger使用,同时要把updatePeriodMillis设为0,禁用Widget的定期更新。
依据广播的更新
在车载HMI的开辟中,偶尔候须要依据广播更新Widget,比力常见的是舆图Widget,可选的做法是根据Location广播更新Widget。
根据广播更新Widget有以下注意事项:
更新连续时间
通常,体系允许广播吸收器(通常在应用步伐的主线程中运行)运行10 秒,然后再将其视为无相应并触发ANR错误。假如更新小组件须要更多时间,须要思量以下替换方法:
- 使用 WorkManager
- 使用BroadcastReceiver.``goAsync方法为吸收方提供更多时间。这允许吸收器实行 30 秒。但是,在此处实行的任何工作都会制止进一步的广播,直到它完成为止,因此太过使用这一点大概会拔苗助长,并导致以后的事件吸收速率更慢
更新优先级
默认环境下,广播作为背景历程运行,这意味着当体系资源告急时大概会导致广播吸收器调用耽误。可以通过将广播设定为前台广播Intent.FLAG_RECEIVER_FOREGROUND,提高广播的优先级。
7. 总结
末了我们再总结一下Widget的使用方法,<appwidget-provider>用于界说widget的根本属性和初始布局。AppWidgetProvider本质上就是一个广播吸收器,我们在AppWidgetProvider中使用RemoteViews表现UI并填凑数据,末了使用AppWidgetManger革新UI。
在车载Android体系中,固然Widget的宿主也是Launcher,但是由于Launcher一样寻常是我们自己重新开辟的,以是,怎样容纳Widget也是须要Launcher的开辟者额外开辟的,这块的内容比力复杂,发起阅读构建应用Widget宿主,并参考AOSP-Launcher3的源码实现。
下一篇,我们来介绍泊车雷达、Camera中须要用到的Android HMI 组件 - SurfaceView、TextureView。 |