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







你可能感興趣的文章

[團隊協作] Simple Twitter 專案後端開發

[團隊協作] Simple Twitter 專案後端開發

我的第一堂 - JavaScript 03 迴圈、函式

我的第一堂 - JavaScript 03 迴圈、函式

1. 全域變數 & 全域屬性

1. 全域變數 & 全域屬性






留言討論




uuko Feb 25, 2020

想請問一個問題,該怎麼分辨使用popMenu或是Dialog的時機呢? 因為我基本上都是使用dialog取代popMenu,想問看看大大的意見

terricom Feb 25, 2020

嗨嗨~感謝你的提問!
我自己的看法是 Dialog 是會中斷用戶當前的行為,用戶必須要去選擇 OK 或是 Cancel 讓 Dialog 功成身退才能 Dismiss 繼續當前的操作,例如處理 API 的 Response 可以跳 Dialog 讓用戶收到成功或失敗的訊息。 Popup Menu 其實就跟其他 Menu 一樣是輔助的功能,例如處理 更多 的選項,因此不是必要的操作,會由用戶自己啟動且不會中斷當前的操作。
如果有其他見解也歡迎分享給我喔!

uuko Feb 26, 2020

好的,謝謝您 期待你更多的文章>///<
那想請問您覺得怎麼樣的資料適合用SQLITE+ROOM (room應該是用在sqlite對吧?
因為我在想如果是小資料的話 像是我文章的todolist是不是不應該用firebase應該要存在sqlite的地方嗎
還是我應該要自己寫後端來串會更好QQ
抱歉,問題有點多QAQ

terricom Feb 26, 2020

感謝你的鼓勵 >< 難得找到 Android 系列文,一起加油!
我覺得本地儲存的場景是處理暫存的資料,例如購物車未結帳的資訊。
如果是 TodoList 的話當然可以用 Room 儲存,只是就無法綁定帳號跨裝置使用,或是清理本地的 Storage 後也無法使用了,可以看需求決定是否需要把資料儲存在遠端,或是還沒同步到遠端的部分資料先用 Room 暫存也行。
能自己寫後端的話真的是太神了!換作我的話二話不說直接接 Firebase,我就廢XD



yaerse May 03, 2021

感恩您的分享,此方式若遇到需要在Fragment中全域存取Menu中的每個Item的話,似乎會有問題
反而是使用toolbar.menu.add() 的方式動態新增 menu item
也就是

private lateinit var menuHelp : MenuItem
menuHelp = toolbar.menu.add(R.string.help)

然後去在Fragmen中四處操作menuHelp

以上淺見,若有更好的方式,請不吝賜教