15

Types of Menu

Let me start by outlining the distinction between Contextual Menus and Popup Menus (taken from here):

  • A PopupMenu is a modal menu anchored to a View

  • A contextual menu [I'm talking specifically about floating context menus] offers actions that affect a specific item or context frame in the UI. You can provide a context menu for any view, but they are most often used for items in a ListView, GridView, or other view collections in which the user can perform direct actions on each item.

As guidance, the docs distinguish between their uses: - PopupMenu: "Providing an overflow-style menu for actions that relate to specific content (such as Gmail's email headers..." - ContextualMenu: "For actions that affect selected content..."

I'm after the visual look and interaction of the PopupMenu.

My setup (and goal)

I have a ListView where each item has a button on the right.

--------------------
| Item 1        [ ]|
--------------------
--------------------
| Item ...      [ ]|
--------------------
--------------------
| Item n        [ ]|
--------------------

while retaining the onItemClick for each ListItem, I'd like to leverage the button to trigger a PopupMenu.

The actions in the PopupMenu are listed in my_menu.xml, so the only difference between each instance of the menu is the item that it was clicked for.

What I've tried

I've tried overriding getView() in my ListAdapter to add an OnClickListener for each button, as per this post. The result was inconsistent; not all the clicks were registering (perhaps due to recycling - I haven't tried this fix yet) but in general the approach seemed squiffy, considering the ease of specifying a context menu for a listview.

I've also tried adding a contextual menu, which was dead easy, except that it's only triggered when the list item is long-pressed.

Is the getView() method the way to go or is there something easier?

In my listview, I've a mini overflow button for each item, similar to each track in playlist for Play Music. They display a PopupMenu for each item there, when you click on the overflow button. Long-press does nothing.

I'd like that but not sure how? The menu page only elaborates how to enable a popup menu for individual views.

registerForContextMenu(ListView lv) is the only thing I've seen that I can apply to an entire ListView, but that seems to only work on long-presses of the entire list. Is there a way to hook that event to a click of a specific view in a list item (whilst still maintaining the list item click for the rest of the list item)?

I've tried setting an onClickListener in getView() as described here but it feels squiffy for PopupMenu, and not all clicks were registering every time (it was consistently inconsistent).

Update - with getView() overridden again

When I create the StacksCursorAdapter, I pass an instance of StackViewFragment which is stored as a field (so it can be assigned to views as the click listener).

ViewHolder is a class with public final ImageView miniOverflow

StacksCursorAdapter (extends SimpleCursorAdapter):

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    View row = super.getView(position, convertView, parent);
    ViewHolder holder = (ViewHolder) row.getTag();

    if (holder == null) {
        holder = new ViewHolder(row);
        row.setTag(holder);
    }
    holder.miniOverflow.setTag(R.id.tag_stack_position, position);
    holder.miniOverflow.setOnClickListener(listener);

    return row;
}

StackViewFragment (extends ListFragment, implements View.OnClickListener):

@Override
public void onClick(View v) {
    Log.d(TAG, "mini overflow clicked");
    Log.d(TAG, "position:::::" + v.getTag(R.id.tag_stack_position));

    PopupMenu popup = new PopupMenu(getActivity(), v);
    MenuInflater inflater = popup.getMenuInflater();
    inflater.inflate(R.menu.menu_stackview_listitem, popup.getMenu());
    popup.show();
}

stack_list_item.xml:

<?xml version="1.0" encoding="utf-8"?>
<!-- a single row item for a Stacks listview -->
<RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="48dp"
        android:descendantFocusability="blocksDescendants"
        >

    ...
    <other views>
    ...


    <ImageView
            android:id="@+id/moreoverflow_btn"
            android:focusable="false"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:alpha="0.4"
            android:scaleType="fitCenter"
            android:src="@drawable/ic_menu_moreoverflow_black"
            android:layout_alignParentRight="true"
            />
</RelativeLayout>

The issue at this junction is that I have to click several times occasionally to get the click to register (on the button). This is without scrolling, so it's not related to view recycling.

Concluding (solution by eskimoapps.com marked as correct)

So there was no issue with the multiple clicks except that my touch target was not in the place I was expecting - the image asset I was using for the mini overflow is weighted to the right (such that the three dots are near the right edge of the touch target) meaning that I was just missing it half the time (facepalm).

Overriding getView() as I showed above seems to work fine, and in fact, getView() can be modified even further, by setting the OnClickListener only if it's needed (but definitely, updating the tag needs to be done for every view, not just the newly created ones):

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    View row = super.getView(position, convertView, parent);
    ViewHolder holder = (ViewHolder) row.getTag();

    if (holder == null) {
        holder = new ViewHolder(row);
        row.setTag(holder);
        holder.miniOverflow.setOnClickListener(listener);
    }
    holder.miniOverflow.setTag(R.id.tag_stack_position, position);

    return row;
}
Community
  • 1
  • 1
ataulm
  • 15,195
  • 7
  • 50
  • 92

2 Answers2

3

Overriding getView is the right approach, and you're on the right track thinking that recycling views will complicate things (if you are in fact recycling views).

Since there's no getOnClickListener methods you'll just have to set a new OnClickListener for the recycled views as well as the uninflated ones (if you don't want to make a custom view that implements getOnClickListener).

eski
  • 7,917
  • 1
  • 23
  • 34
  • if a view has been recycled, could I assume it's still got an OnClickListener, so only set it for new ones? This sounds like a comment I should just try.. :P – ataulm Jun 30 '13 at 21:57
  • It still has an OnClickListener, just not the right one which is why you need to set a new one. – eski Jun 30 '13 at 21:58
  • I don't understand - I'm adding my ListFragment as the listener for each item; that is, there's only one. edit: that said, I just tried it, and it wasn't firing for any list item. – ataulm Jun 30 '13 at 22:02
  • Then there shouldn't be an issue if the position of the view that is clicked doesn't matter, and you wouldn't need to set a new OnClickListener for recycled views. – eski Jun 30 '13 at 22:04
  • Do you still have the onItemClick set? It could be intercepting things, can you show your code? – eski Jun 30 '13 at 22:08
  • A sheepish realisation will be noted in my answer about the inconsistent clicks. Thanks for your help - overriding getView() works as I hoped (now I know the other "issue"!) – ataulm Jun 30 '13 at 22:27
  • 1
    That's good, I was having trouble spotting any issues with your code – eski Jun 30 '13 at 22:30
1

If I wanted to do this today, I'd use the HolderView pattern.

The HolderView pattern requires a custom view/viewgroup (a class extending the View/Viewgroup that is the root of your list/grid item layout, with the 3 standard view constructors).

The general idea is that the adapter is not responsible for updating the contents of the view directly; instead, it will give the view all the necessary information to update itself:

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    if (convertView == null) {
        convertView = createView(parent);
    }

    updateView((GalleryItemView) convertView, position);

    return convertView;
}

private View createView(ViewGroup parent) {
    LayoutInflater inflater = LayoutInflater.from(parent.getContext());

    return inflater.inflate(R.layout.gallery_item_view, null, false);
}

private void updateView(GalleryItemView view, int position) {
    GalleryElement item = getItem(position);

    view.updateWith(position, item, listener);
}

In my view:

public class GalleryItemView extends ImageView {

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

    void updateWith(int position, final GalleryElement item, final GalleryAdapter.GalleryItemListener listener) {
        if (position % 2 == 0) {
            setBackgroundColor(getColor(android.R.color.holo_orange_dark));
        } else {
            setBackgroundColor(getColor(android.R.color.holo_orange_light));
        }

        setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                listener.onGalleryItemClicked(item.id);
            }
        });
    }

    @ColorInt
    private int getColor(@ColorRes int colorRes) {
        return getResources().getColor(colorRes);
    }

}
ataulm
  • 15,195
  • 7
  • 50
  • 92