Android 開發(fā)之 App Widget 詳解

簡介

App Widget:應(yīng)用程序窗口小部件,微型的應(yīng)用程序視圖,它可以被嵌入到其它應(yīng)用程序中礼仗,比如桌面祸憋,并接收周期性的更新会宪。你可以通過一個 App Widget Provider 來發(fā)布一個 Widget,比如時鐘蚯窥、天氣掸鹅、音樂播放器等等±乖可以容納 Widget 的應(yīng)用叫做 App Widget Host巍沙,詳細(xì)參考App Widgets| Android Developers

App Widget Provider是Android中提供的用于實現(xiàn)桌面小工具的類荷鼠,其本質(zhì)是一個廣播句携,即BroadcastReceiver。

創(chuàng)建一個 App Widget 的主要步驟

  1. 在 AndroidManifest 中聲明 App Widget
  2. 在 xml 目錄定義 App Widget 的初始化 xml 文件
  3. 實現(xiàn) Widget 具體布局的 Layout xml允乐。
  4. 繼承 AppWidgetProvider 類矮嫉,實現(xiàn)具體的 Widget 業(yè)務(wù)邏輯削咆。

在 AndroidManifest 中聲明 App Widget

 <!-- 聲明widget對應(yīng)的AppWidgetProvider -->
        <!--android:name屬性聲明的就是 Widget 所用的 AppWidgetProvider 類-->
        <receiver android:name=".common.ExampleAppWidgetProvider">
            <intent-filter>
                <!--所有的窗口小部件都接收android.appwidget.action.APPWIDGET_UPDATE 動作的廣播,
                該廣播根據(jù)android:updatePeriodMillis設(shè)定的間隔時間發(fā)出廣播蠢笋,用于定時更新桌面上的所有窗口小部件拨齐。-->
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
                <!--定義一個自定義的動作廣播,可以通過在該廣播接收器中注冊自定義的動作以使窗口小部件接收自定義的廣播昨寞。-->
                <action android:name="com.skywang.widget.UPDATE_ALL"/>
            </intent-filter>
            <!--聲明了 Widget 的 AppWidgetProviderInfo 對應(yīng)的資源 xml 的位置瞻惋,用的是 xml 目錄下的 example_appwidget_info.xml。-->
            <meta-data
                    android:name="android.appwidget.provider"
                    android:resource="@xml/example_appwidget_info"/>
        </receiver>

在 xml 目錄定義 App Widget 的初始化 xml 文件

<appwidget-provider
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:minWidth="300dp"
        android:minHeight="72dp"
        android:updatePeriodMillis="86400000"
        android:previewImage="@drawable/ic_launcher"
        android:initialLayout="@layout/example_appwidget"
        android:resizeMode="horizontal|vertical"
        android:widgetCategory="home_screen|keyguard">

    <!--
    android:minWidth : 最小寬度
    android:minHeight : 最小高度
    android:updatePeriodMillis : 更新widget的時間間隔(ms)援岩,"86400000"為1個小時
    android:previewImage : 預(yù)覽圖片
    android:initialLayout : 加載到桌面時對應(yīng)的布局文件
    android:resizeMode : widget可以被拉伸的方向歼狼。horizontal表示可以水平拉伸,vertical表示可以豎直拉伸
    android:widgetCategory : widget可以被顯示的位置窄俏。home_screen表示可以將widget添加到桌面蹂匹,keyguard表示widget可以被添加到鎖屏界面。
    android:initialKeyguardLayout : 加載到鎖屏界面時對應(yīng)的布局文件
     -->
</appwidget-provider>

minWidth & minHeight:定義了 Widget 的最小寬高凹蜈,當(dāng) minWidth 和 minHeight 不是桌面 cell 的整數(shù)倍時限寞,Widget 的寬高會被闊至與其最接近的 cells 大小。Google 官方給出了一個大致估算 minWidth & minHeight 的公式仰坦,根據(jù) Widget 所占的 cell 數(shù)量來計算寬高:70 × n ? 30履植,n 是所占的 cell 數(shù)量。
updatePeriodMillis:定義了 Widget 的刷新頻率悄晃,也就是 App Widget Framework 多久請求一次 AppWidgetProvider 的 onUpdate() 回調(diào)函數(shù)玫霎。該時間間隔并不保證精確,出于節(jié)約用戶電量的考慮妈橄,Android 系統(tǒng)默認(rèn)最小更新周期是 30 分鐘庶近,也就是說:如果您的程序需要實時更新數(shù)據(jù),設(shè)置這個更新周期是 2 秒眷蚓,那么您的程序是不會每隔 2 秒就收到更新通知的鼻种,而是要等到 30 分鐘以上才可以,要想實時的更新 Widget沙热,一般可以采用 Service 和 AlarmManager 對 Widget 進(jìn)行更新叉钥。
previewImage:當(dāng)用戶選擇添加 Widget 時的預(yù)覽圖片。如果該屬性沒有定義篙贸,則展示 application 的 launcher icon投队。該屬性是在 3.0 以后引入的。
initialLayout:Widget 的布局 Layout 文件爵川。
configure:定義了用戶在添加 Widget 時彈出的配置頁面的 Activity敷鸦,用戶可以在此進(jìn)行 Widget 的一些配置,該 Activity 是可選的,如果不需要可以不進(jìn)行聲明轧膘。
resizeMode:Widget 在水平和垂直方向是否可以調(diào)整大小钞螟,值可以為:horizontal(水平方向可以調(diào)整大小)谎碍,vertical(垂直方向可以調(diào)整大辛郾酢),none(不可以調(diào)整大畜〉怼)拯啦,也可以 horizontal|vertical 組合表示水平和垂直方向均可以調(diào)整大小。
widgetCategory:表示 Widget 可以顯示的位置熔任,包括 home_screen(桌面)褒链,keyguard(鎖屏),keyguard 屬性需要 5.0 或以上 Android 版本才可以疑苔。
其它更多詳細(xì)屬性可以參考 AppWidgetProviderInfo甫匹。

繼承 AppWidgetProvider 類

AppWidgetProvider 繼承自 BroadcastReceiver,內(nèi)部邏輯非常簡單惦费,就是在 onReceive() 中處理 Widget 相關(guān)的廣播事件(ACTION_APPWIDGET_UPDATE, ACTION_APPWIDGET_DELETED, ACTION_APPWIDGET_ENABLED, ACTION_APPWIDGET_DISABLED, ACTION_APPWIDGET_OPTIONS_CHANGED)分發(fā)到各個回調(diào)函數(shù)中(onUpdate(), onDeleted(), onEnabled(), onDisabled, onAppWidgetOptionsChanged())兵迅。

onUpdate():是最重要的回調(diào)函數(shù),根據(jù) updatePeriodMillis 定義的定期刷新操作會調(diào)用該函數(shù)薪贫,此外當(dāng)用戶添加 Widget 時 也會調(diào)用該函數(shù)恍箭,可以在這里進(jìn)行必要的初始化操作。但如果在<appwidget-provider>中聲明了 android:configure 的 Activity瞧省,在用戶添加 Widget 時扯夭,不會調(diào)用 onUpdate(),需要由 configure Activity 去負(fù)責(zé)去調(diào)用 AppWidgetManager.updateAppWidget() 完成 Widget 更新鞍匾,后續(xù)的定時更新還是會繼續(xù)調(diào)用 onUpdate() 的交洗。
onDeleted():當(dāng) Widget 被刪除時調(diào)用該方法。
onEnabled():當(dāng) Widget 第一次被添加時調(diào)用橡淑,例如用戶添加了兩個你的 Widget藕筋,那么只有在添加第一個 Widget 時該方法會被調(diào)用。所以該方法比較適合執(zhí)行你所有 Widgets 只需進(jìn)行一次的操作梳码。
onDisabled():與 onEnabled 恰好相反,當(dāng)你的最后一個 Widget 被刪除時調(diào)用該方法伍掀,所以這里用來清理之前在 onEnabled() 中進(jìn)行的操作掰茶。
onAppWidgetOptionsChanged():當(dāng) Widget 第一次被添加或者大小發(fā)生變化時調(diào)用該方法,可以在此控制 Widget 元素的顯示和隱藏蜜笤。

示例代碼:

public class ExampleAppWidgetProvider extends AppWidgetProvider {
    private static final String TAG = "ExampleAppWidget";

    // 啟動ExampleAppWidgetService服務(wù)對應(yīng)的action
    private final Intent EXAMPLE_SERVICE_INTENT =
            new Intent("android.appwidget.action.EXAMPLE_APP_WIDGET_SERVICE");
    // 更新 widget 的廣播對應(yīng)的action
    private final String ACTION_UPDATE_ALL = "com.skywang.widget.UPDATE_ALL";
    // 保存 widget 的id的HashSet濒蒋,每新建一個 widget 都會為該 widget 分配一個 id。
    private static Set idsSet = new HashSet();

    // onUpdate() 在更新 widget 時,被執(zhí)行沪伙,
    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        Log.d(TAG, "onUpdate(): appWidgetIds.length="+appWidgetIds.length);

        // 每次 widget 被創(chuàng)建時瓮顽,對應(yīng)的將widget的id添加到set中
        for (int appWidgetId : appWidgetIds) {
            idsSet.add(Integer.valueOf(appWidgetId));
        }
        PreferencesUtils.putString(context, Keys.IDSSET, CharFunction.toJSONString(idsSet));
        updateAllAppWidgets(context, AppWidgetManager.getInstance(context), idsSet);
    }

    // 當(dāng) widget 被初次添加 或者 當(dāng) widget 的大小被改變時,被調(diào)用
    @Override
    public void onAppWidgetOptionsChanged(Context context,
                                          AppWidgetManager appWidgetManager, int appWidgetId,
                                          Bundle newOptions) {
        updateAllAppWidgets(context, AppWidgetManager.getInstance(context), idsSet);

    }

    // widget被刪除時調(diào)用
    @Override
    public void onDeleted(Context context, int[] appWidgetIds) {
        Log.d(TAG, "onDeleted(): appWidgetIds.length="+appWidgetIds.length);

        // 當(dāng) widget 被刪除時围橡,對應(yīng)的刪除set中保存的widget的id
        for (int appWidgetId : appWidgetIds) {
            idsSet.remove(Integer.valueOf(appWidgetId));
        }
        PreferencesUtils.putString(context,Keys.IDSSET, CharFunction.toJSONString(idsSet));
        super.onDeleted(context, appWidgetIds);
    }

    // 第一個widget被創(chuàng)建時調(diào)用
    @Override
    public void onEnabled(Context context) {
        Log.d(TAG, "onEnabled");
        updateAllAppWidgets(context, AppWidgetManager.getInstance(context), idsSet);
        super.onEnabled(context);
    }

    // 最后一個widget被刪除時調(diào)用
    @Override
    public void onDisabled(Context context) {
        Log.d(TAG, "onDisabled");
        // 在最后一個 widget 被刪除時暖混,終止服務(wù)
        context.stopService(EXAMPLE_SERVICE_INTENT);
        super.onDisabled(context);
    }


    // 接收廣播的回調(diào)函數(shù)
    @Override
    public void onReceive(Context context, Intent intent) {
        final String action = intent.getAction();
        Log.d(TAG, "OnReceive:Action: " + action);
        if (ACTION_UPDATE_ALL.equals(action)) {
            // “更新”廣播
            updateAllAppWidgets(context, AppWidgetManager.getInstance(context), idsSet);
        }

        super.onReceive(context, intent);
    }

    // 更新所有的 widget
    private void updateAllAppWidgets(Context context, AppWidgetManager appWidgetManager, Set set) {
        Log.d(TAG, "updateAllAppWidgets(): size="+set.size());
        // widget 的id
        int appID;
        // 迭代器,用于遍歷所有保存的widget的id
        Iterator it = set.iterator();
        while (it.hasNext()) {
            appID = ((Integer)it.next()).intValue();
            // 獲取 example_appwidget.xml 對應(yīng)的RemoteViews
            RemoteViews remoteView = new RemoteViews(context.getPackageName(), R.layout.example_appwidget);
            remoteView.setTextViewText(R.id.tv_gongli, CalendarUtil.getAllInfoHtml(Calendar.getInstance()));
            remoteView.setOnClickPendingIntent(R.id.tv_gongli, getPendingIntent(context, R.id.tv_gongli));
            // 更新 widget
            appWidgetManager.updateAppWidget(appID, remoteView);
        }
    }

    private PendingIntent getPendingIntent(Context context, int buttonId) {
        Intent intent = new Intent();
        intent.setClass(context, ExampleAppWidgetProvider.class);
        intent.addCategory(Intent.CATEGORY_ALTERNATIVE);
        intent.setData(Uri.parse("custom:" + buttonId));
        PendingIntent pi = PendingIntent.getBroadcast(context, 0, EXAMPLE_SERVICE_INTENT, 0 );
        return pi;
    }


}


創(chuàng)建 App Widget Configuration Activity

如果你的 Widget 需要用戶配置一些選項翁授,你可以為你的 Widget 創(chuàng)建 Configuration Activity拣播,當(dāng)用戶添加 Widget 時會自動彈出該 Activity。Configuration Activity 和普通 Activity 一樣需要在 Manifest 中聲明收擦,但是需要額外聲明一個 intent-filter: APPWIDGET_CONFIGURE贮配,例如:

<activity android:name=".ExampleAppWidgetConfigure">
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
    </intent-filter>
</activity>

同時還需要在上述的 appwidget-provider 中聲明:

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    ...
    android:configure="com.example.android.ExampleAppWidgetConfigure"
    ... >
</appwidget-provider>

有兩點需要注意的是:

  1. Activity 必須返回帶 EXTRA_APPWIDGET_ID 的 result。
  2. 聲明Configuration Activity 后 onUpdate() 在 Widget 添加時不會被調(diào)用塞赂,Activity 負(fù)責(zé)調(diào)用 AppWidgetManager.updateAppWidget() 完成 Widget 更新泪勒。
Intent intent = getIntent();
Bundle extras = intent.getExtras();
if (extras != null) {
    mAppWidgetId = extras.getInt(
        AppWidgetManager.EXTRA_APPWIDGET_ID,
        AppWidgetManager.INVALID_APPWIDGET_ID);
}

AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
RemoteViews views = new RemoteViews(context.getPackageName(),R.layout.example_appwidget);
appWidgetManager.updateAppWidget(mAppWidgetId, views);

Intent resultValue = new Intent();
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
setResult(RESULT_OK, resultValue);
finish();

參考:
http://www.reibang.com/p/985547afd22f
http://glgjing.github.io/blog/2015/11/05/android-kai-fa-zhi-app-widget-xiang-jie/
https://blog.csdn.net/yangwen123/article/details/8042499

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市宴猾,隨后出現(xiàn)的幾起案子圆存,更是在濱河造成了極大的恐慌,老刑警劉巖鳍置,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件辽剧,死亡現(xiàn)場離奇詭異,居然都是意外死亡税产,警方通過查閱死者的電腦和手機怕轿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來辟拷,“玉大人撞羽,你說我怎么就攤上這事∩蓝常” “怎么了诀紊?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長隅俘。 經(jīng)常有香客問我邻奠,道長,這世上最難降的妖魔是什么为居? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任碌宴,我火速辦了婚禮,結(jié)果婚禮上蒙畴,老公的妹妹穿的比我還像新娘贰镣。我一直安慰自己呜象,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布碑隆。 她就那樣靜靜地躺著恭陡,像睡著了一般。 火紅的嫁衣襯著肌膚如雪上煤。 梳的紋絲不亂的頭發(fā)上休玩,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天,我揣著相機與錄音楼入,去河邊找鬼哥捕。 笑死,一個胖子當(dāng)著我的面吹牛嘉熊,可吹牛的內(nèi)容都是我干的遥赚。 我是一名探鬼主播,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼阐肤,長吁一口氣:“原來是場噩夢啊……” “哼凫佛!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起孕惜,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤愧薛,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后衫画,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體毫炉,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年削罩,在試婚紗的時候發(fā)現(xiàn)自己被綠了瞄勾。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡弥激,死狀恐怖进陡,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情微服,我是刑警寧澤趾疚,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站以蕴,受9級特大地震影響糙麦,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜丛肮,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一喳资、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧腾供,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至榜聂,卻和暖如春搞疗,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背须肆。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工匿乃, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人豌汇。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓幢炸,卻偏偏與公主長得像,于是被迫代替她去往敵國和親拒贱。 傳聞我的和親對象是個殘疾皇子宛徊,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,724評論 2 354

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