25

I have an app that shows a few fragments (of the same type) in a ViewPager and I'm having some trouble with context menu items. (I'm using the support library).

When a context menu item is selected in the context menu in one of the fragments, the wrong fragment is receiving the onContextItemSelected event call.

For example, if I'm on fragment #3 in the pager, the fragment at position #2 receives it instead. If I swipe back to fragment #2, fragment #3 receives the call instead.

I've got a sample here.

(I'm currently working around this in my own app by having a mHandleContext variable in each fragment and enabling/disabling it when the page is changed. That way the onContextItemSelected call will go out to all the fragments until the right one is called.)

Am I doing something wrong or is this a bug with the support library? As a side note, this didn't happen when I was using ActionBarSherlock 3.5.1, which had its own fork of the support library.

Veeti
  • 5,270
  • 3
  • 31
  • 37
  • 2
    possible duplicate of [How to handle onContextItemSelected in a multi fragment activity](http://stackoverflow.com/questions/5297842/how-to-handle-oncontextitemselected-in-a-multi-fragment-activity) – Sergey Glotov Apr 11 '12 at 19:48

5 Answers5

37

So this is some sort of idiotic design decision by Google or something that has just gone totally unconsidered. The simplest way to work around this is to wrap the onContextItemSelected call with an if statement like this:

if (getUserVisibleHint()) {
    // Handle menu events and return true
} else
    return false; // Pass the event to the next fragment

The compatibility library in ActionBarSherlock 3.5 had a hack like this.

Veeti
  • 5,270
  • 3
  • 31
  • 37
  • 1
    @Seraph answer seems to be more correct, as soon as there is an explanation of this behavior. I had the same issue and solved it by using different groupID's (as in his solution) for context menus of different fragments – marwinXXII Dec 01 '12 at 11:02
  • Arnt `hint`s considered to be non-reliable? – Pimp Trizkit Jan 27 '13 at 23:36
  • Theoretically, maybe, but both fragment adapters for ViewPager do set the hints. – Veeti Nov 18 '13 at 04:29
  • Excellent thanks, I wasted my entire day to find a solution for my problem. I thought my Fragments were duplicated somehow and thats why wrong Fragment was getting called, now I know the culprit. Thanks very much, appreciate your answer. – Arif Nadeem Jun 02 '14 at 12:45
  • 1
    If i inflate the menu from XML with MenuInflater I am unable to set groupId for menu item. – Kurovsky Oct 14 '14 at 10:08
  • 1
    I can verify that the getUserVisibleHint() method is NOT reliable. I have logs proving that wrong fragment is being called *sometimes*, but not always. I am using A ViewPager, 3 fragments of same class, viewPager.setOffscreenPageLimit(2), and My FragmentPagerAdapter extends FragmentStatePagerAdapter. – Nerdy Bunz May 31 '16 at 01:17
  • I confirm that sometimes this solution doesn't work. It is better to use menu item's ids. – Borzh Jan 11 '17 at 15:10
  • will this solution also work on onMenuItemClick in Fragments as getUserVisibleHint inside onMenuItemClick always return false – Manish Butola Sep 29 '17 at 11:26
14

It happens because of this:

public boolean dispatchContextItemSelected(MenuItem item) {
    if (mActive != null) {
        for (int i=0; i<mAdded.size(); i++) {
            Fragment f = mAdded.get(i);
            if (f != null && !f.mHidden) {
                if (f.onContextItemSelected(item)) {
                    return true;
                }
            }
        }
    }
    return false;
}

As you can see, FragmentManager calls Fragment.onContextItemSelected for all of his own fragments until it returns true. In your example I can offer such fix:

    public static class TestListFragment extends ListFragment {

    private int mNumber = 0;
    private ArrayList<String> mItems;

    public static TestListFragment newInstance(int number) {
        Bundle args = new Bundle();
        args.putInt("number", number + 1);

        TestListFragment fragment = new TestListFragment();
        fragment.setArguments(args);

        return fragment;
    }

    public TestListFragment() {}

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mNumber = getArguments().getInt("number");
        mItems = new ArrayList<String>();
        mItems.add("I am list #" + mNumber);
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        setListAdapter(new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1, mItems));
        registerForContextMenu(getListView());
    }

    @Override
    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
        super.onCreateContextMenu(menu, v, menuInfo);
        menu.add(mNumber, 0, 0, "Hello, World!");
    }

    @Override
    public boolean onContextItemSelected(MenuItem item) {
        if(item.getGroupId() == mNumber){
            Log.d("ViewPagerContextMenuBug", "onContextItemSelected called for number " + mNumber);
            Toast.makeText(getActivity(), "onContextItemSelected called for number " + mNumber, Toast.LENGTH_SHORT).show();
            return true;
        }
        return false;
    }

}
Seraph
  • 562
  • 1
  • 4
  • 10
  • I tried this and it doesn't seem to work. item.getGroupId() returns 0 no matter what fragment is being long clicked. – Nerdy Bunz May 31 '16 at 01:20
  • It's a great idea to use the groupID for this purpose. That's definetely the best answer. – YBrush Dec 30 '17 at 19:58
1

Oh Google, I mean WTF?

The problem is that onContextItemSelected is very generic, and it is called for each menu item of each fragment.

You can use MenuItem.OnMenuItemClickListener to force fragment menu to not use all onContextItemSelected, but only same function of this fragment.

Use next implementation:

@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
    super.onCreateContextMenu(menu, v, menuInfo);
    MenuInflater inflater = getActivity().getMenuInflater();
    if (v == btnShare) {
        inflater.inflate(R.menu.share_menu, menu);
        for (int i = 0; i < menu.size(); ++i) {
            MenuItem item = menu.getItem(i);
            item.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
                @Override
                public boolean onMenuItemClick(MenuItem item) {
                    onContextItemSelected(item);
                    return true;
                }
            });
        }
    }
}

@Override
public boolean onContextItemSelected(MenuItem item) {
    AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
    switch (item.getItemId()) {
        case R.id.print:
        // ...
    }
}
Borzh
  • 5,069
  • 2
  • 48
  • 64
  • Thanks! That's the only thing that will work if you use `BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT` in your fragment adapter. Also I think it's the cleanest solution. – steliosf Sep 15 '22 at 17:53
1

Using intent for each of the menu items worked well for me.

    @Override  
    public void onCreateContextMenu(ContextMenu menu, View v, ContextmenuInfo menuInfo) {
        super.onCreateContextMenu(menu, v, menuInfo);

        MenuInflater inflater = super.getActivity.getMenuInflater();

        inflater.infalte(R.menu.list_item, menu);

        for(int i = 0; i < menu.size(); i++) {
            MenuItem item = menu.getItem(i);
            Intent intent = new Intent();
            intent.putExtra(KEY_EXTRA_FRAGMENT_ID, this.fragmentId);
            if (item != null) {
                item.setIntent(intent);
            }
        }
    }

    @Override
    public boolean onContextItemSelected(MeniItem item) {

        Intent intent = item.getIntent();

        if (intent != null) {
            if (intent.getIntExtra(KEY_EXTRA_FRAGMENT_ID, -1) == this.fragmentId) {

                // Implement code according the item function.

                return true;
            }
        }

        return super.onContextItemSelected(item);
    }
technik
  • 1,009
  • 11
  • 17
1

The getUserVisibleHint() solution doesn't work for me - it always returns true even when the fragment isn't on screen. The getGroupId() solution doesn't work either when inflating the menu from an XML resource, which is my situation.

It seems that unless Android changes, any solution is always going to be a bit hacky. I create a global variable to store an ID reference for the current fragment in onCreateView. I then pass it to each ContextMenu MenuItem in onCreateContextMenu. When an item is selected, I validate that these two IDs are the same.

private int myFragmentReference;

@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    // Initialisation stuff

    myFragmentReference = 12345;
}

@Override
public void onCreateContextMenu(ContextMenu contextMenu, View v, ContextMenu.ContextMenuInfo contextMenuInfo) {
    // Usual stuff

    int size = contextMenu.size();
    for (int i = 0; i < size; i++) {
        MenuItem menuItem = contextMenu.getItem(i);
        Intent intent = new Intent();
        intent.putExtra("id", myFragmentReference);
        menuItem.setIntent(intent);
    }
}

@Override
public boolean onContextItemSelected(MenuItem menuItem) {
    int id = 0;
    Intent intent = menuItem.getIntent();
    if (intent != null) {
        id = intent.getIntExtra("id", 0);
    }
    if (id == myFragmentReference) {
        // This is the currently displayed fragment
    }
}
alstr
  • 1,358
  • 18
  • 34
  • Thanks, but this solution is a copy of @technik's. Also change `myFragmentReference = 12345;` to `myFragmentReference = new Random().nextInt();`. – CoolMind Jan 19 '19 at 14:49
  • In my use case I needed a specific ID (but obviously not `12345`) to correspond to the data displayed, hence why I did not use a random number, but I can see how that could be useful. Didn't copy @technik's answer but I can see the similarity so credit to them for getting in first. – alstr Jan 20 '19 at 09:45
  • Ah, understood. Excuse me. Nice that you got this solution yourself. If you wish, I will delete the first message. – CoolMind Jan 20 '19 at 10:55
  • Not a problem at all, it will be useful in certain contexts. – alstr Jan 20 '19 at 12:21