Android Menu 實作踩坑紀錄


Option Menu、Popup Menu 和 Toolbar Menu 我全都要

還在苦惱 Option Menu、Popup Menu 和 Toolbar Menu 如何使用嗎?這篇全都說給你聽!

Android App 中常見「選單」供使用者進行進階的操作,系統預設的圖示會是 triple dots,也可以在 XML 透過 icon 的屬性指定其他圖示。

<menu xmlns:android="http://schemas.android.com/apk/res/android">    
    <item android:id="@+id/new_game"          
                android:icon="@drawable/ic_new_game"          
                android:title="@string/new_game"          
                android:showAsAction="ifRoom"/>    
    <item android:id="@+id/help"          
                android:icon="@drawable/ic_help"          
                android:title="@string/help" />
</menu>

在 XML 指定好 menu item 後,再來就來認識 Menu 的種類以及如何使用,這篇會介紹 Option Menu、Popup Menu 和 Toolbar Menu,官方的 Menu 還有一種 Context Menu,但還沒用過所以本篇就不獻醜了。

Option Menu

Option Menu 除了在 XML 靜態新增外,也可以透過動態的方式在 onCreateOptionsMenu() 或是 onPrepareOptionsMenu() 的 callback 中新增、移除或是停用 menu item。那如何選擇靜態或是動態的方式管理 menu item 呢?官方建議可以依照 menu item 是否會頻繁異動的情況判定,當 menu item 在同個頁面會依不同觸發條件異動時則應動態設定,反之若不會異動則靜態新增就好。

選擇 menu item 後要進行的動作就交給 onOptionsItemSelected(item: MenuItem) 來!預設的 super.onOptionsItemSelected(item) 會回傳 false,當 Activity 和 Fragment 同時包含 onOptionsItemSelected(item: MenuItem) 時,會優先呼叫 Activity 的 onOptionsItemSelected(),之後才會依 Fragment 新增順序呼叫各 Fragment 的 onOptionsItemSelected(),直到其中一項回傳 true 或是全部的 menu item 都被呼叫完畢。

沒那麼簡單的 Option Menu

原以為已經了解如何新增和管理 menu item,但實作時卻發現沒那麼簡單,原先沒有 menu 的 Fragment 竟然會被有 menu 的 Fragment 給污染,明明只有一個 Fragment 實作了 onCreateOptionsMenu() 的 callback 叫出 menu item,沒想到開啟其他沒複寫 onCreatOptionMenu() 的 Fragment 也跟著出現了前面實作的 menu,而出現的 menu item 正是前一頁實作過的 menu!

上述 Menu 被錯誤 inflate 的原因是 Fragment 對 Menu 只有單向控制力,Fragment 只能對 Menu 呼之即來,卻無法揮之即去,導致一個 Fragment 新增了 Menu 以後,後面開啟的 Fragment 都會跟著出現 Menu。

解法 1 : TargetFragment

如果可以知道 Fragment 間開啟的先後順序,就可以透過 TargetFragment 的方式在先開啟的 FragmentA 新增 menu item 並把 TargetFragment 設定為後開啟的 FragmentB。在 FragmentB 的 onResume 中加上:

getTargetFragment().setMenuVisibility(false);

FragmentA 的 onStop 加上:

getTargetFragment().setMenuVisibility(true);

這個做法的缺點是在有 menu 和沒有 menu 的 Fragment 之間都需要透過 TargetFragment 處理,而兩個有不同 menu 的 Fragment 也需要互相設定 TargetFragment,對於 Fragment 開啟沒有一定順序的情況而言,要處理的情況就好多...。

解法 2 : onPrepareOptionsMenu

在不需使用 menu 的通通 Fragment 加上:

override fun onPrepareOptionsMenu(menu: Menu?) { menu?.clear()}

需要 menu 的 Fragment 則加上:

override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
        menu?.clear()
        inflater?.inflate(R.menu.help_menu, menu)
        super.onCreateOptionsMenu(menu, inflater)
    }

因為原先拿到的 menu 可能會是別人家的 menu,記得要先清掉以免 inflate 到別人的!缺點當然就是麻煩,連不用 menu 的 Fragment 都要加上 onPrepareOptionsMenu,一不小心就會漏加。

解法 3 : ToolbarMenu

是日救星 Toolbar Menu!讓我們繼續看下去。

Toolbar Menu

Toolbar Menu 顧名思義就是長在 Toolbar 上的 menu,因為每個 Fragment 都會有自定義的 Toolbar,因此在 Toolbar 身上加 Menu 就不用擔心沾染到別人的 menu item 或是我的 menu item 產生側漏的問題,使用 Toolbar Menu 也可以好自在好安心,靠 bar 的使用方法如下:

toolbar.inflateMenu(R.menu.help_menu)
toolbar.setOnMenuItemClickListener {
    when (it.itemId) {
        R.id.help -> { }
        else -> false
    }
}

Toolbar Menu 解決了在 Fragment 使用 Option Menu 痛點,也支援 toolbar.menu.add() 的方式動態新增 menu item。至於缺點目前還沒想到,如果有其他靠 bar 後想靠背的朋友歡迎交流XD

Popup Menu

有些需求可能要在 RecyclerView 中新增彈出式選單,這時上述兩種 Menu 都派不上用場,Popup Menu 就決定是你了!不囉唆直接上 code:

val wrapper = ContextThemeWrapper(ApplicationContext(), R.style.PopupMenu)
val popup = PopupMenu(wrapper, anchorView, Gravity.END)
popup.inflate(R.menu.help_menu)
popup.setOnMenuItemClickListener { item ->
    when (item.itemId) {
        R.id.new_game -> { }
        R.id.help -> { }
    }
    false
}
popup.show()

初始化 PopupMenu 需要傳入 Context(wrapper)、對齊的 view (anchorView)、對齊方向(Gravity.END),如果有 menu item 需要顯示或隱藏的話也可以透過 menu.findItem(R.id.help_menu).isVisible = true 控制,Popup Menu 也同樣支援 popup.menu.add() 的方式可以動態新增 menu item,缺點一樣 null。

其實除了 Option Menu 雷一點外其他 Menu 都蠻可人的啊,如果有其他用過上面幾種 Menu 的朋友歡迎留言交流!

#Android #Menu





透過分享開發過程中遇過的一些問題、當中的思考過程和最後的解法,期待能和更多人一起集思廣益!

留言討論