天天看點

Android 之視窗小部件詳解--App Widget 1 App Widget簡介2 App Widget主要的相關類介紹

Android 之視窗小部件詳解--App Widget

 版本号 說明 作者 日期 
1.0  添加App Widge介紹和示例  Sky Wang 2013/06/27 

1 App Widget簡介

App Widget是應用程式視窗小部件(Widget)是微型的應用程式視圖,它可以被嵌入到其它應用程式中(比如桌面)并接收周期性的更新。你可以通過一個App Widget Provider來釋出一個Widget。

本文參考Android官方文本,先介紹App Widget的主要元件,然後再以示例來詳細說明。

2 App Widget主要的相關類介紹

2.1 AppWidgetProvider

AppWidgetProvider 繼承自 BroadcastReceiver,它能接收 widget 相關的廣播,例如 widget 的更新、删除、開啟和禁用等。

AppWidgetProvider中的廣播處理函數如下:

onUpdate()

  當 widget 更新時被執行。

  同樣,當使用者首次添加 widget 時,onUpdate() 也會被調用,這樣 widget 就能進行必要的設定工作(如果需要的話) 。但是,如果定義了 widget 的 configure屬性(即android:config,後面會介紹),那麼當使用者首次添加 widget 時,onUpdate()不會被調用;之後更新 widget 時,onUpdate才會被調用。

onAppWidgetOptionsChanged()

  當 widget 被初次添加 或者 當 widget 的大小被改變時,執行onAppWidgetOptionsChanged()。你可以在該函數中,根據 widget 的大小來顯示/隐藏某些内容。可以通過 getAppWidgetOptions() 來傳回 Bundle 對象以讀取 widget 的大小資訊,Bundle中包括以下資訊:

  OPTION_APPWIDGET_MIN_WIDTH -- 包含 widget 目前寬度的下限,以dp為機關。

  OPTION_APPWIDGET_MIN_HEIGHT -- 包含 widget 目前高度的下限,以dp為機關。

  OPTION_APPWIDGET_MAX_WIDTH -- 包含 widget 目前寬度的上限,以dp為機關。

  OPTION_APPWIDGET_MAX_HEIGHT -- 包含 widget 目前高度的上限,以dp為機關。

onAppWidgetOptionsChanged() 是 Android 4.1 引入的。

onDeleted(Context, int[])

  當 widget 被删除時被觸發。

onEnabled(Context)

  當第1個 widget 的執行個體被建立時觸發。也就是說,如果使用者對同一個 widget 增加了兩次(兩個執行個體),那麼onEnabled()隻會在第一次增加widget時觸發。

onDisabled(Context)

  當最後1個 widget 的執行個體被删除時觸發。

onReceive(Context, Intent)

  接收到任意廣播時觸發,并且會在上述的方法之前被調用。

  總結,AppWidgetProvider 繼承于 BroadcastReceiver。實際上,App Widge中的onUpdate()、onEnabled()、onDisabled()等方法都是在 onReceive()中調用的;是onReceive()對特定事情的響應函數。參考android源碼frameworks/base/core/java/android/appwidget/AppWidgetProvider.java中onReceive()的定義:

public void onReceive(Context context, Intent intent) {
    // Protect against rogue update broadcasts (not really a security issue,
    // just filter bad broacasts out so subclasses are less likely to crash).
    String action = intent.getAction();
    if (AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)) {
        Bundle extras = intent.getExtras();
        if (extras != null) {
            int[] appWidgetIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS);
            if (appWidgetIds != null && appWidgetIds.length > 0) {
                this.onUpdate(context, AppWidgetManager.getInstance(context), appWidgetIds);
            }   
        }   
    }   
    else if (AppWidgetManager.ACTION_APPWIDGET_DELETED.equals(action)) {
        Bundle extras = intent.getExtras();
        if (extras != null && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_ID)) {
            final int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID);
            this.onDeleted(context, new int[] { appWidgetId }); 
        }   
    }   
    else if (AppWidgetManager.ACTION_APPWIDGET_OPTIONS_CHANGED.equals(action)) {
        Bundle extras = intent.getExtras();
        if (extras != null && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_ID)
                && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS)) {
            int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID);
            Bundle widgetExtras = extras.getBundle(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS);
            this.onAppWidgetOptionsChanged(context, AppWidgetManager.getInstance(context),
                    appWidgetId, widgetExtras);
        }   
    }   
    else if (AppWidgetManager.ACTION_APPWIDGET_ENABLED.equals(action)) {
        this.onEnabled(context);
    }   
    else if (AppWidgetManager.ACTION_APPWIDGET_DISABLED.equals(action)) {
        this.onDisabled(context);
    }   
}      

View Code

2.2 AppWidgetProviderInfo

AppWidgetProviderInfo描述一個App Widget中繼資料,比如App Widget的布局,更新頻率,以及AppWidgetProvider 類。這個應該在XML裡定義。下面以XML示例來對AppWidgetProviderInfo中常用的類型進行說明。

示例XML:

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

  android:minWidth="40dp"

  android:minHeight="40dp"

  android:updatePeriodMillis="86400000"

  android:previewImage="@drawable/preview"

  android:initialLayout="@layout/example_appwidget"

  android:configure="com.example.android.ExampleAppWidgetConfigure"

  android:resizeMode="horizontal|vertical"

  android:widgetCategory="home_screen|keyguard"

  android:initialKeyguardLayout="@layout/example_keyguard">

</appwidget-provider>

示例說明:

minWidth 和minHeight

  它們指定了App Widget布局需要的最小區域。

  預設的App Widgets所在視窗的桌面位置基于有确定高度和寬度的單元網格中。如果App Widget的最小長度或寬度和這些網格單元的尺寸不比對,那麼這個App Widget将上舍入(上舍入即取比該值大的最接近的整數——譯者注)到最接近的單元尺寸。

  注意:app widget的最小尺寸,不建議比 “4x4” 個單元格要大。關于app widget的尺寸,後面在詳細說明。

minResizeWidth 和 minResizeHeight

  它們屬性指定了 widget 的最小絕對尺寸。也就是說,如果 widget 小于該尺寸,便會因為變得模糊、看不清或不可用。 使用這兩個屬性,可以允許使用者重新調整 widget 的大小,使 widget 的大小可以小于 minWidth 和 minHeight。

  注意:(01) 當 minResizeWidth 的值比 minWidth 大時,minResizeWidth 無效;當 resizeMode 的取值不包括 horizontal 時,minResizeWidth 無效。

           (02) 當 minResizeHeight 的值比 minHeight 大時,minResizeHeight 無效;當 resizeMode 的取值不包括 vertical 時,minResizeHeight 無效。

updatePeriodMillis

  它定義了 widget 的更新頻率。實際的更新時機不一定是精确的按照這個時間發生的。建議更新盡量不要太頻繁,最好是低于1小時一次。 或者可以在配置 Activity 裡面供使用者對更新頻率進行配置。 實際上,當updatePeriodMillis的值小于30分鐘時,系統會自動将更新頻率設為30分鐘!關于這部分,後面會詳細介紹。

  注意: 當更新時機到達時,如果裝置正在休眠,那麼裝置将會被喚醒以執行更新。如果更新頻率不超過1小時一次,那麼對電池壽命應該不會造成多大的影響。 如果你需要比較頻繁的更新,或者你不希望在裝置休眠的時候執行更新,那麼可以使用基于 alarm 的更新來替代 widget 自身的重新整理機制。将 alarm 類型設定為 ELAPSED_REALTIME 或 RTC,将不會喚醒休眠的裝置,同時請将 updatePeriodMillis 設為 0。

initialLayout

  指向 widget 的布局資源檔案

configure

  可選屬性,定義了 widget 的配置 Activity。如果定義了該項,那麼當 widget 建立時,會自動啟動該 Activity。

previewImage

  指定預覽圖,該預覽圖在使用者選擇 widget 時出現,如果沒有提供,則會顯示應用的圖示。該字段對應在 AndroidManifest.xml 中 receiver 的 android:previewImage 字段。由 Android 3.0 引入。

autoAdvanceViewId

  指定一個子view ID,表明該子 view 會自動更新。在 Android 3.0 中引入。

resizeMode

  指定了 widget 的調整尺寸的規則。可取的值有: "horizontal", "vertical", "none"。"horizontal"意味着widget可以水準拉伸,“vertical”意味着widget可以豎值拉伸,“none”意味着widget不能拉伸;預設值是"none"。Android 3.1 引入。

widgetCategory

  指定了 widget 能顯示的地方:能否顯示在 home Screen 或 lock screen 或 兩者都可以。它的取值包括:"home_screen" 和 "keyguard"。Android 4.2 引入。

initialKeyguardLayout

  指向 widget 位于 lockscreen 中的布局資源檔案。Android 4.2 引入。

3 App Widget布局說明

3.1 添加 widget 到lock screen中

  預設情況下(即不設定android:widgetCategory屬性),Android是将widget添加到 home screen 中。

  但在Android 4.2中,若使用者希望 widget 可以被添加到lock screen中,可以通過設定 widget 的 android:widgetCategory 屬性包含keyguard來完成。

  當你把 widget 添加到lock screen中時,你可能對它進行定制化操作,以差別于添加到home screen中的情況。 你能夠通過 getAppWidgetOptions() 來進行判斷 widget 是被添加到lock screen中,還是home screen中。通過 getApplicationOptions() 擷取 Bundle對象,然後讀取 Bundle 的OPTION_APPWIDGET_HOST_CATEGORY值:若值為 WIDGET_CATEGORY_HOME_SCREEN, 則表示該 widget 被添加到home screen中; 若值為 WIDGET_CATEGORY_KEYGUARD,則表示該 widget 被添加到lock screen中。

  另外,你應該為添加到lock screen中的 widget 單獨使用一個layout,可以通過 android:initialKeyguardLayout 來指定。而 widget 添加到home screen中的layout則可以通過 android:initialLayout 來指定。

3.2 布局

一 Widget視窗元件

Android 之視窗小部件詳解--App Widget 1 App Widget簡介2 App Widget主要的相關類介紹

  如上圖所示,典型的App Widget有三個主要元件:一個邊界框(A bounding box),一個架構(a Frame),和控件的圖形控件(Widget Controls)和其他元素。App Widget并不支援全部的視圖視窗,它隻是支援視圖視窗的一個子集,後面會詳細說明支援哪些視圖視窗。

  要設計美觀的App Widget,建議在“邊界框和架構之間(Widget Margins)”以及“架構和控件(Widget Padding)”之間填留有空隙。在Android4.0以上的版本,系統為自動的保留這些空隙。

二 Widget視窗大小

  在AppWidgetProviderInfo中已經介紹了,minWidth 和minHeight 用來指定了App Widget布局需要的最小區域。預設的App Widgets所在視窗的桌面位置基于有确定高度和寬度的單元網格中。如果App Widget的最小長度或寬度和這些網格單元的尺寸不比對,那麼這個App Widget将上舍入(上舍入即取比該值大的最接近的整數——譯者注)到最接近的單元尺寸。

  例如,很多手機提供4x4網格,平闆電腦能提供8x7網格。當widget被添加到時,在滿足minWidth和minHeight限制的前提下,它将被占領的最小數目的細胞。

粗略計算minWidth和minHeight,可以參考下面表格:

單元格個數

(行 / 列)

對應的設定大小 (dp)

(

minWidth

 / 

minHeight

)
1 40dp
2 110dp
3 180dp
4 250dp
n 70 × n − 30

詳細計算minWidth和minHeight,要計算各個區域的大小。以下圖為例:

Android 之視窗小部件詳解--App Widget 1 App Widget簡介2 App Widget主要的相關類介紹

計算結果:

minWidth = 144dp + (2 × 8dp) + (2 × 56dp) = 272dp

minHeight = 48dp + (2 × 4dp) = 56dp

3.3 App Widget支援的布局和控件

Widget并不支援所有的布局和控件,而僅僅隻是支援Android布局和控件的一個子集。

(01) App Widget支援的布局:

  FrameLayout

  LinearLayout

  RelativeLayout

  GridLayout

(02) App Widget支援的控件:

  AnalogClock

  Button

  Chronometer

  ImageButton

  ImageView

  ProgressBar

  TextView

  ViewFlipper

  ListView

  GridView

  StackView

  AdapterViewFlipper

4 App Widget應用執行個體

  應用執行個體需求:建立一個Widget示例,要求Widget能被添加到主屏中,widget包含3個成分:文本、按鈕和圖檔。文本要求:顯示提示資訊;按鈕要求:點選按鈕,彈出一個Toast提示框;圖檔要求:每個5秒随機更新一張圖檔。

第1步 建立Android工程

建立空白的Android工程,即不需要在建立Activity子類。

第2步 編輯manifest

修改AndroidManifest.xml,代碼如下:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.skywang.widget"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="14"
        android:targetSdkVersion="17" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        
        <!-- 聲明widget對應的AppWidgetProvider -->
        <receiver android:name=".ExampleAppWidgetProvider" >
            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
                <action android:name="com.skywang.widget.UPDATE_ALL"/>
            </intent-filter>
            <meta-data android:name="android.appwidget.provider"
                android:resource="@xml/example_appwidget_info" />
        </receiver>
        
        <service android:name=".ExampleAppWidgetService" >
            <intent-filter>
                <action android:name="android.appwidget.action.EXAMPLE_APP_WIDGET_SERVICE" />
            </intent-filter>
        </service>
        
    </application>

</manifest>      

說明:

(01) ExampleAppWidgetProvider是繼承于的AppWidgetProvider類,用來響應widget的添加、删除、更新等操作。

(02) android.appwidget.action.APPWIDGET_UPDATE,必須要顯示聲明的action!因為所有的widget的廣播都是通過它來發送的;要接收widget的添加、删除等廣播,就必須包含它。

(03) action android:name="com.skywang.widget.UPDATE_ALL,這是我自己添加了,是為了接收服務所發送的更新圖檔的廣播。

(04) <meta-data> 指定了 AppWidgetProviderInfo 對應的資源檔案

    android:name -- 指定metadata名,通過android.appwidget.provider來辨識data。

    android:resource -- 指定 AppWidgetProviderInfo 對應的資源路徑。即,xml/example_appwidget_info.xml。

(05) ExampleAppWidgetService 是用于更新widget中的圖檔的服務。

(06) android.appwidget.action.EXAMPLE_APP_WIDGET_SERVICE 用于啟動服務的action。

第3步 編輯AppWidgetProviderInfo對應的資源檔案

在目前工程下建立xml目錄(若xml目錄不存在的話);并在xml目錄下建立example_appwidget_info.xml檔案。xml檔案内容如下:

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="180dp"
    android:minHeight="180dp"
    android:previewImage="@drawable/preview"
    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 : 預覽圖檔
    android:initialLayout : 加載到桌面時對應的布局檔案
    android:resizeMode : widget可以被拉伸的方向。horizontal表示可以水準拉伸,vertical表示可以豎直拉伸
    android:widgetCategory : widget可以被顯示的位置。home_screen表示可以将widget添加到桌面,keyguard表示widget可以被添加到鎖屏界面。
    android:initialKeyguardLayout : 加載到鎖屏界面時對應的布局檔案
     -->
    
</appwidget-provider>      

說明:

(01) android:previewImage,用于指定預覽圖檔。即搜尋到widget時,檢視到的圖檔。若沒有設定的話,系統為指定一張預設圖檔。

(02) android:updatePeriodMillis 更新widget的時間間隔(ms)。在實際測試中,發現設定android:updatePeriodMillis的值為5秒時,不管用!跟蹤android源代碼,發現:

當android:updatePeriodMillis的值小于30分鐘時,會被設定為30分鐘。也就意味着,即使将它的值設為5秒,實際上也是30分鐘之後才更新。是以,我們若向動态的更新widget的某元件,最好通過service、AlarmManager、Timer等方式;本文采用的是service。

android源碼中widget服務檔案:frameworks/base/services/java/com/android/server/AppWidgetService.java

與 android:updatePeriodMillis相關代碼如下:

...
private static final int MIN_UPDATE_PERIOD = 30 * 60 * 1000; // 30 minutes
    ...

void registerForBroadcastsLocked(Provider p, int[] appWidgetIds) {

    ...
    // 擷取updatePeriodMillis的值
    long period = p.info.updatePeriodMillis;
    // 當updatePeriodMillis小于30分鐘時,設為它為30分鐘
    if (period < MIN_UPDATE_PERIOD) {
        period = MIN_UPDATE_PERIOD;
    }    
    mAlarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
            SystemClock.elapsedRealtime() + period, period, p.broadcast);
   ...
}      

第4步 編輯example_appwidget.xml等資源檔案

建立layout/example_appwidget.xml,代碼如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >
  
    <LinearLayout 
        android:layout_width="wrap_content"  
        android:layout_height="wrap_content"
        android:layout_gravity="center" 
        android:orientation="horizontal" >
        
        <TextView  
            android:layout_width="wrap_content"  
            android:layout_height="wrap_content"  
            android:text="HomeScreen Widget" />    
        
        <Button
            android:id="@+id/btn_show"
            android:layout_width="wrap_content"  
            android:layout_height="wrap_content"  
            android:text="Show" />
    </LinearLayout> 
        
    <ImageView
        android:id="@+id/iv_show"
        android:layout_width="wrap_content"  
        android:layout_height="wrap_content" 
        android:layout_gravity="center"/> 
        
</LinearLayout>      

說明:

(01) example_appwidget布局中,包含了“1個文本,1個按鈕和1個圖檔控件”。

點選下載下傳:本工程中需要用到的圖檔

将所有的圖檔放到drawable目錄中。這裡的drawable廣義的,指工程實際用到的圖檔所在目錄;例如,我自己的是放在drawabld-hdmi中。

第5步 編輯ExampleAppWidgetProvider.java

在目前工程下,建立ExampleAppWidgetProvider.java,代碼如下:

package com.skywang.widget;

import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;  
import android.appwidget.AppWidgetProvider;  
import android.content.Context;  
import android.content.Intent;  
import android.os.Bundle;
import android.net.Uri;
import android.widget.RemoteViews;
import android.widget.Toast;
import android.util.Log;

import java.util.Set;
import java.util.HashSet;
import java.util.Iterator;

/*
 * @author : skywang <[email protected]>
 * description : 提供App Widget
 */

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

    private boolean DEBUG = false; 
    // 啟動ExampleAppWidgetService服務對應的action
    private final Intent EXAMPLE_SERVICE_INTENT = 
            new Intent("android.appwidget.action.EXAMPLE_APP_WIDGET_SERVICE");
    // 更新 widget 的廣播對應的action
    private final String ACTION_UPDATE_ALL = "com.skywang.widget.UPDATE_ALL";
    // 儲存 widget 的id的HashSet,每建立一個 widget 都會為該 widget 配置設定一個 id。
    private static Set idsSet = new HashSet();
    // 按鈕資訊
    private static final int BUTTON_SHOW = 1;
    // 圖檔數組
    private static final int[] ARR_IMAGES = { 
        R.drawable.sample_0, 
        R.drawable.sample_1, 
        R.drawable.sample_2, 
        R.drawable.sample_3, 
        R.drawable.sample_4, 
        R.drawable.sample_5,
        R.drawable.sample_6,
        R.drawable.sample_7,
    };
    
    // onUpdate() 在更新 widget 時,被執行,
    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        Log.d(TAG, "onUpdate(): appWidgetIds.length="+appWidgetIds.length);

        // 每次 widget 被建立時,對應的将widget的id添加到set中
        for (int appWidgetId : appWidgetIds) {
            idsSet.add(Integer.valueOf(appWidgetId));
        }
        prtSet();
    }
    
    // 當 widget 被初次添加 或者 當 widget 的大小被改變時,被調用 
    @Override  
    public void onAppWidgetOptionsChanged(Context context,  
            AppWidgetManager appWidgetManager, int appWidgetId,  
            Bundle newOptions) {
        Log.d(TAG, "onAppWidgetOptionsChanged");
        super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId,  
                newOptions);  
    }  
    
    // widget被删除時調用  
    @Override  
    public void onDeleted(Context context, int[] appWidgetIds) {  
        Log.d(TAG, "onDeleted(): appWidgetIds.length="+appWidgetIds.length);

        // 當 widget 被删除時,對應的删除set中儲存的widget的id
        for (int appWidgetId : appWidgetIds) {
            idsSet.remove(Integer.valueOf(appWidgetId));
        }
        prtSet();
        
        super.onDeleted(context, appWidgetIds);  
    }

    // 第一個widget被建立時調用  
    @Override  
    public void onEnabled(Context context) {  
        Log.d(TAG, "onEnabled");
        // 在第一個 widget 被建立時,開啟服務
        context.startService(EXAMPLE_SERVICE_INTENT);
        
        super.onEnabled(context);  
    }  
    
    // 最後一個widget被删除時調用  
    @Override  
    public void onDisabled(Context context) {  
        Log.d(TAG, "onDisabled");

        // 在最後一個 widget 被删除時,終止服務
        context.stopService(EXAMPLE_SERVICE_INTENT);

        super.onDisabled(context);  
    }
    
    
    // 接收廣播的回調函數
    @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);
        } else if (intent.hasCategory(Intent.CATEGORY_ALTERNATIVE)) {
            // “按鈕點選”廣播
            Uri data = intent.getData();
            int buttonId = Integer.parseInt(data.getSchemeSpecificPart());
            if (buttonId == BUTTON_SHOW) {
                Log.d(TAG, "Button wifi clicked");
                Toast.makeText(context, "Button Clicked", Toast.LENGTH_SHORT).show();
            }
        }
        
        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();    
            // 随機擷取一張圖檔
            int index = (new java.util.Random().nextInt(ARR_IMAGES.length));
            
            if (DEBUG) Log.d(TAG, "onUpdate(): index="+index);            
            // 擷取 example_appwidget.xml 對應的RemoteViews            
            RemoteViews remoteView = new RemoteViews(context.getPackageName(), R.layout.example_appwidget);
            
            // 設定顯示圖檔
            remoteView.setImageViewResource(R.id.iv_show, ARR_IMAGES[index]);
            
            // 設定點選按鈕對應的PendingIntent:即點選按鈕時,發送廣播。
            remoteView.setOnClickPendingIntent(R.id.btn_show, getPendingIntent(context,
                    BUTTON_SHOW));

            // 更新 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, intent, 0 );
        return pi;
    }

    // 調試用:周遊set
    private void prtSet() {
        if (DEBUG) {
            int index = 0;
            int size = idsSet.size();
            Iterator it = idsSet.iterator();
            Log.d(TAG, "total:"+size);
            while (it.hasNext()) {
                Log.d(TAG, index + " -- " + ((Integer)it.next()).intValue());
            }
        }
    }
}      

說明:

(01) 當我們建立第一個widget到桌面時,會執行onEnabled()。在onEnabled()中通過 context.startService(EXAMPLE_SERVICE_INTENT) 啟動服務ExampleAppWidgetService。服務的作用就是每隔5秒發送一個ACTION_UPDATE_ALL廣播給我們,用于更新widget中的圖檔。

       僅僅當我們建立第一個widget時才會啟動服務,因為onEnabled()隻會在第一個widget被建立時才執行。

(02) 當我們删除最後一個widget到桌面時,會執行onDisabled()。在onDisabled()中通過 context.stopService(EXAMPLE_SERVICE_INTENT) 終止服務ExampleAppWidgetService。

       僅僅當我們删除最後一個widget時才會終止服務,因為onDisabled()隻會在最後一個widget被删除時才執行。

(03) 本工程中,每添加一個widget都會執行onUpdate()。例外情況:在定義android:configure的前提下,第一次添加widget時不會執行onUpdate(),而是執行android:configure中定義的activity。

(04) onReceive()中,處理兩個廣播:更新桌面的widget 以及 響應按鈕點選廣播。

       當收到ACTION_UPDATE_ALL廣播時,調用updateAllAppWidgets()來更新所有的widget。

       當收到的廣播的categery為Intent.CATEGORY_ALTERNATIVE,并且scheme為BUTTON_SHOW時,對應是按鈕點選事件。按鈕的監聽是在updateAllAppWidgets()中注冊的。

第6步 編輯ExampleAppWidgetService.java

在目前工程下,建立ExampleAppWidgetService.java,代碼如下:

package com.skywang.widget;

import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;

/*
 * @author : skywang <[email protected]>
 * description : 周期性更新AppWidget的服務
 */

public class ExampleAppWidgetService extends Service {
    
    private static final String TAG="ExampleAppWidgetService"; 

    // 更新 widget 的廣播對應的action
    private final String ACTION_UPDATE_ALL = "com.skywang.widget.UPDATE_ALL";
    // 周期性更新 widget 的周期
    private static final int UPDATE_TIME = 5000;
    // 周期性更新 widget 的線程
    private UpdateThread mUpdateThread;
    private Context mContext;
    // 更新周期的計數
    private int count=0;      

    @Override
    public void onCreate() {
        
        // 建立并開啟線程UpdateThread
        mUpdateThread = new UpdateThread();
        mUpdateThread.start();
        
        mContext = this.getApplicationContext();

        super.onCreate();
    }
    
    @Override
    public void onDestroy(){
        // 中斷線程,即結束線程。
        if (mUpdateThread != null) {
            mUpdateThread.interrupt();
        }
        
        super.onDestroy();
    }
    
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    /*
     * 服務開始時,即調用startService()時,onStartCommand()被執行。
     */
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d(TAG, "onStartCommand");        
        super.onStartCommand(intent, flags, startId);
        
        return START_STICKY;
    }
    
    private class UpdateThread extends Thread {

        @Override
        public void run() {
            super.run();

            try {
                count = 0;
                while (true) {
                    Log.d(TAG, "run ... count:"+count);
                    count++;

                    Intent updateIntent=new Intent(ACTION_UPDATE_ALL);
                    mContext.sendBroadcast(updateIntent);
                    
                    Thread.sleep(UPDATE_TIME);
                } 
            } catch (InterruptedException e) {
                // 将 InterruptedException 定義在while循環之外,意味着抛出 InterruptedException 異常時,終止線程。
                e.printStackTrace();
            }
        }
    }
}      

說明:

(01) onCreate() 在建立服務時被執行。它的作用是建立并啟動線程UpdateThread()。

(02) onDestroy() 在銷毀服務時被執行。它的作用是登出線程UpdateThread()。

(03) 服務UpdateThread 每隔5秒,發送1個廣播ACTION_UPDATE_ALL。廣播ACTION_UPDATE_ALL在ExampleAppWidgetProvider被處理:用來更新widget中的圖檔。

點選下載下傳:源代碼

點選檢視:skywang部落格索引

widget在添加到桌面前的效果圖:

Android 之視窗小部件詳解--App Widget 1 App Widget簡介2 App Widget主要的相關類介紹

widget在添加到桌面後的效果圖:

Android 之視窗小部件詳解--App Widget 1 App Widget簡介2 App Widget主要的相關類介紹