【问题标题】:PopupMenu with icons [duplicate]带有图标的 PopupMenu [重复]
【发布时间】:2013-03-16 21:38:58
【问题描述】:

当然,我们在这里处理的是 SDK 11 及更高版本。

我打算做类似的事情:

PopupMenu 中的每个item 旁边,我想放置一个icon

我创建了一个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 文件中我定义了我想要的图标,但是,当弹出菜单显示时,它显示的是没有图标的图标。我应该怎么做才能让这 2 个图标出现?

【问题讨论】:

标签: android popupmenu


【解决方案1】:

如果您使用的是 AppCompat v7,则此方法有效。这有点 hacky,但明显比使用反射更好,并且让您仍然可以使用核心 Android PopupMenu:

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>


这会导致使用菜单资源文件中定义的图标弹出菜单:

【讨论】:

  • 很好的解决方案!小提示:您甚至不需要像以前那样创建PopupMenu 实例。只需创建一个新的MenuBuilder() 将菜单膨胀到其中并将其传递给助手。
  • 很好@david.schreiber - 我从我的代码中删除了一些行。我实际上创建了 PopupMenu,所以我可以使用setonMenuItemClickListener()。将此添加到示例中,因为我相信大多数开发人员都想知道他们的菜单项何时被点击!
  • 无论如何,我找不到需要的类。它抛出了这个异常:java.lang.ClassCastException: com.android.internal.view.menu.MenuBuilder cannot be cast to android.support.v7.view.menu.MenuBuilder
  • @IgniteCoders 使用 v7 支持库中的 MenuBuilder。所以改变你的进口。
  • @IgniteCoders 要修复 ClassCastException,请将 import android.widget.PopupMenu; 替换为 import android.support.v7.widget.PopupMenu;
【解决方案2】:

否则我会实施它:

创建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/

【讨论】:

  • 感谢 Emil 的回复。但是,我曾经找到一种方法来覆盖当前的 PopupMenu 类,使用名为 setforceicon..(true) 或类似的方法。我忘记了
  • 不,我没弄明白,我问你是否已经知道类似的东西......我仍在寻找解决方案!哈哈
  • 如果我在定义菜单 XML 时没有记错,您希望菜单在按下硬件菜单按钮时弹出,对吗?
  • 不,我希望它在角落里按下 View 时弹出,就像上面显示的“谷歌地图”应用程序一样。
  • 它现在工作了吗?当您在所需位置按下视图时它会弹出吗?
【解决方案3】:

Android 弹出菜单有一个隐藏的方法来显示菜单图标。使用Java反射来启用它,如下代码sn-p。

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

【讨论】:

  • 由于某种原因,这个 sn-p 在我创建生产 apk 后不起作用,尽管在调试模式下完美工作。这对我来说是一个令人失望的惊喜:(。你知道如何让它发挥作用吗?
  • 可能,proguard 更改了类/字段名称。您应该同时使用 PopupMenu 类的类名和字段名。
  • 这个解决方案不会改变菜单的宽度。它不考虑添加图标,因此文本被裁剪。
  • 完美搭配android.support.v7.widget.PopupMenu
【解决方案4】:

AppCompat 中的MenuPopupHelper 类具有@hide 注释。如果这是一个问题,或者如果您出于某种原因无法使用 AppCompat,还有另一种解决方案,在包含图标和标题文本的 MenuItem 标题中使用 Spannable

主要步骤是:

  • menu xml 文件扩充您的PopupMenu
  • 如果任何项目有图标,则对 所有 项目执行此操作:
    • 如果项目没有图标,则创建一个透明图标。这样可以确保没有图标的项目将与有图标的项目对齐
    • 创建一个包含图标和标题的SpannableStringBuilder
    • 将菜单项的标题设置为SpannableStringBuilder
    • 将菜单项的图标设置为空,“以防万一”

优点:没有反射。不使用任何隐藏的 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();

截图:

【讨论】:

  • 感谢您为非 AppCompat 项目提供解决方案。所有其他答案(也包括其他问题)甚至都没有提到 AppCompat 是一项要求。
【解决方案5】:

带有图标的弹出菜单使用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>

【讨论】:

  • 我想选择option1做点什么,怎么实现?
  • @JohnJoe 我已经添加了代码示例来处理菜单点击。
  • 此代码显示错误 MenuBuilder 构造函数只能从 MenuBuilder 的同一库组 (groupId=com.android.support) 中调用MenuPopupHelpersetForceShowIcon 显示相同类型的错误
  • Aashish:在方法上方添加@SuppressLint("RestrictedApi"),以消除错误。
  • 太棒了,而且工作正常,只要记住在方法的开头添加@SuppressLint("RestrictedApi")。谢谢
【解决方案6】:

如果你不熟悉反射,你可以使用反射来实现它运行时的方法,在我们的例子中,我们需要在运行时修改 popupMenu 行为,而不是扩展核心类并修改它;)希望有所帮助

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




} 

【讨论】:

  • 这对我有用,这里没有别的。谢谢
  • 并非在所有设备上都能正常工作
  • 您针对 @akaMahesh 测试的设备
  • 你分享的片段是使用反射来访问非公共api,这些私有方法可能因不同的供应商而异,因此这段代码不会在每台设备上运行,我在联想设备上测试过,(不知道模型),我必须使用带有弹出窗口的自定义布局。
  • 这种方式在 Play 商店中的所有在线应用程序中都可以正常工作,而不会发生一次崩溃。我在 Lenovo Tab 10 平板电脑上对其进行了测试,效果很好。顺便感谢您的通知
【解决方案7】:

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

结果

【讨论】:

  • Android Studio 要求我这样做:@SuppressLint("RestrictedApi")
  • popup.getMenu() 并不总是 MenuBuilder 的实例。因此崩溃。
  • @HanWhiteking 确保您使用的是androidx.appcompat.widget.PopupMenu 而不是android.widget.PopupMenu
【解决方案8】:

阅读 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 菜单文件,他已在其中定义了带有相关图标的项目,但此方法有效,但我担心缺乏对我的控制权设置可绘制的边距/填充,因为无法设置这些选项。我开始认为使用弹出菜单不是一个好主意。
  • 现在您可以在 v7 包中使用 MenuPopupHelper 来执行此操作而无需反射。
  • 不支持通过反射访问内部 API,并且可能不适用于所有设备或将来警告。有什么更新可以解决吗??
【解决方案9】:

我用最简单的方法解决了我的问题,没想到这么简单:

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

这是一个使用子菜单

的技巧

【讨论】:

  • 这不是解决方案。图标不显示..
  • 像魅力一样工作。您还可以使用反射并调用 setForceShowIcon(true);
  • 这个答案与 PopUpMenu 无关。我们都知道子菜单支持图标:)
  • 这只是创建一个嵌套的子菜单
  • @Alex 这个答案是针对选项菜单而不是弹出菜单......我们如何在弹出菜单中使用它?
【解决方案10】:

如果您想在弹出菜单中显示图标,请查看https://github.com/shehabic/Droppy,它非常酷且易于使用

【讨论】:

  • 不使用任何库...是否可以在弹出菜单中设置带有标题的图标?
  • @DHAKAD 是的,正如本主题中的人们所讨论的那样,您可以。或者您可以查看此库源,了解他们如何设置图标并准确使用您想要的。
  • 其实我尝试像 android:icon="@drawable/ic_menu_more" 这样简单地添加图标,为什么它不起作用??
  • 我还没试过这个stackoverflow.com/a/36197689/1236933。而且为什么不直接使用上面的 Droppy 库,很简单
  • 这是唯一有效的方法,并且升级安全。此外,以编程方式生成菜单的选项比使用布局和 xml 生成菜单要简洁得多。非常感谢
【解决方案11】:

基于@Ajay 的回答......这就是我所做的

 @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.add_task, menu);  // for the two icons in action bar
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {

            case R.id.menu:
                View menuItemView = findViewById(R.id.menu);
                MenuBuilder menuBuilder =new MenuBuilder(this);
                MenuInflater inflater = new MenuInflater(this);
                inflater.inflate(R.menu.popup, menuBuilder);
                MenuPopupHelper optionsMenu = new MenuPopupHelper(this, menuBuilder, menuItemView); 
                optionsMenu.setForceShowIcon(true);
                optionsMenu.show();

            default:
                return super.onOptionsItemSelected(item);
        }
    }

弹出窗口

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
    android:id="@+id/opt1"
    android:icon="@drawable/change_pic"
    android:title="Change Picture" />
<item
    android:id="@+id/opt2"
    android:icon="@drawable/change_pin"
    android:title="Change Password" />

    <item
        android:id="@+id/opt3"
        android:icon="@drawable/sign_out"
        android:title="Sign Out" />
</menu>

屏幕截图

【讨论】:

    【解决方案12】:

    我正在尝试@Stephen Kidson 的回答和@david.schereiber 的建议,我意识到MenuBuilder 中没有setOnMenuItemClickListener 这样的方法。稍微弄乱了v7的源代码,我找到了这个解决方案:

            MenuBuilder menuBuilder = new MenuBuilder(mContext);
            new SupportMenuInflater(mContext).inflate(R.menu.my_menu, menuBuilder);
            menuBuilder.setCallback(new MenuBuilder.Callback() {
                @Override
                public boolean onMenuItemSelected(MenuBuilder menu, MenuItem menuItem) {
                    // your "setOnMenuItemClickListener" code goes here
                    switch (menuItem.getItemId()) {
                        case R.id.menu_id1:
                            // do something 1
                            return true;
    
                        case R.id.menu_id2:
                            // do something 2
                            return true;
                    }
                    return false;
                }
    
                @Override
                public void onMenuModeChange(MenuBuilder menu) {
                }
            });
            MenuPopupHelper menuHelper = new MenuPopupHelper(mContext, menuBuilder, v);
            menuHelper.setForceShowIcon(true); // show icons!!!!!!!!
            menuHelper.show();
    

    【讨论】:

    • 有一种更直接的方式使用 PopupMenu 对象我上面的答案实例化:menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { ... });
    • 我不想创建PopupMenu只是为了膨胀菜单然后把它拿出来,用我的方法我可以直接把菜单膨胀到MenuBuilder中,我认为它更好
    猜你喜欢
    • 1970-01-01
    • 2016-04-30
    • 1970-01-01
    • 2020-09-03
    • 1970-01-01
    • 2018-09-21
    • 2019-11-14
    • 2019-07-18
    相关资源
    最近更新 更多