Identifying the view selected in a ContextMenu (in onContextItemSelected() method)

Posted / Публикувана 2011-07-12 in category / в категория: Android

Have you ever needed to popup a ContextMenu by longclicking on some of the UI element (different than item in ListView) for example an ImageView instance? At first glance it seems that this can be acomplished quite easy but in reality there is one big problem: you have only one onContextItemSelected() method in your activity that is called every time a context menu item is selected no matter which View requested the ContextMenu via openContextMenu(). This situation is not a problem if just one View is poping a ContextMenu but sometimes you need to popup ContextMenu from several different views. Let me show an example from a real app:

In this layout I have 4 ImageViews. img1 is called "primary" and all other "secondary". By specification I had to create a functionality for secondary images that allows them to set them as primary. Also each image needed to have option to be deleted so I ended up to open ContextMenu on long click for each of the images like this:

When user clicks on menu item onContextItemSelected(MenuItem item) is called. The problem is that there is no way to determine which view opened the menu so you don't know to which element to apply the desired action. When you open context menu from ListView + Adapter there is the following method to obtain such info:

AdapterView.AdapterContextMenuInfo menuInfo = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
ListView lv = (ListView) menuInfo.targetView.getParent();

but there is no way to do something similar when you use View that is not AdapterView (like ImageView, TextView, etc…). There is no way to determine which View opened the menu because item.getMenuInfo() returns null. It turns out that Android developers intentionally did not provided such information in order to discourage opening of ContextMenu by arbitrary UI elements (more about this later in the post). Luckily there is a way to circumvent this limitation. If for example we want to have ImageView with ContextMenuInfo we have to:

1. Subclass ImageView and override getContextMenuInfo() in order to provide additional info:

import android.content.Context;
import android.util.AttributeSet;
import android.view.ContextMenu;
import android.view.View;
import android.view.ContextMenu.ContextMenuInfo;
import android.widget.ImageView;

public class ImageViewWithContextMenuInfo extends ImageView  {
	public ImageViewWithContextMenuInfo(Context context) {
		super(context);
	}

	@Override
	protected ContextMenuInfo getContextMenuInfo() {
		return new ImageViewContextMenuInfo(this);
	}

	public ImageViewWithContextMenuInfo(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
	}

	public ImageViewWithContextMenuInfo(Context context, AttributeSet attrs) {
		super(context, attrs);
	}

	public static class ImageViewContextMenuInfo implements ContextMenu.ContextMenuInfo {
		public ImageViewContextMenuInfo(View targetView) {
			this.targetView = (ImageView) targetView;
		}

		public ImageView targetView;
	}
}

Important things in above code are:

  • overriding getContextMenuInfo() in order to return object of type ImageViewContextMenuInfo
  •  definition of ImageViewContextMenuInfo itself which essentially is just a container for reference to the View itself.

2. Change your layout XML to use your new view class:

You have to replace your ImageViews (only those which open context menu) with ImageViewWithContextMenuInfo using the full path to the class like

<com.ikratko.ogrelab.android.ImageViewWithContextMenuInfo
	android:layout_width="128dp"
	android:layout_height="128dp"
	android:id="@+id/img1" />

3. Get your view in onContextItemSelected():

	public boolean onContextItemSelected(MenuItem item) {
		ImageViewWithContextMenuInfo.ImageViewContextMenuInfo menuInfo = (ImageViewWithContextMenuInfo.ImageViewContextMenuInfo) item.getMenuInfo();
		ImageViewWithContextMenuInfo img = (ImageViewWithContextMenuInfo) menuInfo.targetView;

Now you can use img.getTag() in order to get additional information (that you previously set with setTag()).

 

I have prepared sample project that demonstrates how to wire the things: ContextMenu4All

It has simple layout: 3 image "slots" showing different images. On long click context menu is open that allows you to select new image.



 
Please note:

  • As some people suggest it is not good idea to open context menus using arbitrary views because there is no visual cue that there is such functionality attached. Please read the comment from CommonsWare. However although I totally agree with him I think that there are some cases that using such approach is totally justified. For example in my real app I used it because that it is an inhouse app that will be used by users trained to work with it so there will be no confusion how to use it. If I had to provide additional UI elements for "delete" and "make primary" I would run out of space -- there is simply not enough screen real estate to put additional elements. Also if the missing cue is the problem -- developers can provide it. Small icon of taping finger on top of the image in the bottom right corner (using FrameLayout) will be sufficient. Even better -- it can be animated.

9 Responses to “Identifying the view selected in a ContextMenu (in onContextItemSelected() method)”

  1. Deco Says:

    This has got to be the only website I've found in the past few days that so much as mentions this problem never mind has a working solution and an explanation for why it happens.
    Thanks for taking the time to make this.

  2. Andrey Says:

    Thank you a lot! You have save my time!

  3. Zolf Says:

    thanks a lot for the solution.Can you please guide how can i use it for table row. I have a table and i want to have the Context menu for each table row. how it that look like.appreciate your help. cheers!!

  4. Ogi Bankov / Огнян Банков Says:

    @Zolf
    I guess that you should subclass TableRow as I did with the ImageView above and then use your subclass in the layout.
    BTW, probably you have good reason to use table but I must suggest that you use ListView instead of table if you need to click on rows. ListView will give you additional functionality too like scrolling, etc…

  5. Zolf Says:

    thanks for your feedbacks. In my table I have 4 cols -- sr.no. (TextView),name(TextView),qty(TextView),delete (button).now the reason for using a context menu was to get rid of the delete button from the table col and put it into the context menu. hence give more room to the other 3 cols. But as you recommended to use ListView in place of Table,now my question is can i have 3 cols in a list view and let users add rows.if you know any tutorial on this can you please refer me to it

  6. Ogi Bankov / Огнян Банков Says:

    Here you can find basic example:
    http://stackoverflow.com/questions/4407865/how-to-customize-listview-row-android
    Generally, you need to create separate layout file for your ListView row, then subclass BaseAdapter or ArrayAdapter and override getView() method (where you inflate and populate your row layout).
    For more detailed tutorial(s) I strongly recommend this book: The Busy Coder's Guide to Android Development (http://commonsware.com/Android/). IMHO it is the best android book I've seen.

  7. Zolf Says:

    cheers !!

  8. pavan Says:

    Pavan @(pavanh)

    Very nice tutorial with efficent and detailed explaination, you can also check
    this one context menu

  9. Poilaupat Says:

    I searched a solution for a similar problem for hours. But I always ended on those ListViews…
    Thanks for this great article !