關于Android桌面小組件相關的開發,涉及到的一些點

你可能用過一些 Android APP 的小組件,比如:

  • 支付寶的小組件:之前疫情期間添加了對應小組件卡片在桌面,可點擊小卡片上的查看健康碼的按鈕,可一鍵打開健康碼。
  • 音樂類 APP的小組件:添加對應對應小組件后, 可在 APP 的主屏幕中輕松看到當前播放歌曲的相關信息:歌曲封面、歌曲名、歌手名稱、所屬專輯名稱等。
  • 時鐘類 APP 的小組件:可添加各種樣式的時鐘小組件在屏幕,裝飾你的主屏幕,在你喜歡的小組件上來查看時間。
  • 天氣類 APP 的小組件:可在主屏幕直接看到天氣相關信息,不用再打開天氣的 APP

如果你所開發的項目的產品經理并沒有相關開發需求,可能很多 Android 開發并沒有接觸過相關桌面主屏幕的相關小組件開發,這篇文章主要介紹下相關開發的知識點。

AppWidgetProvider:
幫助實現 AppWidget 提供程序的便利類。 你可以用 AppWidgetProvider 做的事情,你也可以用一個普通的 BroadcastReceiver 做。 AppWidgetProvider 只是從 onReceive(Context,Intent) 中接收到的 Intent 中解析出相關字段,并使用接收到的 extra 調用 hook 方法。
擴展此類并覆蓋 onUpdateonDeletedonEnabledonDisabled 方法中的一個或多個以實現您自己的 AppWidget 功能。

public class WidgetDemoProvider extends AppWidgetProvider {

    /**
     * 當 Widget 第一次被添加時調用,例如用戶添加了兩個你的 Widget,那么只有在添加第一個 Widget 時該  
     * 方法會被調用。所以該方法比較適合執行你所有 Widgets 只需進行一次的操作。對用廣播的 Action 為 
     * ACTION_APPWIDGET_ENABLE。
     */
    @Override
    public void onEnabled(Context context) {
        super.onEnabled(context);
    }

    /**
     * 與 onEnabled 恰好相反,當你的最后一個 Widget 被刪除時調用該方法,所以這里用來清理之前在 onEnabled() 中進行的操作。
     * 當最后一個該類型的小部件從桌面移除時調用,對應的廣播的 Action 為 ACTION_APPWIDGET_DISABLED。
     *
     */
    public void onDisabled(Context context) {
        super.onDisabled(context);
    }

    /**
     * 當 Widget 第一次被添加或者大小發生變化時調用該方法,可以在此控制 Widget 元素的顯示和隱藏。
     * 當小部件布局發生更改的時候調用。對應廣播的 Action 為 ACTION_APPWIDGET_OPTIONS_CHANGED。
     *
     * @param appWidgetManager 您可以調用 AppWidgetManager.updateAppWidget 的 AppWidgetManager 對象。
     * @param appWidgetId 大小改變的widget的appWidgetId。
     * @param newOptions
     */
    @Override
    public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions) {
        super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions);
    }

    /**
     * 當小部件從備份中還原,或者恢復設置的時候,會調用,實際用的比較少。對應廣播的 Action 為 ACTION_APPWIDGET_RESTORED。
     * @param oldWidgetIds
     * @param newWidgetIds
     */
    @Override
    public void onRestored(Context context, int[] oldWidgetIds, int[] newWidgetIds) {
        super.onRestored(context, oldWidgetIds, newWidgetIds);
    }

    /**
     * AppWidget 更新事件
     *
     * 小部件被添加時或者每次小部件更新時都會調用一次該方法,配置文件中配置小部件的更新周期 updatePeriodMillis,每次更新都會調用。對應廣播 Action 為:ACTION_APPWIDGET_UPDATE 和 ACTION_APPWIDGET_RESTORED
     *
     * @param appWidgetManager 更新 AppWidget 狀態; 獲取有關已安裝 AppWidget 提供程序和其他 AppWidget 相關狀態的信息。
     * @param appWidgetIds 需要更新的 appWidgetIds。 請注意,這可能是此提供程序的所有 AppWidget 實例,也可能只是其中的一個子集。
     */
    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager,
            int[] appWidgetIds) {
    }

    /**
     * 當 Widget 被刪除時調用該方法。
     *
     * 每刪除一個小部件就調用一次。對應的廣播的 Action 為: ACTION_APPWIDGET_DELETED 。
     *
     * @param appWidgetIds 已從其組件集群中刪除的 appWidgetIds。
     */
    @Override
    public void onDeleted(Context context, int[] appWidgetIds) {
        super.onDeleted(context, appWidgetIds);
    }

    /**
     *  接收廣播的回調函數
     * onReceive() 中處理的是 Widget 相關的廣播事件,然后分發到各個回調函數中onUpdate(), onDeleted(), onEnabled(), onDisabled, onAppWidgetOptionsChanged()。
     *
     * @param context
     * @param intent
     */
    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (AppWidgetManager.ACTION_APPWIDGET_ENABLED.equals(action)) {
            this.onEnabled(context);
        }
        else if (AppWidgetManager.ACTION_APPWIDGET_DISABLED.equals(action)) {
            this.onDisabled(context);
        }
        ...省略
        super.onReceive(context, intent);
    }
}

根據自己的業務,寫好 WidgetDemoProvider 的自定義代碼后,需要在 AndroidManifest.xml 里注冊一下:

      <receiver android:name=".widget.WidgetDemoProvide"
            android:exported="true">
            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
                <action android:name="xxx..."/>  <!-- 此處可以添加自己需要的 -->
            </intent-filter>
            <meta-data android:name="android.appwidget.provider"
                android:resource="@xml/appwidget_demo" /> <!-- 此處可以添加自己需要的給用戶提前預覽的自定義小組件布局 -->
        </receiver>

<receiver> 元素需要 android:name 屬性,該屬性指定小部件使用的 AppWidgetProviderAppWidgetProvider 的父類就是 BroadcastReceiver)。
<intent-filter> 中的 <action> 元素指定小部件接受 ACTION_APPWIDGET_UPDATE 廣播。這是必須明確聲明的唯一一項廣播,用以接收小部件的增刪改等信息。
<meta-data> 元素指定小部件的資源,并且需要以下屬性:
android:name - 指定元數據名稱。必須使用 android.appwidget.provider 將數據標識為 AppWidgetProviderInfo 描述符。
android:resource - 指定 AppWidgetProviderInfo 資源位置。

appwidget_demo.xml 的示例代碼:

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="180dp"
    android:minHeight="180dp"
    android:updatePeriodMillis="1800000"
    android:previewImage="@mipmap/app_widget_preview_3x3" 
    android:initialLayout="@layout/app_widget_preview_layout_3_3"
    android:resizeMode="horizontal|vertical"> 
</appwidget-provider>

minWidthminHeight :指定小部件默認情況下占用的最小空間。
注意:為使小部件能夠在設備間移植,小部件的最小大小不得超過 4 x 4 單元格。
minResizeWidthminResizeHeight:指定小部件的絕對最小大小。
updatePeriodMillis:定義小部件框架通過調用 onUpdate() 回調方法來從 AppWidgetProvider 請求更新的頻率應該是多大。
initialLayout:指向用于定義小部件布局的布局資源。
configure:定義要在用戶添加小部件時啟動以便用戶配置小部件屬性的 Activity。
previewImage:指定預覽來描繪小部件經過配置后是什么樣子的,用戶在選擇小部件時會看到該預覽。
autoAdvanceViewId :指定應由小部件的托管應用自動跳轉的小部件子視圖的視圖 ID。
resizeMode :指定可以按什么規則來調整微件的大小,可選值為“horizontal|vertical”,一般默認設置橫豎都可以進行調整。
minResizeHeight :指定可將微件大小調整到的最小高度。
minResizeWidth:指定可將微件大小調整到的最小寬度。
widgetCategory:聲明小部件是否可以顯示在主屏幕 (home_screen) 或鎖定屏幕 (keyguard) 上。只有低于 5.0 的 Android 版本才支持鎖定屏幕微件。對于 Android 5.0 及更高版本,只有 home_screen 有效,所以現在將這個值寫為 home_screen 即可。

如果在自己的相關業務代碼里,比如 activity 里如何觸發 WidgetDemoProvider 相關數據以及頁面更新。可以看以下示例代碼:

  private void sendNotify(){
    try {
      Class javaClass = Class.forName("xxx.WidgetDemoProvider");
      final Intent intent = new Intent(this, javaClass);
      intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
      int[] ids = AppWidgetManager.getInstance(GlobeContext.context).getAppWidgetIds(new ComponentName(GlobeContext.context, javaClass));
      intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids);
      sendBroadcast(intent);

    } catch (ClassNotFoundException e) {
      e.printStackTrace();
    }
  }

一些開發注意事項:

  • 小組件的寬高是可以支持用戶自行調整的,只需簡單的設置最低寬高,但是可調整的最小粒度是根據手機的 icon 為標準。小組件數量無限制,用戶對小組件的大小和具體功能喜好都不太一樣,所以解決方案就簡單粗暴一點,你能想到的適配尺寸,每種尺寸搞一個,用戶自己選擇合適的尺寸就好。大、中、小、大中、中小、微小、超大等尺寸,可以全部做一遍。
android:resizeMode="horizontal|vertical"
widget 可以被拉伸的方向。horizontal表示可以水平拉伸,vertical表示可以豎直拉伸
  • 更新時間分為主動更新和定時更新。
    主動更新:即在 APP 中可以動態更新這個桌面小組件,這種情況更新沒有時間限制。
    定時更新:小組件需要展示的數據可能已經發生了變化,但是 APP 已經被系統殺死了,無法主動更新數據,就會導致小組件展示的數據可能是已過期的或者是舊的,這時候就可以用到小組件的定時更新功能,但是這個定時更新有一個限制,基于省電邏輯,最快的更新周期為 30 分鐘。(如果是在 onUpdate 方法中寫個定時器定時更新,這樣是不行的,會被系統給殺死,殺死之后小組件不會消失,而是一直顯示最后一次更新時候的狀態,直到下一次更新數據,類似于電子水墨屏的邏輯。)
  • 一般點擊整個小組件,我們直接調起 APP。點擊跳轉頁面需要用到 PendingIntent,這玩意的 Flag 有很多種模式,具體可以查看文章底部的參考文檔,坑就坑在這個 Flag,31 之后的系統有改動,會報錯,所以 31 的系統需要用 PendingIntent.FLAG_IMMUTABLE,具體看代碼。
    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (AppWidgetManager.ACTION_APPWIDGET_ENABLED.equals(action)) {
            this.onEnabled(context);
        }
        else if (AppWidgetManager.ACTION_APPWIDGET_DISABLED.equals(action)) {
            this.onDisabled(context);
        }
        if (intent.hasCategory(Intent.CATEGORY_ALTERNATIVE)) {
            Uri data = intent.getData();
            int buttonId = Integer.parseInt(data.getSchemeSpecificPart());
            switch (buttonId) {
                case R.id.widget_layout:
                   Intent intent = new Intent(context, RemotePlayerActivity.class);
                   intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                   context.startActivity(goIntent);
                   RemoteViews remoteView = new RemoteViews(context.getPackageName(),R.layout.app_widget_layout);
                   //將按鈕與點擊事件綁定
                   remoteView.setOnClickPendingIntent(R.id.widget_layout,getPendingIntent(context, R.id.widget_layout));
                   break;
            }
        }
        super.onReceive(context, intent);
    }

    private PendingIntent getPendingIntent(Context context, int buttonId) {
        Intent intent = new Intent();
        intent.setClass(context, WidgetDemoProvider.class);
        intent.addCategory(Intent.CATEGORY_ALTERNATIVE);
        intent.setData(Uri.parse("harvic:" + buttonId));
        PendingIntent pendingIntent;
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
            pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE);
        } else {
            pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0);
        }
        return pendingIntent;
    }

  • 通過 PendingIntent 就可以直接調起 APP 的相關頁面,不過這里也有坑,假設你 APP 的啟動頁面是 MainActivity 頁面,點擊小組件你就讓它跳轉到 MainActivity 頁面走正常的 APP 啟動流程,就等同于是點擊小組件就能啟動 APP,哪怕 APP 被殺死了。坑就坑在于,通過這種方式打開的 APP,它不會走 Application 類,也就是你如果是在 Application 中初始化了某些東西,但是 APP 已經被系統殺死了,這時候你再點擊小組件啟動 APP,這時就會發現好多組件用不了,因為沒初始化。
    所以相對省事的做法就是把 Application 的所有需要初始化的東西都放 MainActivity 里面初始化了,目前為了工信部隱私相關合規,應該很多 APP 的初始化代碼應該已經從 Application 放到用戶點同意隱私協議彈框后再去初始化了。

RemoteViews,從字面意思理解為它是一個遠程視圖。是一種遠程的 View,它在其它進程中顯示,卻可以在另一個進程中更新。RemoteViewsAndroid 中的使用場景主要有:自定義通知欄和桌面小部件。

RemoteViewsService,是管理 RemoteViews 的服務。一般,當 AppWidget 中包含 GridViewListViewStackView 等集合視圖時,才需要使用 RemoteViewsService 來進行更新、管理。RemoteViewsService 更新集合視圖的一般步驟是:通過 setRemoteAdapter() 方法來設置 RemoteViews對應 RemoteViewsService
之后在 RemoteViewsService 中,實現 RemoteViewsFactory 接口。然后,在 RemoteViewsFactory 接口中對集合視圖的各個子項進行設置,例如 ListView 中的每一 Item

RemoteViewsFactory 通過 RemoteViewsService 中的介紹,我們知道 RemoteViewsService 是通過
RemoteViewsFactory 來具體管理 layout 中集合視圖的,RemoteViewsFactoryRemoteViewsService 中的一個內部接口。RemoteViewsFactory 提供了一系列的方法管理集合視圖中的每一項。
例如:
public RemoteViews getViewAt(int position):
通過getViewAt()來獲取“集合視圖”中的第position項的視圖,視圖是以RemoteViews的對象返回的。
public int getCount() :
通過getCount()來獲取“集合視圖”中所有子項的總數。

  • 用戶可重新設置原有 widget。Android 12 之前,重新設置 widget 意味著用戶必須刪除現有 widget,然后使用新配置重新添加。Android 12 在多個方面改進了 widget 的配置方式,從而幫助用戶采用更簡單的方式對 widget 進行個性化配置。可重組的 widget 允許用戶對 widget 進行自定義設置。在 Android 12 中,用戶將無需通過刪除和重新添加 widget 來調整這些原有設定。
    要使用這一功能,您需在 appwidget-provider 中把 widgetFeatures 屬性設置為 reconfigurable
    當用戶配置該 widget 時,新的配置會被記錄在 ListWidgetConfigureActivity 中。
    如果您的 widget 依賴默認設置,在 Android 12 中您可跳過初始化操作,通過默認配置來設置 widget
<appwidget-provider
   android:configure="com.example.android.appwidget.ListWidgetConfigureActivity"
   android:widgetFeatures="reconfigurable|configuration_optional"
   ... />
  • Android 12 中 Widget 的尺寸限制改進。除了現有的 minWidthminHeighminResizeWidth 以及 minResizeHeight 以外,Android 12 還添加了新的 appwidget-provider 屬性。您可以使用新的 maxResizeWidthmaxResizeHeight 屬性,來定義用戶所能夠調整的 widget 尺寸的最大高度和寬度。新的 targetCellWidthtargetCellHeight 屬性能夠定義設備主屏幕上的 widget 默認尺寸。如果之前有 targetCellWidthtargetCellHeight 屬性的話,小部件也不至于像現在這么亂而導致用戶不想使用。
<appwidget-provider
   android:maxResizeWidth="240dp"
   android:maxResizeHeight="180dp"
   android:minWidth="180dp"
   android:minHeight="110dp"
   android:minResizeWidth="180dp"
   android:minResizeHeight="110dp"
   android:targetCellWidth="3"
   android:targetCellHeight="2"
   ... />
<!-- maxResizeWidth:定義用戶所能夠調整的小部件尺寸的最大寬度
maxResizeHeight:定義用戶所能夠調整的小部件尺寸的最大高度
targetCellWidth:定義設備主屏幕上的小部件默認寬度所占格數(即使不同型號的手機中也會占定義好的格數,但手機系統版本必須在 Android 12 及以上)
targetCellHeight:定義設備主屏幕上的小部件默認高度所占格數 -->

  • 新的小部件控件。Android 12 使用以下現有控件新增了對有狀態行為的支持:CheckBoxSwitch
    RadioButton,上面這幾個控件大家應該非常熟悉了,但在 Android 12 之前在小部件中想要使用的話也是不可能的。
  • Android 12 以上可以通過 system_app_widget_background_radiussystem_app_widget_inner_radius 系統參數來設置微件圓角的半徑。
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,797評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,179評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,628評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,642評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,444評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,948評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,040評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,185評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,717評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,794評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,418評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,414評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,750評論 2 370

推薦閱讀更多精彩內容