小工具
Android提供一種特別的元件AppWidget, 可以讓使用者在桌面上直接瀏覽資料, 或是執行一些簡單的操作. 例如在桌面上顯示時間, 行事曆或氣候資訊, 這種元件稱為小工具元件.
建立時鐘小工具
底下以一個時鐘小工具進行說明. 開啟新專案後, 選取Activity時選用 Add no Activity. 然後於App按右鍵/New/Widget/AppWidget, 然後依下面圖示說明修改.
此專案因為主Activity, 所以在工具列的App會有x的錯誤, 需點選Edit Configuration/Launch選Nothing
小工具畫面設計
小工具畫面可用的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>
上述編譯安裝後, 即可將小工具放於桌面上
更新畫面
小工具的專用設定檔中, 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)進行觸發