RE: Car Android HMI from scratch (2) - Widget

1. Widget overview

Widget, also known as "widgets", "widgets". A widget is a widget or control of an Android application placed on the home screen (Launcher). Widgets allow you to put your favorite apps on the home screen for quick access to them or to display key information.

Widgets can be of various types, such as info widgets, collection widgets, control widgets, and hybrid widgets. Android provides us with a complete framework to develop our own widgets. We have seen some common widgets such as music widgets, weather widgets, clock widgets, etc. on mobile phones.

Since the in-vehicle system requires us to additionally develop applications such as weather, music, and clock, Widget is also a required course in the development of in-vehicle applications. Not only that, but additional development is required when developing the car Launcher, so that the Launcher has the ability to place Widgets.

References for this article: developer.android.google.cn/guide/topic…


2. Create a Simplest Widget

1. Created Widgetlayout, 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>

res/xml2. Create a new XML under

The resource type of the XML file should be set to appwidget-providerthe basic properties used to define the Widget. In the XML file, define some properties as follows:

 <? 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" />

The specific meaning of each attribute will be described in detail in the next section.

AppWidgetProvider3. Implementation of the extension

AppWidgetProviderThe overridden Updaemethod, and in it is called AppWidgetManager.updateAppWidget()to update the data to the layout RemoteViews, the complete code is as follows:

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. Finally, declare in AndroidManifes.xmlAppWidgetProvider

<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>

Run this program and add this Widget on the Launcher, you can see a simplest 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中元素中的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扩展为一个方便的类来处理小部件广播。它只接收与小部件相关的事件广播,例如当小部件被更新、删除、启用和禁用时。当这些广播事件发生时,将调用以下方法:

  • onUpdate
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)。

  • onDeleted
public void onDeleted(Context context, int[] appWidgetIds) {
}

每次从窗口小部件主机中删除窗口小部件时,都会调用该函数。

  • onEnabled
public void onEnabled(Context context) {
}

这在第一次创建小部件的实例时调用。

例如,如果用户添加了两个小部件实例,则这只是第一次调用。如果您需要打开一个新的数据库或执行另一个只需要对所有小部件实例执行一次的设置,那么这是一个很好的地方。

  • onDisabled
public void onDisabled(Context context) {
}

当创建的小部件的最后一个实例从AppWidgetHost中删除时,将调用此函数。

  • onRestored
public void onRestored(Context context, int[] oldWidgetIds, int[] newWidgetIds) {
}

当AppWidget提供的实例从备份中恢复使调用。此方法调用后,会立即调用onUpdate。

当需要从持久化数据中恢复Widget时,需要重写此方法将旧的AppWidgetID重新映射到新值,并更新任何其他可能相关的状态。

  • onReceive

这是为每个广播调用的,通常不需要实现此方法。


5. Widget 的布局 - RemoteViews

RemoteViews是一个用于描述可在另一个进程中显示的视图层次结构的类。主要用于通知栏和Widget上。

在定义AppWidgetProviderInfo时需要把Widget的布局文件引入,Widget的布局与传统的Android布局文件一样,保存在项目的res/layout/下。

但是需要注意的是,Widget的布局基于RemoteViews,与传统的布局方式不同,并不是每种布局或视图Widget都支持。RemoteViews 仅支持以下布局类型:

FrameLayout
LinearLayout
RelativeLayout
GridLayout

以及以下控件类:

AnalogClock
Button
Chronometer
ImageButton
ImageView
ProgressBar
TextView
ViewFlipper
ListView
GridView
StackView
AdapterViewFlipper

Android 12 之后,支持的控件类增加了三个

CheckBox
Switch
RadioButton
RadioGroup

RemoteViews 也支持 ViewStub,它是一个大小为零的不可见视图,我们在使用传统布局,进行性能优化时也会经常使用。

5.1. RemoteViews 常用方法与说明

  • 创建 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)
  • 设定ProgressBar
 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);
}
  • 调整RemoteViews的布局属性
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集合,请参考官方文档:developer.android.google.cn/guide/topic…

2. 优化更新频率

定期更新

定期更新Widget很常见,但是updatePeriodMillis不能设定小于30分钟的数值,如果需要小于30分钟定时更新事件,建议搭配WorkManger使用,同时要把updatePeriodMillis设为0,禁用Widget的定期更新。

依据广播的更新

在车载HMI的开发中,有时候需要依据广播更新Widget,比较常见的是地图Widget,可选的做法是根据Location广播更新Widget。

根据广播更新Widget有以下注意事项:

更新持续时间

通常,系统允许广播接收器(通常在应用程序的主线程中运行)运行10 秒,然后再将其视为无响应并触发ANR错误。如果更新小组件需要更多时间,需要考虑以下替代方法:

  • 使用 WorkManager

  • 使用BroadcastReceiver.``goAsync方法为接收方提供更多时间。这允许接收器执行 30 秒。但是,在此处执行的任何工作都会阻止进一步的广播,直到它完成为止,因此过度利用这一点可能会适得其反,并导致以后的事件接收速度更慢

更新优先级

By default, broadcasts run as a background process, which means that when system resources are strained, broadcast receiver calls may be delayed. Intent.FLAG_RECEIVER_FOREGROUNDYou can increase the priority of broadcasts by setting them as foreground broadcasts .


7. Summary

Finally, let's summarize the usage of Widget, which is <appwidget-provider>used to define the basic properties and initial layout of widget. AppWidgetProviderEssentially a broadcast receiver, we AppWidgetProvideruse it to RemoteViewsdisplay the UI and populate the data, and finally AppWidgetMangerrefresh the UI.

In the car Android system, although the host of the Widget is also the Launcher, since the Launcher is generally re-developed by ourselves, how to accommodate the Widget also needs to be developed by the developers of the Launcher. The content of this piece is more complicated. It is recommended to read Building Apps Widget host , and refer to the source code implementation of AOSP-Launcher3.

In the next article, let's introduce the Android HMI components - SurfaceView and TextureView, which are used in parking radar and Camera.

Guess you like

Origin juejin.im/post/7118933842739593229