一.簡單上手
1. 配置并顯示widget
1.1 繼承AppWidgetProvider
自定義MyWidgetProvider繼承AppWidgetProvider,重寫相關方法。
class MyWidgetProvider : AppWidgetProvider() {
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
super.onUpdate(context, appWidgetManager, appWidgetIds)
//當更新widget的時候會觸發,添加的時候也會觸發
}
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
super.onDeleted(context, appWidgetIds)
//刪除widget的時候會觸發
}
override fun onDisabled(context: Context?) {
super.onDisabled(context)
//最后一個widget被刪除的時候觸發
}
override fun onEnabled(context: Context?) {
super.onEnabled(context)
//第一個widget被添加的時候觸發
}
override fun onAppWidgetOptionsChanged(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, newOptions: Bundle?) {
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)
//當widget被第一次添加或者widget大小改變的時候觸發
}
override fun onReceive(context: Context, intent: Intent) {
super.onReceive(context, intent)
//處理方式和普通廣播一樣
}
}
AppWidgetProvider實質上就是一個廣播,其中處理了相關的action并給出了回調方法。
1.2 配置appwidget-provider
找到res目錄下的xml目錄,若沒有xml目錄就新建一個,然后新建一個文件widget_info.xml
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialKeyguardLayout="@layout/widget_layout"
android:initialLayout="@layout/widget_layout"
android:minWidth="@dimen/dp_320"
android:minHeight="@dimen/dp_110"
android:previewImage="@mipmap/img_widget_preview"
android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="5"
android:widgetCategory="home_screen">
</appwidget-provider>
這里介紹下常用屬性
-
android:initialLayout
添加到桌面的widget布局 -
android:initialKeyguardLayout
添加到鎖屏頁面的widget布局 -
android:minWidth
最小寬度,通用計算方式: (N * 70)-30=寬度 -
android:minHeight
最小高度,通用計算方式: (N * 70)-30=高度,寬度和高度的格數按照google標準是這樣設置的,但是有很多廠家對Launcher重新定義,所以比如你設置的是5 * 1,但是某些手機上就會變成4 * 1。 -
android:previewImage
預覽圖 -
android:resizeMode
允許橫向縱向拉伸 -
android:updatePeriodMillis
刷新間隔,最小刷新間隔是半小時,設置小于半小時也會按半小時算,且這里還有一點要注意,并不是每過半小時就一定會準時刷新,受設備影響這個時間可能略有提前或延遲。還有當手機息屏后可能會進入休眠狀態,在休眠狀態時不會自動更新,當設備解鎖從休眠狀態恢復時會立即刷新widget。
1.3 配置AndroidManifest.xml
<receiver
android:name=".MyWidgetProvider"
android:label="@string/app_widget_string">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_info" />
</receiver>
在AndroidManifest.xml中需要配置一個廣播接受者,其中固定的兩個配置參數
-
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
指定這個才能接收到widget更新。 -
android:name="android.appwidget.provider"
告訴系統這個廣播接受者是一個widget。
還可以在intent-filter里面配置自定義的action,用法就和普通廣播一樣。
現在已經可以添加widget顯示啦,顯示的內容為widget_layout.xml里的布局,沒錯就是這么簡單。
2. 更新widget
上面已經顯示了widget,接下來就要給widget更新UI。
更新widget的UI是通過AppWidgetManager的updateAppWidget
方法實例來更新的,我們可以通過AppWidgetManager.getInstance(context)
來獲取實例。updateAppWidget有三個重載方法。
-
updateAppWidget(ComponentName provider, RemoteViews views)
指定要刷新widget的ComponentName和RemoteViews,通過AppWidgetManager.getInstance(context).updateAppWidget(componentName, remoteView)來刷新。舉個例子,我在桌面第一頁和第三頁都添加了同一個widget,現在若點擊其中一個的刷新按鈕兩個widget要同時都更新界面,這時就可以用這個方法。這個方法也是最常用來更新widget的方式,可以刷新添加到桌面的所有widget。一般來說,更新widget并不要求在AppWidgetProvider中進行,因為AppWidgetProvider本質上就是一個廣播,只要通過指定remoteView和ComponentName,可在任何包含上下文的環境下更新widget。 -
updateAppWidget(int[] appWidgetIds, RemoteViews views)
刷新部分指定的widget -
updateAppWidget(int appWidgetId, RemoteViews views)
刷新一個指定的widget
class MyWidgetProvider : AppWidgetProvider() {
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
super.onUpdate(context, appWidgetManager, appWidgetIds)
for (appWidgetId in appWidgetIds) {
appWidgetManager.updateAppWidget(appWidgetId, remoteView)
}
//uploadWidget(context)
}
private fun uploadWidget(context: Context) {
val remoteView = RemoteViews(context.packageName, R.layout.widget_layout)
val componentName = ComponentName(context, javaClass)
AppWidgetManager.getInstance(context).updateAppWidget(componentName, remoteView)
}
}
上面代碼兩種方式都能刷新全部widget
3. widget的點擊
package com.example.kotlintest.widget
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.widget.RemoteViews
import com.example.kotlintest.R
class MyWidgetProvider : AppWidgetProvider() {
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
super.onUpdate(context, appWidgetManager, appWidgetIds)
val intent = Intent(REFRESH_CLICK).apply {
component = ComponentName(context, MyWidgetProvider::class.java)
}
val pendingIntent = PendingIntent.getBroadcast(
context,
R.id.tv_refresh,
intent,
PendingIntent.FLAG_UPDATE_CURRENT
)
val remoteView = RemoteViews(context.packageName, R.layout.widget_layout)
remoteView.setOnClickPendingIntent(R.id.tv_refresh, pendingIntent)
uploadWidget(context,remoteView)
}
override fun onReceive(context: Context, intent: Intent) {
super.onReceive(context, intent)
when (intent.action) {
REFRESH_CLICK -> {
//點擊事件
}
}
}
private fun uploadWidget(context: Context,remoteView: RemoteViews) {
val componentName = ComponentName(context, javaClass)
AppWidgetManager.getInstance(context).updateAppWidget(componentName, remoteView)
}
companion object {
const val REFRESH_CLICK = "com.example.kotlintest.action.CLICK_REFRESH"
}
}
二. 開發widget中需要注意處理的點
1. 初始化問題
當widget刷新時,如果應用沒有處于開啟狀態下,這時會創建APP進程并初始化Application,之后回調widget的onUpdate方法。然而這里會有一個問題,由于部分app為了性能優化,將部分初始化操作移動到了引導頁或Main頁面里了,這樣當widget想使用某些功能時,由于只創建了Application,在引導頁或main頁面里進行初始化的那部分功能沒有進行初始化,便會拋出各種異常。所以這里開發的時候需要重點檢查一遍。
2. UI設置
- 當添加widget出現小組件添加錯誤、顯示失敗等,優先檢查xml布局是否正確,尤其是不能包含自定義View等。
- 通過RemoteViews更新widget,可能每次更新都創建了一個RemoteViews對象,但是RemoteViews只是一個action集合,只代表你對systemServer端widget的操作,一旦通過RemoteViews更新過widget,有些步驟就可以不用重復設置(列如點擊事件)
- widget不支持動畫,如果一定要實現動畫,可以開子線程循環刷新bitmap。
3. 網絡請求
盡量不要直接在AppWidgetProvider中進行網絡請求,和耗時操作。
- 在AppWidgetProvider中進行網絡請求,當未開啟APP情況下,會請求失敗拋出SocketTimeoutException異常。這一點很重要,很多系統都會限制在后臺程序里靜態廣播的網絡請求。如果有需要,請開啟Service,在Service中進行網絡請求。
- 由于AppWidgetProvider優先級很低,代表當前進程容易被系統回收,所以盡量不要再AppWidgetProvider中進行耗時操作,否則可能會出現AppWidgetProvider中的任務未執行完進程就已經被系統回收。建議耗時操作開啟Service執行。
4. 定時任務
很大一部分app都有定時刷新widget的需求,而系統的刷新間隔要求大于等于30分鐘,這顯然是滿足不了需求。這里有兩種方案。
- 單獨進程的前臺service
- 通過JobScheduler
如果對實時性要求不是太高,可以考慮使用JobScheduler
5. 關于Service通知問題
我們知道在Android8.0后開啟Service需要指定為前臺通知,這樣就會有一個通知欄效果。如果在widget中想開啟Service進行網絡請求,而又不想出通知,可以使用bindService方式。
bindService是Context的方法,網上大部分文章都拿Activity做例子,導致很多人不知道bindService其實在Application等Context的子類中都能使用。