一.簡(jiǎn)單上手
1. 配置并顯示widget
1.1 繼承AppWidgetProvider
自定義MyWidgetProvider繼承AppWidgetProvider,重寫相關(guān)方法。
class MyWidgetProvider : AppWidgetProvider() {
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
super.onUpdate(context, appWidgetManager, appWidgetIds)
//當(dāng)更新widget的時(shí)候會(huì)觸發(fā),添加的時(shí)候也會(huì)觸發(fā)
}
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
super.onDeleted(context, appWidgetIds)
//刪除widget的時(shí)候會(huì)觸發(fā)
}
override fun onDisabled(context: Context?) {
super.onDisabled(context)
//最后一個(gè)widget被刪除的時(shí)候觸發(fā)
}
override fun onEnabled(context: Context?) {
super.onEnabled(context)
//第一個(gè)widget被添加的時(shí)候觸發(fā)
}
override fun onAppWidgetOptionsChanged(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, newOptions: Bundle?) {
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)
//當(dāng)widget被第一次添加或者widget大小改變的時(shí)候觸發(fā)
}
override fun onReceive(context: Context, intent: Intent) {
super.onReceive(context, intent)
//處理方式和普通廣播一樣
}
}
AppWidgetProvider實(shí)質(zhì)上就是一個(gè)廣播,其中處理了相關(guān)的action并給出了回調(diào)方法。
1.2 配置appwidget-provider
找到res目錄下的xml目錄,若沒(méi)有xml目錄就新建一個(gè),然后新建一個(gè)文件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
添加到鎖屏頁(yè)面的widget布局 -
android:minWidth
最小寬度,通用計(jì)算方式: (N * 70)-30=寬度 -
android:minHeight
最小高度,通用計(jì)算方式: (N * 70)-30=高度,寬度和高度的格數(shù)按照google標(biāo)準(zhǔn)是這樣設(shè)置的,但是有很多廠家對(duì)Launcher重新定義,所以比如你設(shè)置的是5 * 1,但是某些手機(jī)上就會(huì)變成4 * 1。 -
android:previewImage
預(yù)覽圖 -
android:resizeMode
允許橫向縱向拉伸 -
android:updatePeriodMillis
刷新間隔,最小刷新間隔是半小時(shí),設(shè)置小于半小時(shí)也會(huì)按半小時(shí)算,且這里還有一點(diǎn)要注意,并不是每過(guò)半小時(shí)就一定會(huì)準(zhǔn)時(shí)刷新,受設(shè)備影響這個(gè)時(shí)間可能略有提前或延遲。還有當(dāng)手機(jī)息屏后可能會(huì)進(jìn)入休眠狀態(tài),在休眠狀態(tài)時(shí)不會(huì)自動(dòng)更新,當(dāng)設(shè)備解鎖從休眠狀態(tài)恢復(fù)時(shí)會(huì)立即刷新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中需要配置一個(gè)廣播接受者,其中固定的兩個(gè)配置參數(shù)
-
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
指定這個(gè)才能接收到widget更新。 -
android:name="android.appwidget.provider"
告訴系統(tǒng)這個(gè)廣播接受者是一個(gè)widget。
還可以在intent-filter里面配置自定義的action,用法就和普通廣播一樣。
現(xiàn)在已經(jīng)可以添加widget顯示啦,顯示的內(nèi)容為widget_layout.xml里的布局,沒(méi)錯(cuò)就是這么簡(jiǎn)單。
2. 更新widget
上面已經(jīng)顯示了widget,接下來(lái)就要給widget更新UI。
更新widget的UI是通過(guò)AppWidgetManager的updateAppWidget
方法實(shí)例來(lái)更新的,我們可以通過(guò)AppWidgetManager.getInstance(context)
來(lái)獲取實(shí)例。updateAppWidget有三個(gè)重載方法。
-
updateAppWidget(ComponentName provider, RemoteViews views)
指定要刷新widget的ComponentName和RemoteViews,通過(guò)AppWidgetManager.getInstance(context).updateAppWidget(componentName, remoteView)來(lái)刷新。舉個(gè)例子,我在桌面第一頁(yè)和第三頁(yè)都添加了同一個(gè)widget,現(xiàn)在若點(diǎn)擊其中一個(gè)的刷新按鈕兩個(gè)widget要同時(shí)都更新界面,這時(shí)就可以用這個(gè)方法。這個(gè)方法也是最常用來(lái)更新widget的方式,可以刷新添加到桌面的所有widget。一般來(lái)說(shuō),更新widget并不要求在AppWidgetProvider中進(jìn)行,因?yàn)锳ppWidgetProvider本質(zhì)上就是一個(gè)廣播,只要通過(guò)指定remoteView和ComponentName,可在任何包含上下文的環(huán)境下更新widget。 -
updateAppWidget(int[] appWidgetIds, RemoteViews views)
刷新部分指定的widget -
updateAppWidget(int appWidgetId, RemoteViews views)
刷新一個(gè)指定的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的點(diǎn)擊
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 -> {
//點(diǎn)擊事件
}
}
}
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"
}
}
二. 開(kāi)發(fā)widget中需要注意處理的點(diǎn)
1. 初始化問(wèn)題
當(dāng)widget刷新時(shí),如果應(yīng)用沒(méi)有處于開(kāi)啟狀態(tài)下,這時(shí)會(huì)創(chuàng)建APP進(jìn)程并初始化Application,之后回調(diào)widget的onUpdate方法。然而這里會(huì)有一個(gè)問(wèn)題,由于部分app為了性能優(yōu)化,將部分初始化操作移動(dòng)到了引導(dǎo)頁(yè)或Main頁(yè)面里了,這樣當(dāng)widget想使用某些功能時(shí),由于只創(chuàng)建了Application,在引導(dǎo)頁(yè)或main頁(yè)面里進(jìn)行初始化的那部分功能沒(méi)有進(jìn)行初始化,便會(huì)拋出各種異常。所以這里開(kāi)發(fā)的時(shí)候需要重點(diǎn)檢查一遍。
2. UI設(shè)置
- 當(dāng)添加widget出現(xiàn)小組件添加錯(cuò)誤、顯示失敗等,優(yōu)先檢查xml布局是否正確,尤其是不能包含自定義View等。
- 通過(guò)RemoteViews更新widget,可能每次更新都創(chuàng)建了一個(gè)RemoteViews對(duì)象,但是RemoteViews只是一個(gè)action集合,只代表你對(duì)systemServer端widget的操作,一旦通過(guò)RemoteViews更新過(guò)widget,有些步驟就可以不用重復(fù)設(shè)置(列如點(diǎn)擊事件)
- widget不支持動(dòng)畫(huà),如果一定要實(shí)現(xiàn)動(dòng)畫(huà),可以開(kāi)子線程循環(huán)刷新bitmap。
3. 網(wǎng)絡(luò)請(qǐng)求
盡量不要直接在AppWidgetProvider中進(jìn)行網(wǎng)絡(luò)請(qǐng)求,和耗時(shí)操作。
- 在AppWidgetProvider中進(jìn)行網(wǎng)絡(luò)請(qǐng)求,當(dāng)未開(kāi)啟APP情況下,會(huì)請(qǐng)求失敗拋出SocketTimeoutException異常。這一點(diǎn)很重要,很多系統(tǒng)都會(huì)限制在后臺(tái)程序里靜態(tài)廣播的網(wǎng)絡(luò)請(qǐng)求。如果有需要,請(qǐng)開(kāi)啟Service,在Service中進(jìn)行網(wǎng)絡(luò)請(qǐng)求。
- 由于AppWidgetProvider優(yōu)先級(jí)很低,代表當(dāng)前進(jìn)程容易被系統(tǒng)回收,所以盡量不要再AppWidgetProvider中進(jìn)行耗時(shí)操作,否則可能會(huì)出現(xiàn)AppWidgetProvider中的任務(wù)未執(zhí)行完進(jìn)程就已經(jīng)被系統(tǒng)回收。建議耗時(shí)操作開(kāi)啟Service執(zhí)行。
4. 定時(shí)任務(wù)
很大一部分app都有定時(shí)刷新widget的需求,而系統(tǒng)的刷新間隔要求大于等于30分鐘,這顯然是滿足不了需求。這里有兩種方案。
- 單獨(dú)進(jìn)程的前臺(tái)service
- 通過(guò)JobScheduler
如果對(duì)實(shí)時(shí)性要求不是太高,可以考慮使用JobScheduler
5. 關(guān)于Service通知問(wèn)題
我們知道在Android8.0后開(kāi)啟Service需要指定為前臺(tái)通知,這樣就會(huì)有一個(gè)通知欄效果。如果在widget中想開(kāi)啟Service進(jìn)行網(wǎng)絡(luò)請(qǐng)求,而又不想出通知,可以使用bindService方式。
bindService是Context的方法,網(wǎng)上大部分文章都拿Activity做例子,導(dǎo)致很多人不知道bindService其實(shí)在Application等Context的子類中都能使用。