寫「香港天晴」的 widget,遇到一些奇怪的情況,特此記下。本篇是之前寫的再修訂一下而完成的。

香港天晴的小工具
現時「香港天晴」有的小工具

Widget 不要做為可修改大小

自從 Android 3.0 以來,Widget 可以設定為自訂大小 (resizeable) ,根據不同的大小顯示不同的資訊,如以下的天氣 widget:

Weather Widget
Android Developers上的 Widget 例子

這樣可以以一個 widget 便代替以上四個 widget,減少一個程式有太多不同的 widget的情況。

可是呢,實際做下去會發現有點問題:

找不到 widget 的實際大小

要根據不同大小顯示不同資料,便首先要獲取現在 widget 的大小資訊,而這只能靠 onAppWidgetOptionsChanged() 獲取 widget 的最小和最大大小,但卻不能知道那大小等於桌面的多少個 cells。就算 android 文件說一個 cells 大約等於 70dp ,再用此計算,也會發覺很多出乎意料的情況。加上 android 有太多的 screen size,在某款手機上需要 4 個 cells 才可顯示的資訊,在其他機上可能只需 3 個 cells。要顯示得好的話要在不同的機款上做大量的測試。

Launcher 對 widget 改變大小的處理

不同 launcher 對 resizeable widget 的操作也有所不同。有些如 android 官方文件所言,會執行 onAppWidgetOptionsCHanged(),但有些卻不會:如 Samsung Galaxy S3 和 Sony Xperia Z。在他們的預設 launcher 上 resize widget 也不會執行 onAppWidgetOptionsChanged(),令 widget 不會因大小改變而更新!

這些問題實在不值得費時間去解決。為「香港天晴」開發了 resizeable widget 後,看到人說 resizeable widget 是以 GridLayoutScrollView 為主的,那樣大小改變了也不影響顯示。

那麼 Google 你拿天氣 widget 當例子是想做什麼啊?

勸大家若不是用 GridLayoutScrollView 做 widget 的話,便不要做可調較大小的 widget 吧。

題外話: 解決 Samsung 某些電話沒有執行 onAppWidgetOptionsChanged() 的方法

RemoteView View ID 一定要存在

要更新 widget,是要透過 ViewIDRemoteViews 中更新的:

RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget); 
views.setTextViewText(R.id.textview1, text_string);

在 ICS 以上,若 textView1R.layout.widget 中沒有定義的話,是不會有任何錯誤的。可是在 Android 2.x 上,若 ID 不存在的話,widget 會直接顯示 Error view,而不會出現任何錯誤訊息。

這著實費了我一番功夫才找到問題所在。

Widget 中不能自訂字型

Activity 中,可以用以下的方法使用其他字型:

Typeface customTypeFace = Typeface.createFromAsset(getAssets(),
"fonts/custom_font.ttf");
((TextView)findViewById(R.id.custom_text_view)).setTypeface(customTypeFace);

可是在 Widget 中,由於只能使用 RemoteViews,沒有 setTypeface。要使用其他非內置的字型時,只能將文字變成 Bitmap,然後在 ImageView 顯示。

問題來了,怎樣知道文字變成圖片後的長度和闊度呢?網上有不同的方法,但都不能因應 font size 而自動調整。幾經研究後,才找到以下的方法:

Paint textPaint = new Paint();
textPaint.setTypeface(typeface);
textPaint.setStyle(Paint.Style.FILL);
textPaint.setTextAlign(Align.LEFT);

Rect bound = new Rect();
textPaint.getTextBounds(text, 0, text.length(), bound);

int imageViewSize = Math.max(Math.abs(bound.top), bound.width());

Bitmap myBitmap = Bitmap.createBitmap(imageViewSize, imageViewSize, Bitmap.Config.ARGB_8888);

Canvas myCanvas = new Canvas(myBitmap);

myCanvas.drawText(text, -bound.left + (imageViewSize - bound.width())/2, -bound.top + (imageViewSize -bound.height())/2, textPaint);

重點是 imageViewSize 的設定和最後 drawText 的 xy 座標。不過以上的字型是正方形的字體,若大家用的為長方形的話,可能又有所不同。

TransactionTooLargeException

「香港天晴」每一個 icon 皆是 ImageView,天氣警告又各自用一個 ImageView

香港天晴的 Widget
所有圖案都是獨立的 ImageView

。若你跟我以前一樣,更新時使用 setImageViewBitmap 的話,或早或遲你也會遲到 TransactionTooLargeException。這是因為更新 RemoteViews的 data 容量最大為 1MB 。 BitMap太大太多的話,便會超過此限定容量而出現 TransactionTooLargeException

解決方法為將 Bitmap 寫進暫存檔案,然後使用 setImageViewUri 來顯示圖片,這樣便不用整個 Bitmap 的資料掉進去 RemoteView,可以有效的減少更新容量。

不過使用此方法也會遇到其他問題,為免文章太長,遲點再另外解釋。

所有 AppWidgetProvider 指向的 class不能共享

在 4.0+ 上可以用 resizeable widget ,但在 android 2.x 上的話只能以幾個不同大小的 widget 來代替。因為基本上不同大小的 widget 的功能是一樣的,所以打算以同一個 class 來做。不過不同的 AppWidgetProvider 不能指向同一個 class ,指向同一個的話,在 launcher 只會有一個相關的 widget 出現。

Android 2.x 的 Widget 列表
Android 2.x 上有多個功能一樣但大小大同的 Widget

解決方法是,extend 幾個 class 來給 AppWidgetProvider 用,只要 class 的名字不同便沒問題。

LockScreen widget 的大小

Android 4.2 以上支援 LockScreen Widget,該用戶在鎖定的畫面上也能顯示 widget 小工具。但要注意的是,widget 在 lockscreen 上的長闊跟桌面上的長闊有所不同,同一個 widget 直接搬到 lockscreen 上未必能正確顯示,所以又要再測試。

Lockscreen widget
同一 widget,左邊的是放在 lockscreen 上,右邊的是放在桌面

另外 lockscreen 上的 widget 只能有兩個大小選擇 (兩個分別就是:大和小),在設計 widget 上也要注意一下。

updatePeriodMillis

不要倚賴 updatePeriodMillis 作更新,因為它的最短的更新時間為 30 分鐘,就算你設定為一分鐘也沒有用。想要更短的更新時間的話只能用 AlarmManager

更新 Widget 時請整個 RemoteView 一起更新

每次更新 RemoteView時,請整個 view 的內容一起更新。不要因為只想改其中一隻字便只更新那個 TextView,只更新其中一個的話,可能在你的測試機上沒事,但一推出後,必定會收到很多報告說 widget 沒有反應沒有顯示等怪問題。

我知道你作為一個追求完美的 programmer,必然不想電腦浪費一分一秒去做沒有用的工作。明明只是改其中一個文字,為何要全部作更新呢?但這是 Android 的方法,請跟從吧。

最後

請細心的閱讀 Android Developer Guide 有關 widget 的部份,遇到問題時,一看再看,很多時便會找到解決方法。

祝你不會浪費太多時間在 widget developement 上。