Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save danielesegato/9829833 to your computer and use it in GitHub Desktop.
Save danielesegato/9829833 to your computer and use it in GitHub Desktop.
Android Option Menu + Fragments Workaround with ViewPager / ActionBar / Drawer, Menu Items not displaying

This gist is a workaround to this bug:

https://code.google.com/p/android/issues/detail?id=29472

No changes are needed to your current code but changing the extended fragment/activity class.

All you have to do is put this two classes in your code and make your activity extend MenuManagerActivity and your fragment MenuDelegatorFragment. If you are using Nested fragment you must make sure that all fragments containing nested Fragments with options menu also extend MenuDelegatorFragment (both if the parent fragment need an option menu both if it doesn't need it).

A suggestion that works very for every app: always, as first thing when you start a project, write a MyFragment and MyActivity class (or Base/Abstract whatever prefix suits you) and extend that with every single fragment/activity you write. It will be easier to handle cases like this.

No change is needed to your actual fragment apart from the extended class. Using a Viewpager, Actionbar Tabs, Navigation Drawer (hamburger menu) on Android pre-4.4 Kitkat made the menu created from the fragments (or nested fragments) unreliable, meaning that switching between pages, changing fragments etc caused the menu not to update in sync with the currently showed fragment.

Resulting in options menu items being visible when their fragment was not or vice-versa.

This workaround delegate all the handling of the option menu to the activity that will call the fragments methods to create the menu.

The fragment side of the thing disable the framework handling and register / unregister the fragment to the activity when the fragment come in / go out from the current view, which cause an invalidate on the menu.

There's also an issue with nested fragments: they may be left resumed and with "hint visibility" = true even if the parent fragment is detached. This code also take care of that situation.

There's a DEBUG flag in the fragment implementation you can use to check what's wrong if in your situation something is not working as you expect.

I don't know if work in 100% of the cases. But it works perfectly fine and reliably for me so I share it for everyone.

Let me know in comments if this worked for you. Suggestions are welcome!

import android.support.v4.app.Fragment;
import android.util.Log;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Fragment with a workaround to handle menus in conjunction with MenuManagerActivity
* <p/>
* To make it work properly you need to assure that all fragments with option menu and that have nested fragments with option menu extend this fragment.
* <p/>
* Apparently when you detach a parent fragment the nested fragment is not detached and actually not even paused, nor the visibility hint is set to false.
* For this reason I had to handle an hierarchy check between fragments making the parent fragment notify children when he is going away and coming back
* and making children ask to be informed when the parent go away.
* <p/>
* I think I handled any fancy configuration now, not 100% sure tough. I'll keep improving if I found bugs. The nested fragment problem was an hard one to
* catch and fix, I wrote a good debug log that can be enabled by just setting a variable to true. Don't worry about performance since when compiling with
* that variable to false the code for logging is effectively removed, not even the if is evaluated.
*/
public class MenuDelegatorFragment extends Fragment {
public static final String LTAG = MenuDelegatorFragment.class.getSimpleName();
private boolean mHasMenu;
private final Set<MenuDelegatorFragment> mChildrenShowingMenu = new HashSet<MenuDelegatorFragment>();
private static final boolean DEBUG = false;
private void debugLog(String what) {
Log.v(LTAG, String.format("%s[%d] (%s): %s", mHasMenu ? "M" : "-", getId(), ((Object) this).getClass().getSimpleName(), what));
}
@Override
public void setHasOptionsMenu(boolean hasMenu) {
if (DEBUG) {
debugLog(String.format("setHasOptionsMenu(%b)", hasMenu));
}
// Note that I'm not calling the super constructor here!
// Cause I'll delegate the menu handling to the MenuManagerActivity instead of letting
// the framework do it!
mHasMenu = hasMenu;
if (mHasMenu && isHierarchyVisible()) {
showMenu();
} else {
hideMenu();
}
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
boolean wasUserVisible = getUserVisibleHint();
super.setUserVisibleHint(isVisibleToUser);
if (wasUserVisible == isVisibleToUser) {
return;
}
if (DEBUG) {
debugLog(String.format("setUserVisibleHint(%b)", isVisibleToUser));
}
if (mHasMenu) {
if (isVisibleToUser && isHierarchyVisible()) {
showMenu();
} else {
hideMenu();
}
}
if (isVisibleToUser) {
notifyChildrenToShowMenu();
} else {
notifyChildrenToHideMenu();
}
}
@Override
public void onPause() {
super.onPause();
if (DEBUG) {
debugLog("onPause()");
}
if (mHasMenu) {
hideMenu();
}
notifyChildrenToHideMenu();
}
@Override
public void onResume() {
if (DEBUG) {
debugLog("onResume()");
}
super.onResume();
if (mHasMenu && isHierarchyVisible()) {
showMenu();
}
notifyChildrenToShowMenu();
}
private boolean isHierarchyVisible() {
boolean visible = getUserVisibleHint() && isResumed();
Fragment parent = getParentFragment();
boolean hierarchyVisible;
if (parent == null) {
hierarchyVisible = visible;
} else if (parent instanceof MenuDelegatorFragment) {
hierarchyVisible = visible && ((MenuDelegatorFragment) parent).isHierarchyVisible();
} else {
Log.w(LTAG, String.format("non-%s in the hierarchy, can't grant menu will work correctly in every situation: %s", LTAG, ((Object) parent).getClass().getSimpleName()));
hierarchyVisible = visible && parent.getUserVisibleHint() && parent.isResumed();
}
if (DEBUG) {
debugLog(String.format("isHierarchyVisible() -> %b", hierarchyVisible));
}
return hierarchyVisible;
}
private void showMenu() {
if (((MenuManagerActivity) getActivity()).registerFragmentForOptionMenu(this)) {
if (DEBUG) {
debugLog("showMenu()");
}
if (getParentFragment() instanceof MenuDelegatorFragment) {
((MenuDelegatorFragment) getParentFragment()).addChildShowingMenu(this);
}
}
}
private void hideMenu() {
if (((MenuManagerActivity) getActivity()).unregisterFragmentForOptionMenu(this)) {
if (DEBUG) {
debugLog("hideMenu()");
}
if (getParentFragment() instanceof MenuDelegatorFragment) {
((MenuDelegatorFragment) getParentFragment()).removeChildShowingMenu(this);
}
}
}
private void parentIsGoingAway() {
if (DEBUG) {
debugLog("parentIsGoingAway()");
}
hideMenu();
}
private void parentIsComingBack() {
if (DEBUG) {
debugLog("parentIsComingBack()");
}
if (mHasMenu && isHierarchyVisible()) {
showMenu();
}
}
private void removeChildShowingMenu(MenuDelegatorFragment child) {
if (mChildrenShowingMenu.remove(child)) {
if (DEBUG) {
debugLog(String.format("removeChildShowingMenu([%s] %s)", child.getId(), ((Object) child).getClass().getSimpleName()));
}
}
}
private void addChildShowingMenu(MenuDelegatorFragment child) {
if (mChildrenShowingMenu.add(child)) {
if (DEBUG) {
debugLog(String.format("addChildShowingMenu([%s] %s)", child.getId(), ((Object) child).getClass().getSimpleName()));
}
}
}
private void notifyChildrenToHideMenu() {
if (mChildrenShowingMenu.size() == 0) {
return;
}
if (DEBUG) {
debugLog("notifyChildrenToHideMenu():" + mChildrenShowingMenu.size());
}
for (MenuDelegatorFragment child : mChildrenShowingMenu) {
child.parentIsGoingAway();
}
mChildrenShowingMenu.clear();
}
private void notifyChildrenToShowMenu() {
List<Fragment> children = getChildFragmentManager().getFragments();
if (children == null || children.size() == 0) {
return;
}
if (DEBUG) {
debugLog("notifyChildrenToShowMenu():" + children.size());
}
for (Fragment child : children) {
if (child instanceof MenuDelegatorFragment) {
((MenuDelegatorFragment) child).parentIsComingBack();
} else if (child.hasOptionsMenu()) {
Log.w(LTAG, String.format("non-%s in the hierarchy, can't grant menu will work correctly in every situation: %s", LTAG, ((Object) child).getClass().getSimpleName()));
}
}
}
}
import android.os.Handler;
import android.os.Looper;
import android.support.v4.app.FragmentActivity;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import java.util.HashSet;
import java.util.Set;
/**
* Activity to handle menu in a custom fashion bypassing the framework bug: https://code.google.com/p/android/issues/detail?id=29472
* To be used in conjunction with MenuDelegatorFragment.
*/
public class MenuManagerActivity extends FragmentActivity {
private boolean mOptionMenuCreated;
private Set<MenuDelegatorFragment> mRegisteredFragments = new HashSet<MenuDelegatorFragment>();
private final Handler handler = new Handler(Looper.getMainLooper());
private boolean mInvalidatePosted;
public boolean registerFragmentForOptionMenu(MenuDelegatorFragment fragment) {
if (mRegisteredFragments.add(fragment)) {
if (mOptionMenuCreated) {
postInvalidateOptionsMenu();
}
return true;
}
return false;
}
public boolean unregisterFragmentForOptionMenu(MenuDelegatorFragment fragment) {
if (mRegisteredFragments.remove(fragment)) {
postInvalidateOptionsMenu();
return true;
}
return false;
}
/**
* Method needed cause subsequents calls of {@link #invalidateOptionsMenu()} on Android pre 4.4
* will get eaten and ignored by the framework.
*/
private void postInvalidateOptionsMenu() {
if (mInvalidatePosted) {
return;
}
mInvalidatePosted = true;
handler.post(new Runnable() {
@Override
public void run() {
mInvalidatePosted = false;
invalidateOptionsMenu();
}
});
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
boolean displayMenu = super.onCreateOptionsMenu(menu);
final MenuInflater menuInflater = new MenuInflater(this);
for (MenuDelegatorFragment f : mRegisteredFragments) {
f.onCreateOptionsMenu(menu, menuInflater);
}
mOptionMenuCreated = true;
return displayMenu;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
boolean displayMenu = super.onPrepareOptionsMenu(menu);
for (MenuDelegatorFragment f : mRegisteredFragments) {
f.onPrepareOptionsMenu(menu);
}
return displayMenu;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// this may be weird behavior: I let the call reach both the activity both any single fragment registered for option menu
boolean handled = super.onOptionsItemSelected(item);
for (MenuDelegatorFragment f : mRegisteredFragments) {
handled |= f.onOptionsItemSelected(item);
}
return handled;
}
@Override
protected void onDestroy() {
super.onDestroy();
mRegisteredFragments.clear();
}
}
@philips77
Copy link

Hi,
I've used somehow simpler solution to solve problem with hiding menus from nested fragments. In my nested fragments I added just one line of code:

    @Override
    public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
        if (getParentFragment().isMenuVisible()) // <- the new line
            inflater.inflate(R.menu.menu, menu);
    }

I'm not sure if it handles all situations but it works for me. If the parent fragment is not visible (in my case the ViewPager is showing another 'main' fragment, the isMenuVisible() returns false. Otherwise it returns true.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment