開發 Android Widget 遇到的問題
寫「香港天晴」的 widget,遇到一些奇怪的情況,特此記下。本篇是之前寫的再修訂一下而完成的。
Widget 不要做為可修改大小
自從 Android 3.0 以來,Widget 可以設定為自訂大小 (resizeable) ,根據不同的大小顯示不同的資訊,如以下的天氣 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 是以 GridLayout
、ScrollView
為主的,那樣大小改變了也不影響顯示。
那麼 Google 你拿天氣 widget 當例子是想做什麼啊?
勸大家若不是用 GridLayout
或 ScrollView
做 widget 的話,便不要做可調較大小的 widget 吧。
題外話: 解決 Samsung 某些電話沒有執行 onAppWidgetOptionsChanged()
的方法
RemoteView View ID 一定要存在
要更新 widget,是要透過 ViewID
在 RemoteViews
中更新的:
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget);
views.setTextViewText(R.id.textview1, text_string);
在 ICS 以上,若 textView1
在 R.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
。
。若你跟我以前一樣,更新時使用 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 出現。
解決方法是,extend
幾個 class
來給 AppWidgetProvider
用,只要 class
的名字不同便沒問題。
LockScreen widget 的大小
Android 4.2 以上支援 LockScreen Widget,該用戶在鎖定的畫面上也能顯示 widget 小工具。但要注意的是,widget 在 lockscreen 上的長闊跟桌面上的長闊有所不同,同一個 widget 直接搬到 lockscreen 上未必能正確顯示,所以又要再測試。
另外 lockscreen 上的 widget 只能有兩個大小選擇 (兩個分別就是:大和小),在設計 widget 上也要注意一下。
updatePeriodMillis
不要倚賴 updatePeriodMillis
作更新,因為它的最短的更新時間為 30 分鐘,就算你設定為一分鐘也沒有用。想要更短的更新時間的話只能用 AlarmManager
。
更新 Widget 時請整個 RemoteView
一起更新
每次更新 RemoteView
時,請整個 view
的內容一起更新。不要因為只想改其中一隻字便只更新那個 TextView
,只更新其中一個的話,可能在你的測試機上沒事,但一推出後,必定會收到很多報告說 widget 沒有反應沒有顯示等怪問題。
我知道你作為一個追求完美的 programmer,必然不想電腦浪費一分一秒去做沒有用的工作。明明只是改其中一個文字,為何要全部作更新呢?但這是 Android 的方法,請跟從吧。
最後
請細心的閱讀 Android Developer Guide 有關 widget 的部份,遇到問題時,一看再看,很多時便會找到解決方法。
祝你不會浪費太多時間在 widget developement 上。