第二十章 AppWidget

      在〈第二十章 AppWidget〉中尚無留言

小工具

Android提供一種特別的元件AppWidget, 可以讓使用者在桌面上直接瀏覽資料, 或是執行一些簡單的操作. 例如在桌面上顯示時間, 行事曆或氣候資訊, 這種元件稱為小工具元件.

建立時鐘小工具

底下以一個時鐘小工具進行說明. 開啟新專案後, 選取Activity時選用 Add no Activity. 然後於App按右鍵/New/Widget/AppWidget, 然後依下面圖示說明修改.

此專案因為主Activity, 所以在工具列的App會有x的錯誤, 需點選Edit Configuration/Launch選Nothing

android_appwidget_1

小工具畫面設計

小工具畫面可用的Layout只有FrameLayout, LinearLayout, RelativeLayout及GridLayout. 而能用的畫面元件也蠻少的, 如TextView, Button, ImageButton, ListView, ImageView等. 注意, EditText不可以使用
打開res/layout/clock_app_widget.xml, 設定如下

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#09C"
    android:padding="@dimen/widget_margin"
    android:orientation="vertical">
    <TextView
        android:id="@+id/now_text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:background="#09C"
        android:text="12:00"
        android:textColor="#ffffff"
        android:textSize="36sp"
        android:textStyle="bold|italic" />
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/textView"
        android:text="@string/app_name"
        android:gravity="center"
        android:textColor="#ffffff"
        android:textSize="12sp"/>
</LinearLayout>

小工具專用設定檔

專用設定檔位於res/xml之下, 開啟clock_app_widget_info.xml, 可看到minHeight及minWidth. 桌面每格為70*70dp, 且建議周圍要有30dp的空間. 因為此例子為4*1格, 所以寬度為70*4-30=250, 高度為70*1-30=40.
updatePeriodMillis為多久會更新一次, 單位是ms, 因此例每分鐘要變更一次, 所以設定為60000ms. 修改如下

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:configure="com.asuscomm.mahaljsp.ch20_clock.ClockAppWidgetConfigureActivity"
    android:initialKeyguardLayout="@layout/clock_app_widget"
    android:initialLayout="@layout/clock_app_widget"
    android:minHeight="40dp"
    android:minWidth="250dp"
    android:previewImage="@drawable/example_appwidget_preview"
    android:updatePeriodMillis="60000"
    android:widgetCategory="home_screen"></appwidget-provider>

小工具元件類別

開啟ClockAppWidget.java, 此類別繼承了AppWidgetProvider, 而AppWidgetProvider又繼承了BroadcastReceiver類別. 此類別有如下的方法

onEnable() : 安裝第一個小工具到桌面時會調用, 且只調用一次, 也就是安裝第二個時, 就不會調用
onUpdate() : 每安裝一個小工具, 就會調用一次
onDelete() : 每刪除一個小工具, 就會調用一次
onDisable() : 刪除最後一個小工具時調用, 且只會調用一次

ClockAppWidget.java

public class ClockAppWidget extends AppWidgetProvider {

    static void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
                                int appWidgetId) {

        CharSequence widgetText = ClockAppWidgetConfigureActivity.loadTitlePref(context, appWidgetId);
        // Construct the RemoteViews object
        RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.clock_app_widget);
        views.setTextViewText(R.id.appwidget_text, widgetText);
        // Instruct the widget manager to update the widget
        appWidgetManager.updateAppWidget(appWidgetId, views);
    }
    @Override
    public void onEnabled(Context context) {
        // Enter relevant functionality for when the first widget is created
    }
    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        // There may be multiple widgets active, so update all of them
        for (int appWidgetId : appWidgetIds) {
            updateAppWidget(context, appWidgetManager, appWidgetId);
        }
    }
    @Override
    public void onDeleted(Context context, int[] appWidgetIds) {
        // When the user deletes the widget, delete the preference associated with it.
        for (int appWidgetId : appWidgetIds) {
            ClockAppWidgetConfigureActivity.deleteTitlePref(context, appWidgetId);
        }
    }
    @Override
    public void onDisabled(Context context) {
        // Enter relevant functionality for when the last widget is disabled
    }
}

AndroidManifest.xml設定檔

在設定檔中, 需加入<receiver>標簽, 設定如下

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.asuscomm.mahaljsp.ch20_clock">
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <receiver android:name=".ClockAppWidget">
            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
            </intent-filter>
            <meta-data
                android:name="android.appwidget.provider"
                android:resource="@xml/clock_app_widget_info" />
        </receiver>
        <activity android:name=".ClockAppWidgetConfigureActivity">
            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
            </intent-filter>
        </activity>
    </application>
</manifest>

上述編譯安裝後, 即可將小工具放於桌面上

android_appwidget_2

更新畫面

小工具的專用設定檔中, updatePeriodMills 定義了 appWidget 的刷新頻率, 但是基於節約使用者電量的考慮, Android 系統預設最小更新週期是 30 分鐘, 也就是說, 如果程式需要即時更新資料, 設置這個更新週期是 2 秒, 那麼程式還是不會每隔 2 秒就收到更新通知的, 而是要等到 30 分鐘以上才會更新, 所以這個例子不能依靠onUpdate();

錯誤的寫法

那怎麼辦? 還好系統有一個ACTION_TIME_CLICK的廣播每隔一分鐘就廣播一次. 問題來了, 要在那裏註冊監聽這個廣播呢? 這個廣播不能使用靜態註冊, 所以只能使用動態註冊. 所以應該可以把動態註冊寫在AppWidget的onEnabled()裏, 而且AppWidget又是繼承了BroadcastReceiver類別, 所以可以覆寫onReceiver()來更改畫面, 如下程式碼. 但是經過實測, 這樣的寫法會在一段時間後莫名的失去註冊而無法接收到廣播, 時間就會停止更新. 在Google Play中下載經典Sense風格時鐘小工具就有這個Issue

以下是錯誤的程式碼

public void onEnabled(Context context) {
    context.getApplicationContext().registerReceiver(
            this, new IntentFilter(Intent.ACTION_TIME_TICK));
}
public void onReceive(Context context, Intent intent) {
    RemoteViews views=new RemoteViews(context.getPackageName(), R.layout.clock_app_widget);
    AppWidgetManager manager=AppWidgetManager.getInstance(context);
    ComponentName cn=new ComponentName(context, ClockAppWidget.class);
    views.setTextViewText(R.id.now_text, getTime());
    manager.updateAppWidget(cn, views);
    super.onReceive(context, intent);
}
private static String getTime(){
    Date d=new Date(System.currentTimeMillis());
    SimpleDateFormat sdf=new SimpleDateFormat("HH:mm");
    return sdf.format(d);
}

正確的寫法

AppWidget的onEnabled()在小工具第一次安裝到畫面時會被執行, 而且它還有一個特性, 就是在重新開機後, 會自動的執行一次. 所以, 可以利用這個特性使用下面的步驟
1. 在AppWidget的onEnabled()啟動一個Service.
2. 在這個Service註冊一個接收TIME_TICK的Receiver.
3. 在Receiver中更新AppWidget的畫面


AppWidget註冊Service

下面的程式碼說明啟動Service的方式.  請特別注意, 每次安裝一次小工具, 系統就會new 出ClockAppWidget的物件, 而每次new 出來後, 裏面的物件變數都是預設值. 所以intentService一定要使用static. 這觀念在張X裕的書中有嚴重的錯誤.

public class ClockAppWidget extends AppWidgetProvider {
    static Intent intentService;//一定要使用static
    /*
            請注意, 書上寫的有錯誤
            每次安裝一個小工具到桌面, 就會new 出此類別的物件.
            而onEnable只有在第一次安裝時才會執行
             所以如果把intentService寫成物件變數, 則第二個小工具開始, 此intentService都是null
        */
    static void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
                                int appWidgetId) {
        CharSequence widgetText = ClockAppWidgetConfigureActivity.loadTitlePref(context, appWidgetId);
        RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.clock_app_widget);
        views.setTextViewText(R.id.appwidget_text, widgetText);
        SimpleDateFormat sdf=new SimpleDateFormat("HH:mm");
        views.setTextViewText(R.id.now_text, sdf.format(new Date(System.currentTimeMillis())));
        appWidgetManager.updateAppWidget(appWidgetId, views);
    }
    @Override
    public void onEnabled(Context context) {
        intentService=new Intent(context, ClockService.class);
        context.startService(intentService);
    }
    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        for (int appWidgetId : appWidgetIds) {
            updateAppWidget(context, appWidgetManager, appWidgetId);
        }
    }
    @Override
    public void onDeleted(Context context, int[] appWidgetIds) {
        for (int appWidgetId : appWidgetIds) {
            ClockAppWidgetConfigureActivity.deleteTitlePref(context, appWidgetId);
        }
    }
    @Override
    public void onDisabled(Context context) {
        context.stopService(intentService);
        intentService=null;
    }
}

TimeTickReceiver的寫法如下

public class TimeTickReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        RemoteViews views=new RemoteViews(context.getPackageName(), R.layout.clock_app_widget);
        SimpleDateFormat sdf=new SimpleDateFormat("HH:mm");
        views.setTextViewText(R.id.now_text, sdf.format(new Date(System.currentTimeMillis())));
        ComponentName cn=new ComponentName(context, ClockAppWidget.class);
        AppWidgetManager manager=AppWidgetManager.getInstance(context);
        manager.updateAppWidget(cn, views);
    }
}

至於Service就只是單純的註冊監聽即可. 不過請注意一下, 在設定檔中要如上<service>標簽

public class ClockService extends Service {
    TimeTickReceiver timeTickReceiver;
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
    @Override
    public void onCreate() {
        if(timeTickReceiver==null)timeTickReceiver=new TimeTickReceiver();
    }
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        registerReceiver(timeTickReceiver, new IntentFilter(Intent.ACTION_TIME_TICK));
        return Service.START_STICKY;
    }
    @Override
    public void onDestroy() {
        unregisterReceiver(timeTickReceiver);
    }
}

完整程式碼下載 : ClockWidget.rar

RemoteViews

上述程式碼中有一個RemoteViews物件, 此物件包含了小工具畫面layout裏的所有元件. 取得RemoteViews物件的建構子有二個參數. 第一個為context.getPackageName(), 是用來取得package的名稱, 第二個參數為R.layout.clock_app_widget, 即畫面配置檔. 取得後, 再使用setTextViewText(), setImageViewBitmap(), setProgressBar(), setViewVisiblity()等方法進行內容的變更

AppWidgetManager

上面利用RemoteViews設定每個元件的內容後, 並沒有馬上更改, 因為小工具可能被安裝了好幾個, 所以到底是要變更那一個呢, 就要使用此管理器來指定變更.
取得AppWidgetManager的方式為AppWidgetManager.getInstance(context). 再使用下面的方法變更內容
appWidgetManager.updateAppWidget(int, RemoteViews) : 變更指定的小工具
appWidgetManager.updateAppWidget(int[], RemoteViews) : 變更陣列中的小工具
appWidgetManager.updateAppWidget(ComponentName, RemoteViews) : 變更所有的小工具

ComponentName的取得方式為new ComponentName(context, ClockAppWidget.class);

小工具元件發送廣播

小工具裏的元件, 也可以送出廣播, 其步驟如下
1. 在 onUpdate() 中使用PendingIntent.getBroadcase()新增PendingIntent 物件, 指定要發送什麼訊號
2. 使用RemoteViews.setOnClickPendingIntent(R.id.xxx, pending)

public class ClockAppWidget extends AppWidgetProvider {
    public static final String ACTION_UPDATE="com.asuscomm.mahaljsp.ClockAppWidget.ACTION_UPDATE";
    static Intent intentService;//一定要使用static
    static void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
                                int appWidgetId) {
        CharSequence widgetText = ClockAppWidgetConfigureActivity.loadTitlePref(context, appWidgetId);
        RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.clock_app_widget);
        views.setTextViewText(R.id.appwidget_text, widgetText);
        SimpleDateFormat sdf=new SimpleDateFormat("HH:mm");
        views.setTextViewText(R.id.now_text, sdf.format(new Date(System.currentTimeMillis())));
        PendingIntent pending=PendingIntent.getBroadcast(context, 0, new Intent(ACTION_UPDATE),
                PendingIntent.FLAG_UPDATE_CURRENT);
        views.setOnClickPendingIntent(R.id.txtRefresh, pending);
        appWidgetManager.updateAppWidget(appWidgetId, views);
    }

發送廣播後, 當然, 就看誰要接收, 就實作onReceiver, 即可

小工具元件啟動Activity

同樣, 在在 onUpdate() 中使用PendingIntent.getActivity()新增PendingIntent 物件, 指定要發送什麼訊號
然後使用RemoteVies.setOnClickPendingIntent(R.id.xxx, pending)進行觸發

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *