Android Widget的使用和需要注意的問題

一.簡單上手

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分鐘,這顯然是滿足不了需求。這里有兩種方案。

  1. 單獨進程的前臺service
  2. 通過JobScheduler
    如果對實時性要求不是太高,可以考慮使用JobScheduler

5. 關于Service通知問題

我們知道在Android8.0后開啟Service需要指定為前臺通知,這樣就會有一個通知欄效果。如果在widget中想開啟Service進行網絡請求,而又不想出通知,可以使用bindService方式。
bindService是Context的方法,網上大部分文章都拿Activity做例子,導致很多人不知道bindService其實在Application等Context的子類中都能使用。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容