Android Widget的使用和需要注意的問(wèn)題

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

  1. 單獨(dú)進(jìn)程的前臺(tái)service
  2. 通過(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的子類中都能使用。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • RemoteViews詳細(xì)解釋 原載于:RemoteViews詳細(xì)解釋 說(shuō)明 想要完全的理解RetmoteView...
    simaxiaochen閱讀 3,609評(píng)論 0 6
  • Read The Fucking Source Code 引言 Android AppWidget相對(duì)偏冷門。 開(kāi)...
    科技猿人閱讀 11,058評(píng)論 3 18
  • 什么是AppWidget?AppWidget就是我們平常在桌面上見(jiàn)到的那種一個(gè)個(gè)的小窗口,利用這個(gè)小窗口可以給用戶...
    MrMagicWang閱讀 953評(píng)論 1 1
  • 因?yàn)楣卷?xiàng)目需求,需要實(shí)現(xiàn)widget 來(lái)刷新數(shù)據(jù),稀里糊涂的做,然后踩了很多坑,寫個(gè)文章來(lái)總結(jié)下widget一些...
    Android猿來(lái)如此閱讀 7,244評(píng)論 0 5
  • 本文主要從系統(tǒng)層怎樣加載一個(gè)widget分析,不包含怎樣創(chuàng)建一個(gè)含有widget的app。所謂widget,梗概流...
    福爾摩斯春卷閱讀 5,245評(píng)論 3 8