How to handle onContextItemSelected in a multi fragment activity?

AndroidContextmenuAndroid FragmentsAndroid Support-Library

Android Problem Overview


I'm currently trying to adapt my application to use the "Compatibility Libraries for Android v4" to provide the benefits of the usage of fragments even to Android 1.6 users.

The implementation of a context menu seems to be tricky:

  • The main activity of the application is extending the FragmentActivity class.
  • The fragments are all based on one class which extends the Fragment class.
  • The fragment class is calling registerForContextMenu() in its onCreateView() method and overrides the methods onCreateContextMenu() and onContextItemSelected().

For onCreateContextMenu() this works pretty well. The context menu is inflated from a resource file and slightly modified based on the selected item (which is based on a listView... even if the fragment is not an ListFragment).

The problem occurs when a context menu entry is selected. onContextItemSelected() is called for all currently existing fragments starting with the first added one.

In my case the fragments are used to show the content of a folder structure. When the context menu of a subfolder fragment is opened and a menu item is selected, onContextItemSelected() is first called on the upper levels (depending on how many fragments are allowed/visible in this moment).

Right now, I use a workaround by a field on activity level which holds the tag of last fragment calling its onCreateContextMenu(). This way I can call "return super.onContextItemSelected(item)" in the begin of onContextItemSelected() when the stored tag is not the same as getTag(). But this approach looks a bit dirty to me.

Why is onContextItemSelected() called on all fragments? and not just one the one that was calling onCreateContextMenu()?

What is the most elegant way to handle this?

Android Solutions


Solution 1 - Android

I'll post an answer even though you found a workaround because I just dealt with a similar issue. When you inflate the context menu for a specific fragment, assign each menu item a groupId that is unique to the fragment. Then test for the groupId in 'onContextItemSelected.' For Example:

public void onCreateContextMenu(ContextMenu menu, View v,ContextMenuInfo menuInfo) {
    menu.add(UNIQUE_FRAGMENT_GROUP_ID, MENU_OPTION_1, 0, R.string.src1);
    menu.add(UNIQUE_FRAGMENT_GROUP_ID, MENU_OPTION_2, 0, R.string.src2);
}
public boolean onContextItemSelected(MenuItem item) {
    //only this fragment's context menus have group ID of -1
    if (item.getGroupId() == UNIQUE_FRAGMENT_GROUP_ID) {
    	switch(item.getItemId()) {
        case MENU_OPTION_1: doSomething(); break;
        case MENU_OPTION_2: doSomethingElse(); break;
    }
}

This way all of your fragments will still receive calls to 'onContextItemSelected,' but only the correct one will respond, thus avoiding the need to write activity-level code. I assume a modified version of this technique could work even though you aren't using 'menu.add(...)'

Solution 2 - Android

Another one solution:

@Override
public boolean onContextItemSelected(MenuItem item) {
    if (getUserVisibleHint()) {
        // context menu logic
        return true;
    }
    return false;
}

Based upon this patch from Jake Wharton.

Solution 3 - Android

I liked the simple solution by Sergei G (based on Jake Wharton fix), but inverted because it is easier to add to several fragments:

public boolean onContextItemSelected(android.view.MenuItem item) 
{  
    if( getUserVisibleHint() == false ) 
    {
  	    return false;
    }

    // The rest of your onConextItemSelect code
    AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
 }

After that, the code same as it was before.

Solution 4 - Android

I found out a very easy solution. As onCreateContextMenu() is called every time the ContextMenu is created I set a boolean variable to true.

public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
	super.onCreateContextMenu(menu, v, menuInfo);
	MenuInflater inflater = getActivity().getMenuInflater();
	inflater.inflate(R.menu.film_menu, menu);
	bMenu=true;
}

The only other thing I have to do is ask for that variable OnContextItemSelected()

public boolean onContextItemSelected(MenuItem item) {
	if (bMenu) {
		bMenu=false;
    	if (item.getItemId() == R.id.filmProperties) {
    		///Your code
    		return true;
    	} else {
    		return super.onContextItemSelected(item);
    	}
	} else {
		return super.onContextItemSelected(item);
	}
}

That's it.

Solution 5 - Android

I found an alternative. It does not change anything on my problem above, but it makes it pointless.

I have remove the context menu completely from my application. Instead I capture the longclick on a list item and change the visible buttons of the action bar in this moment. From the user point of view this is much more tablet like as a context menu.

In backward compatible applications the actionbar does not exist. So I've decided to build my own (kind of toolbar on top) for the devices pre Honeycomb.

If you would like to stay with the context menu, I did not find a better solution as the workaround I've mentioned above.

Solution 6 - Android

In my first fragment, i have set all my menu id > 5000 so, as first line of code of onContextItemSelected of first fragment i have

if (item.getItemId() < 5000) return false;

and the second fragment will be invoked.

Solution 7 - Android

If you are using adapters with listviews in your fragment this might help.

public boolean onContextItemSelected(final MenuItem item) {
   	final AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();

    //Check if the context menu call came from the list in this fragment (needed for support for multiple fragments in one screen)
    if (info.targetView.getParent() != getView().findViewById(android.R.id.list))
    	return super.onContextItemSelected(item);

    //Handle context menu item call
    switch (item.getItemId()) {
        ...
    }
}

Solution 8 - Android

Just change

 @Override
    public boolean onContextItemSelected(MenuItem item) {
    return true;
 }
 

to

@Override
    public boolean onContextItemSelected(MenuItem item) {
    return super.onContextItemSelected(item); 
 }

and will work great!!!

Solution 9 - Android

IMHO we may just check if target view is child of fragment listview. It is very simple and work for me well. I just added to all my fragments:if (getListView.getPositionForView(info.targetView) == -1) return false when migrate from older API

This is example from one of my parent fragments. This is Scala, but I hope you got an idea.

@Loggable
override def onContextItemSelected(menuItem: MenuItem): Boolean = {
  for {
    filterBlock <- TabContent.filterBlock
    optionBlock <- TabContent.optionBlock
    environmentBlock <- TabContent.environmentBlock
    componentBlock <- TabContent.componentBlock
  } yield menuItem.getMenuInfo match {
  case info: AdapterContextMenuInfo =>
    if (getListView.getPositionForView(info.targetView) == -1)
      return false
    TabContent.adapter.getItem(info.position) match {
      case item: FilterBlock.Item =>
        filterBlock.onContextItemSelected(menuItem, item)
      case item: OptionBlock.Item =>
        optionBlock.onContextItemSelected(menuItem, item)
      case item: EnvironmentBlock.Item =>
        environmentBlock.onContextItemSelected(menuItem, item)
      case item: ComponentBlock.Item =>
        componentBlock.onContextItemSelected(menuItem, item)
      case item =>
        log.debug("skip unknown context menu item " + info.targetView)
        false
    }
  case info =>
    log.fatal("unsupported menu info " + info)
    false
  }
} getOrElse false

P.S. If you trace calls of onContextItemSelected(...) you may notify that super.onContextItemSelected(item) return always false. Valid onContextItemSelected invoked AFTER, not WITHIN. So super.onContextItemSelected(item) is useless and I replaced it with false.

Solution 10 - Android

I found an easier solution than the exposed:

public boolean onContextItemSelected(MenuItem item) {
    ListView yourList = (ListView) (ListView) getView().findViewById(R.id.yourList);
   
	if (!yourList.hasFocus())
		return false;

    switch(item.getItemId()) {
        ...
    }
}

Solution 11 - Android

In the method changed return true; to return super.onContextItemSelected(item); in my onContextItemSelected() override and everything started working.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionLars K.View Question on Stackoverflow
Solution 1 - AndroidRich EhmerView Answer on Stackoverflow
Solution 2 - AndroidSergey GlotovView Answer on Stackoverflow
Solution 3 - AndroidazelezView Answer on Stackoverflow
Solution 4 - AndroidRawpalView Answer on Stackoverflow
Solution 5 - AndroidLars K.View Answer on Stackoverflow
Solution 6 - AndroidfrussoView Answer on Stackoverflow
Solution 7 - Androiduser1103538View Answer on Stackoverflow
Solution 8 - AndroidAmit HoodaView Answer on Stackoverflow
Solution 9 - AndroidEzhikView Answer on Stackoverflow
Solution 10 - AndroidFederico QuevedoView Answer on Stackoverflow
Solution 11 - AndroidAli ImranView Answer on Stackoverflow