你可能用過一些 Android APP
的小組件,比如:
- 支付寶的小組件:之前疫情期間添加了對應小組件卡片在桌面,可點擊小卡片上的查看健康碼的按鈕,可一鍵打開健康碼。
- 音樂類
APP
的小組件:添加對應對應小組件后, 可在 APP 的主屏幕中輕松看到當前播放歌曲的相關信息:歌曲封面、歌曲名、歌手名稱、所屬專輯名稱等。 - 時鐘類
APP
的小組件:可添加各種樣式的時鐘小組件在屏幕,裝飾你的主屏幕,在你喜歡的小組件上來查看時間。 - 天氣類
APP
的小組件:可在主屏幕直接看到天氣相關信息,不用再打開天氣的APP
。
如果你所開發的項目的產品經理并沒有相關開發需求,可能很多 Android
開發并沒有接觸過相關桌面主屏幕的相關小組件開發,這篇文章主要介紹下相關開發的知識點。
AppWidgetProvider:
幫助實現AppWidget
提供程序的便利類。 你可以用AppWidgetProvider
做的事情,你也可以用一個普通的BroadcastReceiver
做。AppWidgetProvider
只是從onReceive(Context,Intent)
中接收到的Intent
中解析出相關字段,并使用接收到的extra
調用hook
方法。
擴展此類并覆蓋onUpdate
、onDeleted
、onEnabled
或onDisabled
方法中的一個或多個以實現您自己的AppWidget
功能。
public class WidgetDemoProvider extends AppWidgetProvider {
/**
* 當 Widget 第一次被添加時調用,例如用戶添加了兩個你的 Widget,那么只有在添加第一個 Widget 時該
* 方法會被調用。所以該方法比較適合執行你所有 Widgets 只需進行一次的操作。對用廣播的 Action 為
* ACTION_APPWIDGET_ENABLE。
*/
@Override
public void onEnabled(Context context) {
super.onEnabled(context);
}
/**
* 與 onEnabled 恰好相反,當你的最后一個 Widget 被刪除時調用該方法,所以這里用來清理之前在 onEnabled() 中進行的操作。
* 當最后一個該類型的小部件從桌面移除時調用,對應的廣播的 Action 為 ACTION_APPWIDGET_DISABLED。
*
*/
public void onDisabled(Context context) {
super.onDisabled(context);
}
/**
* 當 Widget 第一次被添加或者大小發生變化時調用該方法,可以在此控制 Widget 元素的顯示和隱藏。
* 當小部件布局發生更改的時候調用。對應廣播的 Action 為 ACTION_APPWIDGET_OPTIONS_CHANGED。
*
* @param appWidgetManager 您可以調用 AppWidgetManager.updateAppWidget 的 AppWidgetManager 對象。
* @param appWidgetId 大小改變的widget的appWidgetId。
* @param newOptions
*/
@Override
public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions) {
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions);
}
/**
* 當小部件從備份中還原,或者恢復設置的時候,會調用,實際用的比較少。對應廣播的 Action 為 ACTION_APPWIDGET_RESTORED。
* @param oldWidgetIds
* @param newWidgetIds
*/
@Override
public void onRestored(Context context, int[] oldWidgetIds, int[] newWidgetIds) {
super.onRestored(context, oldWidgetIds, newWidgetIds);
}
/**
* AppWidget 更新事件
*
* 小部件被添加時或者每次小部件更新時都會調用一次該方法,配置文件中配置小部件的更新周期 updatePeriodMillis,每次更新都會調用。對應廣播 Action 為:ACTION_APPWIDGET_UPDATE 和 ACTION_APPWIDGET_RESTORED
*
* @param appWidgetManager 更新 AppWidget 狀態; 獲取有關已安裝 AppWidget 提供程序和其他 AppWidget 相關狀態的信息。
* @param appWidgetIds 需要更新的 appWidgetIds。 請注意,這可能是此提供程序的所有 AppWidget 實例,也可能只是其中的一個子集。
*/
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager,
int[] appWidgetIds) {
}
/**
* 當 Widget 被刪除時調用該方法。
*
* 每刪除一個小部件就調用一次。對應的廣播的 Action 為: ACTION_APPWIDGET_DELETED 。
*
* @param appWidgetIds 已從其組件集群中刪除的 appWidgetIds。
*/
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
super.onDeleted(context, appWidgetIds);
}
/**
* 接收廣播的回調函數
* onReceive() 中處理的是 Widget 相關的廣播事件,然后分發到各個回調函數中onUpdate(), onDeleted(), onEnabled(), onDisabled, onAppWidgetOptionsChanged()。
*
* @param context
* @param intent
*/
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (AppWidgetManager.ACTION_APPWIDGET_ENABLED.equals(action)) {
this.onEnabled(context);
}
else if (AppWidgetManager.ACTION_APPWIDGET_DISABLED.equals(action)) {
this.onDisabled(context);
}
...省略
super.onReceive(context, intent);
}
}
根據自己的業務,寫好 WidgetDemoProvider
的自定義代碼后,需要在 AndroidManifest.xml
里注冊一下:
<receiver android:name=".widget.WidgetDemoProvide"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="xxx..."/> <!-- 此處可以添加自己需要的 -->
</intent-filter>
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/appwidget_demo" /> <!-- 此處可以添加自己需要的給用戶提前預覽的自定義小組件布局 -->
</receiver>
<receiver>
元素需要android:name
屬性,該屬性指定小部件使用的AppWidgetProvider
(AppWidgetProvider
的父類就是BroadcastReceiver
)。
<intent-filter>
中的<action>
元素指定小部件接受ACTION_APPWIDGET_UPDATE
廣播。這是必須明確聲明的唯一一項廣播,用以接收小部件的增刪改等信息。
<meta-data>
元素指定小部件的資源,并且需要以下屬性:
android:name
- 指定元數據名稱。必須使用android.appwidget.provider
將數據標識為AppWidgetProviderInfo
描述符。
android:resource
- 指定AppWidgetProviderInfo
資源位置。
appwidget_demo.xml
的示例代碼:
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="180dp"
android:minHeight="180dp"
android:updatePeriodMillis="1800000"
android:previewImage="@mipmap/app_widget_preview_3x3"
android:initialLayout="@layout/app_widget_preview_layout_3_3"
android:resizeMode="horizontal|vertical">
</appwidget-provider>
minWidth
和minHeight
:指定小部件默認情況下占用的最小空間。
注意:為使小部件能夠在設備間移植,小部件的最小大小不得超過 4 x 4 單元格。
minResizeWidth
和minResizeHeight
:指定小部件的絕對最小大小。
updatePeriodMillis
:定義小部件框架通過調用 onUpdate() 回調方法來從 AppWidgetProvider 請求更新的頻率應該是多大。
initialLayout
:指向用于定義小部件布局的布局資源。
configure
:定義要在用戶添加小部件時啟動以便用戶配置小部件屬性的 Activity。
previewImage
:指定預覽來描繪小部件經過配置后是什么樣子的,用戶在選擇小部件時會看到該預覽。
autoAdvanceViewId
:指定應由小部件的托管應用自動跳轉的小部件子視圖的視圖 ID。
resizeMode
:指定可以按什么規則來調整微件的大小,可選值為“horizontal|vertical”,一般默認設置橫豎都可以進行調整。
minResizeHeight
:指定可將微件大小調整到的最小高度。
minResizeWidth
:指定可將微件大小調整到的最小寬度。
widgetCategory
:聲明小部件是否可以顯示在主屏幕 (home_screen
) 或鎖定屏幕 (keyguard
) 上。只有低于 5.0 的 Android 版本才支持鎖定屏幕微件。對于 Android 5.0 及更高版本,只有home_screen
有效,所以現在將這個值寫為home_screen
即可。
如果在自己的相關業務代碼里,比如 activity
里如何觸發 WidgetDemoProvider
相關數據以及頁面更新。可以看以下示例代碼:
private void sendNotify(){
try {
Class javaClass = Class.forName("xxx.WidgetDemoProvider");
final Intent intent = new Intent(this, javaClass);
intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
int[] ids = AppWidgetManager.getInstance(GlobeContext.context).getAppWidgetIds(new ComponentName(GlobeContext.context, javaClass));
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids);
sendBroadcast(intent);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
一些開發注意事項:
- 小組件的寬高是可以支持用戶自行調整的,只需簡單的設置最低寬高,但是可調整的最小粒度是根據手機的
icon
為標準。小組件數量無限制,用戶對小組件的大小和具體功能喜好都不太一樣,所以解決方案就簡單粗暴一點,你能想到的適配尺寸,每種尺寸搞一個,用戶自己選擇合適的尺寸就好。大、中、小、大中、中小、微小、超大等尺寸,可以全部做一遍。
android:resizeMode="horizontal|vertical"
widget 可以被拉伸的方向。horizontal表示可以水平拉伸,vertical表示可以豎直拉伸
- 更新時間分為主動更新和定時更新。
主動更新:即在APP
中可以動態更新這個桌面小組件,這種情況更新沒有時間限制。
定時更新:小組件需要展示的數據可能已經發生了變化,但是APP
已經被系統殺死了,無法主動更新數據,就會導致小組件展示的數據可能是已過期的或者是舊的,這時候就可以用到小組件的定時更新功能,但是這個定時更新有一個限制,基于省電邏輯,最快的更新周期為 30 分鐘。(如果是在onUpdate
方法中寫個定時器定時更新,這樣是不行的,會被系統給殺死,殺死之后小組件不會消失,而是一直顯示最后一次更新時候的狀態,直到下一次更新數據,類似于電子水墨屏的邏輯。) - 一般點擊整個小組件,我們直接調起
APP
。點擊跳轉頁面需要用到PendingIntent
,這玩意的Flag
有很多種模式,具體可以查看文章底部的參考文檔,坑就坑在這個Flag
,31 之后的系統有改動,會報錯,所以 31 的系統需要用PendingIntent.FLAG_IMMUTABLE
,具體看代碼。
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (AppWidgetManager.ACTION_APPWIDGET_ENABLED.equals(action)) {
this.onEnabled(context);
}
else if (AppWidgetManager.ACTION_APPWIDGET_DISABLED.equals(action)) {
this.onDisabled(context);
}
if (intent.hasCategory(Intent.CATEGORY_ALTERNATIVE)) {
Uri data = intent.getData();
int buttonId = Integer.parseInt(data.getSchemeSpecificPart());
switch (buttonId) {
case R.id.widget_layout:
Intent intent = new Intent(context, RemotePlayerActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(goIntent);
RemoteViews remoteView = new RemoteViews(context.getPackageName(),R.layout.app_widget_layout);
//將按鈕與點擊事件綁定
remoteView.setOnClickPendingIntent(R.id.widget_layout,getPendingIntent(context, R.id.widget_layout));
break;
}
}
super.onReceive(context, intent);
}
private PendingIntent getPendingIntent(Context context, int buttonId) {
Intent intent = new Intent();
intent.setClass(context, WidgetDemoProvider.class);
intent.addCategory(Intent.CATEGORY_ALTERNATIVE);
intent.setData(Uri.parse("harvic:" + buttonId));
PendingIntent pendingIntent;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE);
} else {
pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0);
}
return pendingIntent;
}
- 通過
PendingIntent
就可以直接調起APP
的相關頁面,不過這里也有坑,假設你APP
的啟動頁面是MainActivity
頁面,點擊小組件你就讓它跳轉到MainActivity
頁面走正常的APP
啟動流程,就等同于是點擊小組件就能啟動APP
,哪怕APP
被殺死了。坑就坑在于,通過這種方式打開的APP
,它不會走Application
類,也就是你如果是在Application
中初始化了某些東西,但是APP
已經被系統殺死了,這時候你再點擊小組件啟動APP
,這時就會發現好多組件用不了,因為沒初始化。
所以相對省事的做法就是把Application
的所有需要初始化的東西都放MainActivity
里面初始化了,目前為了工信部隱私相關合規,應該很多APP
的初始化代碼應該已經從Application
放到用戶點同意隱私協議彈框后再去初始化了。
RemoteViews
,從字面意思理解為它是一個遠程視圖。是一種遠程的View
,它在其它進程中顯示,卻可以在另一個進程中更新。RemoteViews
在Android
中的使用場景主要有:自定義通知欄和桌面小部件。
RemoteViewsService
,是管理RemoteViews
的服務。一般,當AppWidget
中包含GridView
、ListView
、StackView
等集合視圖時,才需要使用RemoteViewsService
來進行更新、管理。RemoteViewsService
更新集合視圖的一般步驟是:通過setRemoteAdapter()
方法來設置RemoteViews
對應RemoteViewsService
。
之后在RemoteViewsService
中,實現RemoteViewsFactory
接口。然后,在RemoteViewsFactory
接口中對集合視圖的各個子項進行設置,例如ListView
中的每一Item
。
RemoteViewsFactory
通過RemoteViewsService
中的介紹,我們知道RemoteViewsService
是通過
RemoteViewsFactory
來具體管理layout
中集合視圖的,RemoteViewsFactory
是RemoteViewsService
中的一個內部接口。RemoteViewsFactory
提供了一系列的方法管理集合視圖中的每一項。
例如:
public RemoteViews getViewAt(int position):
通過getViewAt()來獲取“集合視圖”中的第position項的視圖,視圖是以RemoteViews的對象返回的。
public int getCount() :
通過getCount()來獲取“集合視圖”中所有子項的總數。
-
用戶可重新設置原有 widget。在
Android 12
之前,重新設置widget
意味著用戶必須刪除現有widget
,然后使用新配置重新添加。Android 12
在多個方面改進了widget
的配置方式,從而幫助用戶采用更簡單的方式對widget
進行個性化配置。可重組的widget
允許用戶對widget
進行自定義設置。在Android 12
中,用戶將無需通過刪除和重新添加widget
來調整這些原有設定。
要使用這一功能,您需在appwidget-provider
中把widgetFeatures
屬性設置為reconfigurable
。
當用戶配置該widget
時,新的配置會被記錄在ListWidgetConfigureActivity
中。
如果您的widget
依賴默認設置,在Android 12
中您可跳過初始化操作,通過默認配置來設置widget
。
<appwidget-provider
android:configure="com.example.android.appwidget.ListWidgetConfigureActivity"
android:widgetFeatures="reconfigurable|configuration_optional"
... />
-
Android 12 中 Widget 的尺寸限制改進。除了現有的
minWidth
、minHeigh
、minResizeWidth
以及minResizeHeight
以外,Android 12
還添加了新的appwidget-provider
屬性。您可以使用新的maxResizeWidth
和maxResizeHeight
屬性,來定義用戶所能夠調整的widget
尺寸的最大高度和寬度。新的targetCellWidth
和targetCellHeight
屬性能夠定義設備主屏幕上的widget
默認尺寸。如果之前有targetCellWidth
和targetCellHeight
屬性的話,小部件也不至于像現在這么亂而導致用戶不想使用。
<appwidget-provider
android:maxResizeWidth="240dp"
android:maxResizeHeight="180dp"
android:minWidth="180dp"
android:minHeight="110dp"
android:minResizeWidth="180dp"
android:minResizeHeight="110dp"
android:targetCellWidth="3"
android:targetCellHeight="2"
... />
<!-- maxResizeWidth:定義用戶所能夠調整的小部件尺寸的最大寬度
maxResizeHeight:定義用戶所能夠調整的小部件尺寸的最大高度
targetCellWidth:定義設備主屏幕上的小部件默認寬度所占格數(即使不同型號的手機中也會占定義好的格數,但手機系統版本必須在 Android 12 及以上)
targetCellHeight:定義設備主屏幕上的小部件默認高度所占格數 -->
-
新的小部件控件。
Android 12
使用以下現有控件新增了對有狀態行為的支持:CheckBox
、Switch
、RadioButton
,上面這幾個控件大家應該非常熟悉了,但在Android 12
之前在小部件中想要使用的話也是不可能的。 -
Android 12
以上可以通過system_app_widget_background_radius
和system_app_widget_inner_radius
系統參數來設置微件圓角的半徑。