Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Triggering a PopupMenu with buttons inside a ListView item

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;
}
like image 743
ataulm Avatar asked Jun 30 '13 21:06

ataulm


People also ask

What are the two types of popup menus?

PopupMenu - A modal menu that is anchored to a particular view within an activity. The menu appears below that view when it is selected. Used to provide an overflow menu that allows for secondary actions on an item. PopupWindow - A simple dialog box that gains focus when appearing on screen.

Which of the following shows the pop up menu for selecting the items?

Android Context Menu: Android Context Menu is a floating menu that only appears when the user clicks for a long time on an element and useful for elements that affect the selected content or context frame.

What are menus & also explain about pop up menu?

A context menu (also called contextual, shortcut, and pop up or pop-up menu) is a menu in a graphical user interface (GUI) that appears upon user interaction, such as a right-click mouse operation.

What is popup menu in VB?

Popup menus are sometimes referred to as speed menus, right-click menus, or context menus. Popup menus are generally invoked by right-clicking the mouse button. Based on the hover location of the mouse, most applications will display a specific menu.


2 Answers

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).

like image 112
eski Avatar answered Oct 17 '22 13:10

eski


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);
    }

}
like image 1
ataulm Avatar answered Oct 17 '22 13:10

ataulm