带有图标的弹出菜单

74

当然,这里我们处理的是SDK 11及以上版本。

我打算做类似于这样的事情: 在此输入图像描述

在那个PopupMenu 的每个项目旁边,我想放置一个图标

我创建了一个XML文件,并将其放置在/menu目录下:

<menu xmlns:android="http://schemas.android.com/apk/res/android" >

    <item
        android:id="@+id/action_one"
        android:title="Sync"
        android:icon="@android:drawable/ic_popup_sync"
        />

    <item
        android:id="@+id/action_two"
        android:title="About"
        android:icon="@android:drawable/ic_dialog_info"
        />
</menu>

正如你所注意到的,在xml文件中,我定义了我想要的图标,但是当弹出菜单显示时,它们显示为没有图标。我应该怎么做才能使这两个图标出现?


我认为这是实现这种功能最简单的方法: https://dev59.com/qoPba4cB1Zd3GeqPsXa4#33487225 - Anton
12个回答

111

如果您使用AppCompat v7,这种方法可以奏效。它有点“hacky”,但比使用反射要好得多,并且可以使您仍然使用核心Android PopupMenu:

这种方法适用于使用AppCompat v7的情况。尽管有些取巧,但比使用反射要好得多,并且还能够继续使用Android的核心弹出菜单。

PopupMenu menu = new PopupMenu(getContext(), overflowImageView);
menu.inflate(R.menu.popup);
menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { ... });

MenuPopupHelper menuHelper = new MenuPopupHelper(getContext(), (MenuBuilder) menu.getMenu(), overflowImageView);
menuHelper.setForceShowIcon(true);
menuHelper.show();

res/menu/popup.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:id="@+id/menu_share_location"
        android:title="@string/share_location"
        android:icon="@drawable/ic_share_black_24dp"/>

</menu>
此操作将导致弹出菜单使用在您的菜单资源文件中定义的图标:

enter image description here


2
很棒的解决方案!小提示:您甚至不需要像您所做的那样创建一个PopupMenu实例。只需创建一个新的MenuBuilder()将菜单填充到其中并将其传递给助手即可。 - david.schreiber
1
@david.schreiber 很好的发现 - 我从我的代码中删除了一些行,所以这个被忽略了。实际上我创建了 PopupMenu,这样我就可以使用 setonMenuItemClickListener()。我会将此添加到示例中,因为我相信大多数开发人员都想知道何时单击了他们的菜单项! - Stephen Kidson
2
@IgniteCoders 使用来自v7支持库的MenuBuilder。因此,请更改您的导入。 - Jeffrey
2
@IgniteCoders 为了解决 ClassCastException,将 import android.widget.PopupMenu; 替换为 import android.support.v7.widget.PopupMenu; - Mr-IDE
2
一个类似的解决方案实际上在https://www.material.io/components/menus/android#adding-icons-on-popup-menus中被建议,但是警告是这个API受限制可能在未来无法使用。 - antanas_sepikas
显示剩余8条评论

45

我会以另一种方式来实现:

创建一个名为 PopUpWindow 的布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/llSortChangePopup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/sort_popup_background"
android:orientation="vertical" >

<TextView
    android:id="@+id/tvDistance"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@string/distance"
    android:layout_weight="1.0"
    android:layout_marginLeft="20dp"
    android:paddingTop="5dp"
    android:gravity="center_vertical"
    android:textColor="@color/my_darker_gray" />

<ImageView
    android:layout_marginLeft="11dp"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="@drawable/sort_popup_devider" 
    android:contentDescription="@drawable/sort_popup_devider"/>

<TextView
    android:id="@+id/tvPriority"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@string/priority"
    android:layout_weight="1.0"
    android:layout_marginLeft="20dp"
    android:gravity="center_vertical"
    android:clickable="true"
    android:onClick="popupSortOnClick"
    android:textColor="@color/my_black" />


<ImageView
    android:layout_marginLeft="11dp"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="@drawable/sort_popup_devider" 
    android:contentDescription="@drawable/sort_popup_devider"/>

<TextView
    android:id="@+id/tvTime"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@string/time"
    android:layout_weight="1.0"
    android:layout_marginLeft="20dp"
    android:gravity="center_vertical"
    android:clickable="true"
    android:onClick="popupSortOnClick"
    android:textColor="@color/my_black" />

<ImageView
    android:layout_marginLeft="11dp"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="@drawable/sort_popup_devider" 
    android:contentDescription="@drawable/sort_popup_devider"/>

<TextView
    android:id="@+id/tvStatus"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@string/status"
    android:layout_weight="1.0"
    android:layout_marginLeft="20dp"
    android:gravity="center_vertical"
    android:textColor="@color/my_black" 
    android:clickable="true"
    android:onClick="popupSortOnClick"
    android:paddingBottom="10dp"/>

 </LinearLayout>

并且在你的Activity中创建PopUpWindow

    // The method that displays the popup.
private void showStatusPopup(final Activity context, Point p) {

   // Inflate the popup_layout.xml
   LinearLayout viewGroup = (LinearLayout) context.findViewById(R.id.llStatusChangePopup);
   LayoutInflater layoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
   View layout = layoutInflater.inflate(R.layout.status_popup_layout, null);

   // Creating the PopupWindow
   changeStatusPopUp = new PopupWindow(context);
   changeStatusPopUp.setContentView(layout);
   changeStatusPopUp.setWidth(LinearLayout.LayoutParams.WRAP_CONTENT);
   changeStatusPopUp.setHeight(LinearLayout.LayoutParams.WRAP_CONTENT);
   changeStatusPopUp.setFocusable(true);

   // Some offset to align the popup a bit to the left, and a bit down, relative to button's position.
   int OFFSET_X = -20;
   int OFFSET_Y = 50;

   //Clear the default translucent background
   changeStatusPopUp.setBackgroundDrawable(new BitmapDrawable());

   // Displaying the popup at the specified location, + offsets.
   changeStatusPopUp.showAtLocation(layout, Gravity.NO_GRAVITY, p.x + OFFSET_X, p.y + OFFSET_Y);
}

最后,通过按钮或其他任何方式 onClick 来弹出它:

 imTaskStatusButton.setOnClickListener(new OnClickListener() 
        {
            public void onClick(View v) 
            {
                 int[] location = new int[2];
                 currentRowId = position;
                 currentRow = v;    
                 // Get the x, y location and store it in the location[] array
                 // location[0] = x, location[1] = y.
                 v.getLocationOnScreen(location);

                 //Initialize the Point with x, and y positions
                 point = new Point();
                 point.x = location[0];
                 point.y = location[1];
                 showStatusPopup(TasksListActivity.this, point);
            }
        });
一个关于PopUpWindow的好例子:

http://androidresearch.wordpress.com/2012/05/06/how-to-create-popups-in-android/


4
感谢Emil的回复。不过我曾经找到了一种方法来覆盖当前的PopupMenu类,使用一个名为setforceicon..(true)或类似的方法。不过我忘记具体细节了。 - Alex
这里的 currentRowId = position; 和 currentRow = v; 是什么意思? - DroidLearner
@DroidLearner,在这种情况下,你可以忽略它,它与弹出示例无关。 - Emil Adz
R.id.llStatusChangePopup和R.layout.status_popup_layout是什么? - Compaq LE2202x
R.layout.status_popup_layout 是弹出窗口的布局。llStatusChangePopup 是我弹出窗口的布局。 - Emil Adz
显示剩余19条评论

30

使用 MenuBuilderMenuPopupHelper 创建带图标的弹出式菜单

    MenuBuilder menuBuilder =new MenuBuilder(this);
    MenuInflater inflater = new MenuInflater(this);
    inflater.inflate(R.menu.menu, menuBuilder);
    MenuPopupHelper optionsMenu = new MenuPopupHelper(this, menuBuilder, view);
    optionsMenu.setForceShowIcon(true);

    // Set Item Click Listener
    menuBuilder.setCallback(new MenuBuilder.Callback() {
        @Override
        public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
            switch (item.getItemId()) {
                case R.id.opt1: // Handle option1 Click
                    return true;
                case R.id.opt2: // Handle option2 Click
                    return true;
                default:
                    return false;
            }
        }

        @Override
        public void onMenuModeChange(MenuBuilder menu) {}
    });


    // Display the menu
    optionsMenu.show();

menu.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/opt1"
        android:icon="@mipmap/ic_launcher"
        android:title="option 1" />
    <item
        android:id="@+id/opt2"
        android:icon="@mipmap/ic_launcher"
        android:title="option 2" />
</menu>

enter image description here


我想选择选项1并执行某些操作,如何实现? - John Joe
6
这段代码显示错误信息:“MenuBuilder构造函数只能在同一库组(groupId=com.android.support)内调用”,针对MenuBuilder出现了相同类型的错误,MenuPopupHelpersetForceShowIcon也有类似的错误提示。 - Aashish Kumar
@Aashish 这个解决方案对我有效。我不知道为什么你会遇到这样的错误。尝试进行特定于错误的搜索可能会有所帮助。我会尝试找出问题所在。如果你找到了解决方案,请在这里分享,这可能会帮助其他人。 - Ajay Sivan
6
Aashish:在这个方法上面添加 @SuppressLint("RestrictedApi"),以消除错误。 - lenooh
3
很棒,而且它可以正常运行,只需记得在方法开头添加@SuppressLint("RestrictedApi")。谢谢。 - ParSa
显示剩余3条评论

29

Android弹出菜单有一个隐藏方法可以显示菜单图标。使用Java反射将其启用,如下代码片段所示。

public static void setForceShowIcon(PopupMenu popupMenu) {
    try {
        Field[] fields = popupMenu.getClass().getDeclaredFields();
        for (Field field : fields) {
            if ("mPopup".equals(field.getName())) {
                field.setAccessible(true);
                Object menuPopupHelper = field.get(popupMenu);
                Class<?> classPopupHelper = Class.forName(menuPopupHelper
                        .getClass().getName());
                Method setForceIcons = classPopupHelper.getMethod(
                        "setForceShowIcon", boolean.class);
                setForceIcons.invoke(menuPopupHelper, true);
                break;
            }
        }
    } catch (Throwable e) {
        e.printStackTrace();
    }
}

1
由于某种原因,这段代码片段在我创建生产 APK 后无法工作,尽管在调试模式下完美运行。这对我来说是一个令人失望的惊喜 :( 。你有任何想法如何让它工作吗? - Ivan
1
很可能是Proguard更改了类/字段名称。您应该为PopupMenu类同时更改类名和字段名。 - Bao Le
1
这个解决方案不会改变菜单的宽度。它没有考虑图标的添加,因此文本被裁剪了。 - AsafK
android.support.v7.widget.PopupMenu 完美配合。 - Eugene Krivenja

29
在AppCompat中的MenuPopupHelper类有@hide注解。如果这是一个问题,或者由于某种原因你无法使用AppCompat,那么可以使用另一种解决方案,即在MenuItem标题中使用一个包含图标和标题文本的Spannable。主要步骤如下:
- 用menu xml文件填充你的PopupMenu - 如果任何一个item有图标,则对所有的items都进行以下操作:
- 如果该item没有图标,则创建一个透明图标。这可以确保没有图标的item与有图标的item对齐。 - 创建一个包含图标和标题的SpannableStringBuilder - 将menuItem的标题设置为SpannableStringBuilder - 将menuItem的图标设置为null,以防万一
优点:没有反射。不使用任何隐藏的api。可以与框架PopupMenu一起使用。
缺点:代码更多。如果你有一个没有图标的子菜单,在小屏幕上它将有不必要的左边距。
详细信息:
首先,在dimens.xml文件中定义图标的大小:
<dimen name="menu_item_icon_size">24dp</dimen>

接下来,我们介绍一些将XML定义的图标移动到标题中的方法:

/**
 * Moves icons from the PopupMenu's MenuItems' icon fields into the menu title as a Spannable with the icon and title text.
 */
public static void insertMenuItemIcons(Context context, PopupMenu popupMenu) {
    Menu menu = popupMenu.getMenu();
    if (hasIcon(menu)) {
        for (int i = 0; i < menu.size(); i++) {
            insertMenuItemIcon(context, menu.getItem(i));
        }
    }
}

/**
 * @return true if the menu has at least one MenuItem with an icon.
 */
private static boolean hasIcon(Menu menu) {
    for (int i = 0; i < menu.size(); i++) {
        if (menu.getItem(i).getIcon() != null) return true;
    }
    return false;
}

/**
 * Converts the given MenuItem's title into a Spannable containing both its icon and title.
 */
private static void insertMenuItemIcon(Context context, MenuItem menuItem) {
    Drawable icon = menuItem.getIcon();

    // If there's no icon, we insert a transparent one to keep the title aligned with the items
    // which do have icons.
    if (icon == null) icon = new ColorDrawable(Color.TRANSPARENT);

    int iconSize = context.getResources().getDimensionPixelSize(R.dimen.menu_item_icon_size);
    icon.setBounds(0, 0, iconSize, iconSize);
    ImageSpan imageSpan = new ImageSpan(icon);

    // Add a space placeholder for the icon, before the title.
    SpannableStringBuilder ssb = new SpannableStringBuilder("       " + menuItem.getTitle());

    // Replace the space placeholder with the icon.
    ssb.setSpan(imageSpan, 1, 2, 0);
    menuItem.setTitle(ssb);
    // Set the icon to null just in case, on some weird devices, they've customized Android to display
    // the icon in the menu... we don't want two icons to appear.
    menuItem.setIcon(null);
}

最后,创建您的PopupMenu并在显示它之前使用上述方法:

PopupMenu popupMenu = new PopupMenu(view.getContext(), view);
popupMenu.inflate(R.menu.popup_menu);
insertMenuItemIcons(textView.getContext(), popupMenu);
popupMenu.show();

截图: 截图

2
感谢您为非AppCompat项目提供解决方案。其他所有答案(包括其他问题的答案)甚至都没有提到需要使用AppCompat。 - Jeffrey

17

如果您不熟悉这个内容,您可以通过反射来实现它。借助这个很棒的Java高级特性,您可以修改在JVM中运行的应用程序的运行时行为,可以查看对象并在运行时执行其方法,在我们的情况下,我们需要在运行时修改弹出菜单的行为,而不是扩展核心类并对其进行修改;希望这可以帮助到您。

private void showPopupMenu(View view) {
    // inflate menu
    PopupMenu popup = new PopupMenu(mcontext, view);
    MenuInflater inflater = popup.getMenuInflater();
    inflater.inflate(R.menu.main, popup.getMenu());

    Object menuHelper;
    Class[] argTypes;
    try {
        Field fMenuHelper = PopupMenu.class.getDeclaredField("mPopup");
        fMenuHelper.setAccessible(true);
        menuHelper = fMenuHelper.get(popup);
        argTypes = new Class[]{boolean.class};
        menuHelper.getClass().getDeclaredMethod("setForceShowIcon", argTypes).invoke(menuHelper, true);
    } catch (Exception e) {

    }
    popup.show();




} 

这个对我有用,其他的都没用。谢谢。 - jonb
不适用于每个设备 - akaMahesh
你测试的是哪个设备?@akaMahesh - moahmed ayed
4
您分享的代码片段正在使用反射来访问非公共API。这些私有方法可能因不同的供应商而异,因此此代码可能无法在所有设备上运行。我在一个联想设备(型号未知)上进行了测试,必须使用自定义布局和PopupWindow。 - akaMahesh
这种方式在我所有的Play商店在线应用程序中都能正常工作,没有任何崩溃。我在联想Tab 10平板电脑上测试过它,它运行得非常好。顺便说一句,感谢您的注意。 - moahmed ayed
非常好的解决方案。谢谢 ;) - Ali Sheykhi

14

/res/menu目录中的list_item_menu.xml文件

<?xml version="1.0" encoding="utf-8"?>
    <menu xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto">

            <item
                android:id="@+id/locale"
                android:title="Localizar"
                android:icon="@mipmap/ic_en_farmacia_ico"
                app:showAsAction="always">
            </item>

            <item android:id="@+id/delete"
                android:title="Eliminar"
                android:icon="@mipmap/ic_eliminar_ico"
                app:showAsAction="always">
            </item>
    </menu>

在我的活动中

private void showPopupOption(View v){
    PopupMenu popup = new PopupMenu(getContext(), v);
    popup.getMenuInflater().inflate(R.menu.list_item_menu, popup.getMenu());

    popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
        public boolean onMenuItemClick(MenuItem menu_item) {
            switch (menu_item.getItemId()) {
                case R.id.locale:
                    break;
                case R.id.delete:
                    break;
            }
            return true;
        }
    });

    MenuPopupHelper menuHelper = new MenuPopupHelper(getContext(), (MenuBuilder) popup.getMenu(), v);
    menuHelper.setForceShowIcon(true);
    menuHelper.setGravity(Gravity.END);
    menuHelper.show();
}

结果

弹出菜单


7
Android Studio 正在要求我使用@SuppressLint("RestrictedApi") - IgniteCoders
1
popup.getMenu() 不总是 MenuBuilder 的实例。因此会崩溃。 - Han Whiteking
@HanWhiteking 请确保您使用的是 androidx.appcompat.widget.PopupMenu 而不是 android.widget.PopupMenu - Rondev

10

阅读PopupMenu源代码。我们可以通过以下代码显示图标:

Field field = popupMenu.getClass().getDeclaredField("mPopup");
field.setAccessible(true);
MenuPopupHelper menuPopupHelper = (MenuPopupHelper) field.get(popupMenu);
menuPopupHelper.setForceShowIcon(true);

但是MenuPopupHelper.java位于Android内部包中。因此我们需要使用反射:

    PopupMenu popupMenu = new PopupMenu(this, anchor);
    popupMenu.getMenuInflater().inflate(R.menu.process, popupMenu.getMenu());

    try {
        Field field = popupMenu.getClass().getDeclaredField("mPopup");
        field.setAccessible(true);
        Object menuPopupHelper = field.get(popupMenu);
        Class<?> cls = Class.forName("com.android.internal.view.menu.MenuPopupHelper");
        Method method = cls.getDeclaredMethod("setForceShowIcon", new Class[]{boolean.class});
        method.setAccessible(true);
        method.invoke(menuPopupHelper, new Object[]{true});
    } catch (Exception e) {
        e.printStackTrace();
    }

    popupMenu.show();

谢谢 - 尽管这种方法适用于使用Alex的原始xml菜单文件的情况,他在其中定义了带有相关图标的项目,但我担心我所获得的关于设置可绘制对象的边距/填充的控制不足,因为这些选项无法设置。我开始认为使用弹出菜单并不是一个好主意。 - Simon
1
现在您可以在v7包中使用MenuPopupHelper来完成这个操作,无需进行反射。 - Desmond Yao
1
通过反射访问内部API不受支持,可能无法在所有设备上或将来的设备上工作。是否有任何更新来解决这个问题? - Aashish Kumar

6
我用最简单的方法解决了我的问题,从未想过会如此简单:
在main.xml文件中:
<menu xmlns:android="http://schemas.android.com/apk/res/android" >

<item
    android:id="@+id/action_more"
    android:icon="@android:drawable/ic_menu_more"
    android:orderInCategory="1"
    android:showAsAction="always"
    android:title="More">
    <menu>
        <item
            android:id="@+id/action_one"
            android:icon="@android:drawable/ic_popup_sync"
            android:title="Sync"/>
        <item
            android:id="@+id/action_two"
            android:icon="@android:drawable/ic_dialog_info"
            android:title="About"/>
    </menu>
</item>

在MainActivity.java中。
@Override
public boolean onCreateOptionsMenu(Menu menu) {
    // Inflate the menu; this adds items to the action bar if it is present.
    getMenuInflater().inflate(R.menu.main, menu);
    return true;
}

那是通过使用子菜单的一个技巧。

14
这不是一个解决方案。图标没有显示。 - Sandra
运行得非常好。您还可以使用反射并调用setForceShowIcon(true); - Luis
7
这个回答与弹出菜单无关。我们都知道子菜单支持图标 :) - rupps
这只是创建一个嵌套子菜单。 - martyglaubitz
@Alex 这个答案是针对选项菜单而不是弹出菜单的...我们怎么在弹出菜单中使用它? - GS Nayma
这是针对ActionBar选项菜单的,不适用于弹出菜单。 - Zain

5

不使用任何库,是否可以在弹出菜单中设置带标题的图标? - GS Nayma
@DHAKAD,是的,你可以,正如这个帖子中讨论的那样。或者你可以查看这个库源代码,看看他们是如何设置图标并使用你想要的内容的。 - Gintama
我尝试像这样简单地添加图标 android:icon="@drawable/ic_menu_more",为什么它不起作用? - GS Nayma
我还没有尝试过这个 https://dev59.com/7mw15IYBdhLWcg3wIoRM#36197689。为什么不直接使用上面提到的Droppy库,它很简单易用。 - Gintama
这是唯一可行且不会影响升级的方法。通过编程生成菜单选项比使用布局和XML生成菜单更加简洁优雅。非常感谢。 - Mijo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接