顯示具有 Android 標籤的文章。 顯示所有文章
顯示具有 Android 標籤的文章。 顯示所有文章

星期六, 1月 18, 2014

程式設計師 vs. 新聞記者

Android 的 PopupMenu 不支援顯示 icon。可是很多時候你會想放個 icon 給他... 那怎麼辦呢?

深挖 PopupMenu 的程式碼以後,發現他實際是透過 MenuPopupHelper 這個 class 來顯示實際的 PopupWindow,而該 class 有個 setForceShowIcon() 函式,可以用來強制顯示 icon。

可是... 這個 mPopup 在 PopupMenu 裡是 private field 啊... client code access 不到它... 怎麼辦咧?

在這裡有兩個選擇...

  1. 搞肛的辦法...:把 PopupMenu.java 跟所有用到的 internal class 實作複製一份過來,多新增一個 setForceShowIcon() 方法。
  2. 偷吃步:透過 Java 的 Reflection 機制來偷偷存取該 private field。

當然,我不會在這裡講搞肛的辦法,因為我是怠惰的程式設計師,能偷吃步一定要偷吃步的(怠惰的程式設計師施工原則:自己要維護的程式碼越少越好)。

所以,本來這樣的東西:

PopupMenu menu = ...;
...
menu.mPopup.setForceShowIcon(true);

就變成了...:

public static void hackPopupMenuToShowIcon(PopupMenu popup, boolean showIcon)
{
    try
    {
        // 從所有 field 中找出名稱為 mPopup 的 field。
        Field[] fields = popup.getClass().getDeclaredFields();
        for (Field field : fields)
        {
            if ("mPopup".equals(field.getName()))
            {
                // 設定存取權(本來是 private field 不能存取)
                field.setAccessible(true);
                // 取得 popup 這個 instance 的 mPopup
                Object menuPopupHelper = field.get(popup);
                // 取得 mPopup 的 class
                Class classPopupHelper = Class.forName(menuPopupHelper.getClass().getName());
                // 取得 mPopup 的 class 的 setForceShowIcon() 這個 method
                Method setForceIcons = classPopupHelper.getMethod("setForceShowIcon", boolean.class);
                // invoke 該 method...
                setForceIcons.invoke(menuPopupHelper, showIcon);
                break;
            }
        }
    catch (Exception e)
    {
        e.printStackTrace();
    }
}

// 使用時變成
PopupMenu menu = ...;
...
hackPopupMenuToShowIcon(menu, true);

感覺就像台灣的新聞記者......

  • 馬總統正在公園慢跑。

硬要寫成

  • 馬總統英九先生目前正在一個被稱作公園的地方執行一個慢跑的動作。

星期四, 1月 09, 2014

RenderScript with AndroidStudio

=== 中文版 ===

我只是想 log 一些用 Android Studio 寫 RenderScript 的 gotcha...

  1. 如果你把 <script-name>.rs 放在跟 .java 同一個目錄,grandle 是不會幫你編的... 你得在跟 src、res 同一層開一個新的資料夾叫「rs」,然後把你的 <script-name>.rs 丟進裡面。
  2. 如果你在執行的時候,出現「Invalid RS info file /data/data/<package>/cache/<package>/<script-name>.o.info! (No such file or directory)」,表示你使用的 build-tool 高過執行環境的 API Level。請改用更低的 build-tool...

    具體作法,是去 build.grandle 裡面,把 buildToolsVersion 改低,例如改成 17...(記得要先去 SDK Manager 裡面裝上更低版本的 build tool。)

=== English Version ===

I just want to log some RenderScript gotcha's while developing with Android Studio.

  1. If you just dropped your <script-name>.rs within the same directory with your .java file, grandle won't compile it for you... You've got to make a new directory aloneside src and res, named "rs", and throw your <script-name>.rs over there.
  2. If you encounter "Invalid RS info file /data/data/<package>/cache/<package>/<script-name>.o.info! (No such file or directory)", that means your build-tool is too new to the environment API level. Switch to a lower version build-tool would make it run.

    In practice, edit build.grandle, switch to a lower "buildToolsVersion", for example 17... (don't forget to install a lower version build-tool with SDK Manager first.)

星期四, 3月 08, 2012

_id、_ID 傻傻分不清楚

最近在寫 Android 的 Database 程式的時候,發現點擊 ExpandableListView 的第一層要讓他展開的時候,總是會出現以下的 IllegalStateException:

E/AndroidRuntime(12706): FATAL EXCEPTION: main E/AndroidRuntime(12706): java.lang.IllegalStateException: Couldn't read row 0, col -1 from CursorWindow. Make sure the Cursor is initialized correctly before accessing data from it. E/AndroidRuntime(12706): at android.database.CursorWindow.nativeGetLong(Native Method) E/AndroidRuntime(12706): at android.database.CursorWindow.getLong(CursorWindow.java:515) E/AndroidRuntime(12706): at android.database.AbstractWindowedCursor.getLong(AbstractWindowedCursor.java:75) E/AndroidRuntime(12706): at android.database.CursorWrapper.getLong(CursorWrapper.java:106) E/AndroidRuntime(12706): at android.widget.CursorTreeAdapter$MyCursorHelper.getId(CursorTreeAdapter.java:436) E/AndroidRuntime(12706): at android.widget.CursorTreeAdapter.getGroupId(CursorTreeAdapter.java:191) E/AndroidRuntime(12706): at android.widget.ExpandableListConnector.getItemId(ExpandableListConnector.java:421) E/AndroidRuntime(12706): at android.widget.AdapterView.getItemIdAtPosition(AdapterView.java:756) E/AndroidRuntime(12706): at android.widget.AdapterView.setSelectedPositionInt(AdapterView.java:1128) E/AndroidRuntime(12706): at android.widget.AbsListView.onTouchEvent(AbsListView.java:3147) E/AndroidRuntime(12706): at android.view.View.dispatchTouchEvent(View.java:5541) E/AndroidRuntime(12706): at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:1951) E/AndroidRuntime(12706): at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1712) E/AndroidRuntime(12706): at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:1957) E/AndroidRuntime(12706): at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1726) E/AndroidRuntime(12706): at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:1957) E/AndroidRuntime(12706): at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1726) E/AndroidRuntime(12706): at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:1957) E/AndroidRuntime(12706): at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1726) E/AndroidRuntime(12706): at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:1957) E/AndroidRuntime(12706): at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1726) E/AndroidRuntime(12706): at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:1957) E/AndroidRuntime(12706): at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1726) E/AndroidRuntime(12706): at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:1957) E/AndroidRuntime(12706): at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1726) E/AndroidRuntime(12706): at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:1957) E/AndroidRuntime(12706): at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1726) E/AndroidRuntime(12706): at com.android.internal.policy.impl.PhoneWindow$DecorView.superDispatchTouchEvent(PhoneWindow.java:1912) E/AndroidRuntime(12706): at com.android.internal.policy.impl.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1371) E/AndroidRuntime(12706): at android.app.Activity.dispatchTouchEvent(Activity.java:2364) E/AndroidRuntime(12706): at com.android.internal.policy.impl.PhoneWindow$DecorView.dispatchTouchEvent(PhoneWindow.java:1860) E/AndroidRuntime(12706): at android.view.View.dispatchPointerEvent(View.java:5721) E/AndroidRuntime(12706): at android.view.ViewRootImpl.deliverPointerEvent(ViewRootImpl.java:2890) E/AndroidRuntime(12706): at android.view.ViewRootImpl.handleMessage(ViewRootImpl.java:2466) E/AndroidRuntime(12706): at android.view.ViewRootImpl.processInputEvents(ViewRootImpl.java:845) E/AndroidRuntime(12706): at android.view.ViewRootImpl.handleMessage(ViewRootImpl.java:2475) E/AndroidRuntime(12706): at android.os.Handler.dispatchMessage(Handler.java:99) E/AndroidRuntime(12706): at android.os.Looper.loop(Looper.java:137) E/AndroidRuntime(12706): at android.app.ActivityThread.main(ActivityThread.java:4424) E/AndroidRuntime(12706): at java.lang.reflect.Method.invokeNative(Native Method) E/AndroidRuntime(12706): at java.lang.reflect.Method.invoke(Method.java:511) E/AndroidRuntime(12706): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:784) E/AndroidRuntime(12706): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:551) E/AndroidRuntime(12706): at dalvik.system.NativeStart.main(Native Method)

他唯一比較算是「錯誤訊息」的是「Couldn't read row 0, col -1 from CursorWindow. Make sure the Cursor is initialized correctly before accessing data from it.」這個,是表示說程式試圖存取某個 column,可是 query 中沒有這個欄位,所以會回傳 -1。於是乎,程式就這麼爆炸了。

可是... 他只告訴我「col -1」,沒告訴我他到底想查哪個欄位啊... = = 所以只好去追程式碼,發現主要是這個東西「android.widget.CursorTreeAdapter.getGroupId()」會試著抓「_id」這欄位。

SQLite 在 create table 的時候,會自動幫你建立叫做「_id」的欄位作為索引。但是如果你的 create table statement 中有包含某個 integer 型態的 primary key,他就會改用這個欄位來索引(就是不會再多建一個「_id』)了。

Android 的 Cursor 會拿這個欄位作為內部索引,會試圖存取它。當找不到這個欄位的時候,就會丟出 IllegalStateException。

可是呢,我很犯賤的在 create table 的時候用了 _ID 作為 primary key,於是即使 query 是「SELECT `_id` FROM `table_name`;」,他回傳的資料中還是會以 _ID 作為欄位名稱...

結論就是... create table 的時候得用「_id」,用「_Id」、「_iD」、「_ID」統統不行... XD~