轉(zhuǎn)載請注明原創(chuàng)出處,謝謝!
- GitHub: @Ricco
ps:
因業(yè)務(wù)需求,需要給app增加Widget小組件,網(wǎng)上找了很多文檔,文檔零零散散,也比較老,所以總結(jié)一下
應(yīng)用 widget 概覽
小組件是什么
個(gè)人任務(wù)小組件就是,自定義在主屏幕上“一目了然”的視圖。讓用戶在主屏幕即可進(jìn)行訪問,查看重要數(shù)據(jù)和功能。豐富手機(jī)可玩性。
小組件類型
- 普通
- 列表(ListView)
- 其他
桌面小組件和負(fù)一屏小部件的區(qū)別
已小米手機(jī)為例,普通開發(fā)的小組件是不能放到負(fù)一屏的,在開發(fā)階段我們需要對小米進(jìn)行適配,然后將app上傳到小米小部件開發(fā)者平臺才能進(jìn)行調(diào)試,在正式環(huán)境,還需要去審核。
創(chuàng)建簡單的widget
聲明 AppWidgetProviderInfo XML
AppWidgetProviderInfo 對象定義了 widget 的基本特性。您可以使用單個(gè) <appwidget-provider> 元素在 XML 資源文件中定義 AppWidgetProviderInfo 對象,并將其保存在項(xiàng)目的 res/xml/ 文件夾中。
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="40dp"
android:minHeight="40dp"
android:targetCellWidth="1"
android:targetCellHeight="1"
android:maxResizeWidth="250dp"
android:maxResizeHeight="120dp"
android:updatePeriodMillis="86400000"
android:description="@string/example_appwidget_description"
android:previewLayout="@layout/example_appwidget_preview"
android:initialLayout="@layout/example_loading_appwidget"
android:configure="com.example.android.ExampleAppWidgetConfigurationActivity"
android:resizeMode="horizontal|vertical"
android:widgetCategory="home_screen"
android:widgetFeatures="reconfigurable|configuration_optional">
</appwidget-provider>
- minWidth 和 minHeight:指定 widget 的默認(rèn)大小,如果微件的最小寬度或高度的值與單元格的尺寸不匹配,則這些值會(huì)向上舍入到最接近的單元格大小。
- updatePeriodMillis:定義微件框架通過調(diào)用 onUpdate() 回調(diào)方法來從 AppWidgetProvider 請求更新的頻率。不能保證實(shí)際更新按此值正好準(zhǔn)時(shí)發(fā)生,google建議盡可能降低更新頻率(不超過每小時(shí)一次),以節(jié)省電池電量。不支持小于 30 分鐘的值,也就是最小設(shè)置為1800000
- initialLayout:指向用于定義微件布局的布局資源。
使用 AppWidgetProvider 類處理微件廣播
AppWidgetProvider 類會(huì)處理 widget 廣播,并響應(yīng) widget 生命周期事件來更新 widget。
在清單中聲明微件
首先,在應(yīng)用的 AndroidManifest.xml 文件中聲明 AppWidgetProvider 類
<receiver android:name="ExampleAppWidgetProvider"
android:lable="小組件"
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/example_appwidget_info" />
</receiver>
- android:lable:調(diào)起在 widget 選擇器中顯示時(shí),widget 需要具有唯一的名稱。
實(shí)現(xiàn) AppWidgetProvider 類
AppWidgetProvider 類擴(kuò)展了 BroadcastReceiver 作為一個(gè)輔助類來處理微件廣播。它僅接收與微件相關(guān)的事件廣播,例如當(dāng)更新、刪除、啟用和停用微件時(shí)發(fā)出的廣播。
- onUpdate:調(diào)用此方法可以按 AppWidgetProviderInfo 中的 updatePeriodMillis 屬性定義的時(shí)間間隔來更新微件。
- onAppWidgetOptionsChanged:當(dāng)首次放置微件時(shí)以及每當(dāng)調(diào)整微件的大小時(shí),系統(tǒng)都會(huì)調(diào)用此方法。
- onDeleted:每次從微件托管應(yīng)用中刪除微件時(shí),都會(huì)調(diào)用此方法。
- onEnabled:首次創(chuàng)建微件實(shí)例時(shí),系統(tǒng)會(huì)調(diào)用此方法。例如,如果用戶添加 widget 的兩個(gè)實(shí)例,只有首次添加時(shí)會(huì)調(diào)用此方法。
- onDisabled:從微件托管應(yīng)用中刪除了微件的最后一個(gè)實(shí)例時(shí),會(huì)調(diào)用此方法。
- onReceive:針對每個(gè)廣播調(diào)用此方法,并且是在上述各個(gè)回調(diào)方法之前調(diào)用。
public class TestWidget extends AppWidgetProvider {
/**
* 調(diào)用此方法可以按 AppWidgetProviderInfo 中的 updatePeriodMillis 屬性定義的時(shí)間間隔來更新微件。
*/
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
for (int appWidgetId : appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId);
}
}
/**
* 當(dāng)首次放置微件時(shí)以及每當(dāng)調(diào)整微件的大小時(shí),系統(tǒng)都會(huì)調(diào)用此方法。
*/
@Override
public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions) {
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions);
Bundle appWidgetOptions = appWidgetManager.getAppWidgetOptions(appWidgetId);
// 包含微件實(shí)例寬度的下限
Log.i("TAG", "onAppWidgetOptionsChanged: 包含微件實(shí)例寬度的下限 " + appWidgetOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH));
// 包含微件實(shí)例的高度下限
Log.i("TAG", "onAppWidgetOptionsChanged: 包含微件實(shí)例的高度下限 " + appWidgetOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT));
// 包含微件實(shí)例的寬度上限
Log.i("TAG", "onAppWidgetOptionsChanged: 包含微件實(shí)例的寬度上限 " + appWidgetOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH));
// 包含微件實(shí)例的高度上限
Log.i("TAG", "onAppWidgetOptionsChanged: 包含微件實(shí)例的高度上限 " + appWidgetOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT));
}
/**
* 每次從微件托管應(yīng)用中刪除微件時(shí),都會(huì)調(diào)用此方法。
*/
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
}
/**
* 首次創(chuàng)建微件實(shí)例時(shí),系統(tǒng)會(huì)調(diào)用此方法。例如,如果用戶添加 widget 的兩個(gè)實(shí)例,只有首次添加時(shí)會(huì)調(diào)用此方法。
*/
@Override
public void onEnabled(Context context) {
}
/**
* 從微件托管應(yīng)用中刪除了微件的最后一個(gè)實(shí)例時(shí),會(huì)調(diào)用此方法。
*/
@Override
public void onDisabled(Context context) {
}
/**
* 針對每個(gè)廣播調(diào)用此方法,并且是在上述各個(gè)回調(diào)方法之前調(diào)用。
* 要注意,實(shí)測?。?!會(huì)收到appwidgetId為0,從而導(dǎo)致組件更新失敗
*/
@Override
public void onReceive(Context context, Intent intent) {
super.onReceive(context, intent);
int appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
if (appWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID) {
updateAppWidget(context, AppWidgetManager.getInstance(context), appWidgetId);
} else {
// 要注意,實(shí)測!??!會(huì)收到appwidgetId為0,從而導(dǎo)致組件更新失敗
Log.i("TAG", "onReceive: 收到appwidgetId為0的廣播");
int[] appWidgetIds = AppWidgetManager.getInstance(context).getAppWidgetIds(new ComponentName(context, TestWidget.class));
for (int id : appWidgetIds) {
updateAppWidget(context, AppWidgetManager.getInstance(context), id);
}
}
}
private void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_test);
// ...
// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, views);
}
}
創(chuàng)建 widget 布局
RemoteViews 僅限于支持以下布局:
AdapterViewFlipper、FrameLayout、GridLayout、GridView、LinearLayout、ListView、RelativeLayout、StackView、ViewFlipper
以及以下小部件:
AnalogClock、Button、Chronometer、ImageButton、ImageView、ProgressBar、TextClock、TextView
自 API 31 起,以下小部件和布局也可以使用:
CheckBox、RadioButton、RadioGroup、Switch
這些類的子類不受支持。
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@android:id/background"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#FFFFFF"
android:gravity="center"
android:orientation="vertical">
<LinearLayout
android:id="@+id/ll_root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="@dimen/dp_80"
android:orientation="vertical"
android:padding="@dimen/dp_16">
</LinearLayout>
</FrameLayout>
- android:id="@android:id/background"加上這行代碼,手機(jī)會(huì)自動(dòng)圓角,但是還是建議你給background設(shè)置上圓角,解決適配部分手機(jī)、低版本沒有圓角的問題(三星)
- android:padding="@dimen/dp_16" 更具設(shè)計(jì)規(guī)范,要是是有邊距的
- android:minHeight="@dimen/dp_80"更具我結(jié)合三方app、并且多輪測試下來,強(qiáng)烈介意采用這種規(guī)則適配,要不然你在平板上看很完美,但是手機(jī)上會(huì)被拖拽拉升的特別大,很難看的。
應(yīng)用主動(dòng)刷新刷新
<intent-filter>里加一個(gè)action,例如“aaa.test.refresh”;
public static void refreshWidget(Context context) {
try {
Intent intent = new Intent(context, TestWidget.class);
intent.setAction("aaa.test.refresh");
int[] appWidgetIds = AppWidgetManager.getInstance(context).getAppWidgetIds(new ComponentName(context, TestWidget.class));
for (int appWidgetId : appWidgetIds) {
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
PendingIntent broadcast = PendingIntent.getBroadcast(context, appWidgetId, intent, PendingIntent.FLAG_ONE_SHOT);
try {
broadcast.send();
} catch (PendingIntent.CanceledException e) {
e.printStackTrace();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
應(yīng)用主動(dòng)添加小組件
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
ComponentName myProvider = new ComponentName(context, ExampleAppWidgetProvider.class);
if (appWidgetManager.isRequestPinAppWidgetSupported()) {
PendingIntent successCallback = PendingIntent.getBroadcast(
context, 0, new Intent(...), PendingIntent.FLAG_UPDATE_CURRENT);
appWidgetManager.requestPinAppWidget(myProvider, null, successCallback);
}
- 各大廠商的手機(jī)好像有兼容問題,不能保證都成功