RE: 從零開始的車載Android HMI(二) - Widget
1. Widget 概述
Widget,又叫“微件”、“小部件”。小部件是放置在主螢幕(Launcher)上的Android應用程式的小工具或控制元件。通過小部件可以將自己喜歡的應用程式放在主螢幕上,以便快速訪問它們或是顯示一些重點資訊。
小部件可以是多種型別,例如資訊小部件、集合小部件、控制元件小部件和混合小部件。Android為我們提供了一個完整的框架來開發我們自己的小部件。在手機上我們已經看過一些常見的小部件,例如音樂小部件,天氣小部件,時鐘小部件等。
由於車載系統需要我們額外開發天氣、音樂、時鐘等應用,所以Widget在車載應用開發中,也算是必修課了。不僅如此,開發車載Launcher時還需要做額外開發,使Launcher具有擺放Widget的能力。
本文參考資料:http://developer.android.google.cn/guide/topics/appwidgets/overview
2. 建立一個最簡單的Widget
1.建立Widget
的佈局,simple_widget.xml
```
<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" />
```
2.在res/xml
下建立一個新的XML
XML檔案的資源型別應設定為appwidget-provider
用於定義Widget的基本屬性。在XML檔案中,定義一些屬性,如下所示:
```
各個屬性的具體含義,下一節會詳細介紹。
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
```
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/simple_widget_info" />
```
執行這個程式,並在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中
關於小部件尺寸的計算問題請參考 : Provide flexible widget layouts
3.2. AppWidgetProviderInfo 使用方法
AppWidgetProviderInfo
需要在res/xml中使用<appwidget-provider/>
標記將需要的屬性定義出來即可。
```
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(MapremoteViews)建立一個新的 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集合,請參考官方文件:http://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。
- Android車載應用開發與分析(1) - Android Automotive概述與編譯
- 【Android R】車載 Android 核心服務 - CarService 解析
- 【Android R】車載 Android 核心服務 - CarPropertyService
- 車載Android程式設計師的2022年終總結與轉行建議
- 從應用工程師的角度再談車載 Android 系統
- Android 車載應用開發與分析(12) - SystemUI (一)
- RE: 從零開始的車載Android HMI(三) - SurfaceView
- RE: 從零開始的車載Android HMI(二) - Widget
- RE: 從零開始的車載Android HMI(一) - Lottie
- Android 車載應用開發與分析 (3)- 構建 MVVM 架構(Java版)
- Android 車載應用開發與分析 (4)- 編寫基於AIDL 的 SDK