105

我正在尝试更改选项菜单的默认颜色为白色:我希望选项菜单上的每个项目都有黑色背景。

我在菜单元素中的 item 元素上尝试了一些类似 android:itemBackground="#000000" 的拍摄,但没有成功。

我怎样才能做到这一点?

4

13 回答 13

77

在花费大量时间尝试所有选项之后,我能够使用 AppCompat v7 获得应用程序来更改溢出菜单背景的唯一方法是使用 itemBackground 属性:

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    ...
    <item name="android:itemBackground">@color/overflow_background</item>
    ...
</style>

从 API 4.2 测试到 5.0。

于 2015-03-11T20:42:20.817 回答
52

这显然是许多程序员都面临的问题,而 Google 尚未提供令人满意的支持解决方案。

关于这个主题的帖子有很多交叉的意图和误解,所以请在回复之前阅读整个答案。

下面我从本页的其他答案中包含了一个更“精致”和评论良好的破解版本,还结合了这些非常相关的问题的想法:

更改android菜单的背景颜色

如何更改选项菜单的背景颜色?

Android:自定义应用程序的菜单(例如背景颜色)

http://www.macadamian.com/blog/post/android_-_theming_the_unthemable/

Android MenuItem 切换按钮

是否可以使 Android 选项菜单背景不透明?

http://www.codeproject.com/KB/android/AndroidMenusMyWay.aspx

将菜单背景设置为不透明

我在 2.1(模拟器)、2.2(2 个真实设备)和 2.3(2 个真实设备)上测试了这个 hack。我还没有要测试的 3.X 平板电脑,但如果我这样做了,我会在此处发布任何需要的更改。鉴于 3.X 平板电脑使用操作栏而不是选项菜单,如下所述:

http://developer.android.com/guide/topics/ui/menus.html#options-menu

这个 hack 几乎肯定不会对 3.X 平板电脑造成任何影响(没有坏处也没有好处)。

问题陈述(在使用负面评论触发回复之前阅读此内容):

选项菜单在不同设备上的风格截然不同。纯黑,有些是白字,纯白,有些是黑字。我和许多其他开发人员希望控制选项菜单单元格的背景颜色以及选项菜单文本的颜色

某些应用程序开发人员只需要设置单元格背景颜色(而不是文本颜色),他们可以使用另一个答案中描述的 android:panelFullBackground 样式以更简洁的方式执行此操作。但是,目前无法通过样式来控制选项菜单文本颜色,因此只能使用此方法将背景更改为不会使文本“消失”的另一种颜色。

我们很想用一个记录在案的、面向未来的解决方案来做到这一点,但从 Android <= 2.3 开始根本就没有这个解决方案。因此,我们必须使用适用于当前版本的解决方案,旨在最大限度地减少未来版本中崩溃/中断的可能性。我们想要一个解决方案,如果它必须失败,它会优雅地返回默认行为。

有很多合理的原因可以解释为什么人们可能需要控制选项菜单的外观(通常是为了匹配应用程序其余部分的视觉风格),所以我不会详述。

有一个谷歌 Android 错误发布了关于此问题:请通过为这个错误加星标来添加您的支持(注意谷歌不鼓励“我也是”评论:只要一个星就足够了):

http://code.google.com/p/android/issues/detail?id=4441

迄今为止的解决方案总结:

一些海报建议了涉及 LayoutInflater.Factory 的黑客攻击。建议的 hack 对 Android <= 2.2 有效,而对 Android 2.3 失败,因为该 hack 做了一个未记录的假设:可以直接调用 LayoutInflater.getView(),而无需当前在同一个 LayoutInflater 实例上调用 LayoutInflater.inflate()。Android 2.3 中的新代码打破了这一假设并导致了 NullPointerException。

下面我稍微改进的 hack 不依赖于这个假设。

此外,黑客还依赖于使用内部的、未记录的类名“com.android.internal.view.menu.IconMenuItemView”作为字符串(而不是 Java 类型)。我看不出有任何方法可以避免这种情况并仍然实现既定目标。但是,如果当前系统上没有出现“com.android.internal.view.menu.IconMenuItemView”,则可以谨慎地进行黑客攻击。

同样,请理解这是一个 hack,我绝不是声称这将适用于所有平台。但是我们开发人员并不是生活在一个幻想的学术世界中,一切都必须按部就班:我们有一个问题要解决,我们必须尽我们所能解决它。例如,“com.android.internal.view.menu.IconMenuItemView”似乎不太可能出现在 3.X 平板电脑上,因为它们使用操作栏而不是选项菜单。

最后,一些开发人员通过完全抑制 Android 选项菜单并编写自己的菜单类来解决这个问题(参见上面的一些链接)。我还没有尝试过,但是如果您有时间编写自己的视图并弄清楚如何替换 Android 的视图(我敢肯定这里的细节是魔鬼),那么它可能是一个不需要任何内容​​的好解决方案无证黑客。

哈克:

这是代码。

要使用此代码,请从您的活动 onCreate() 或活动 onCreateOptionsMenu() 调用 addOptionsMenuHackerInflaterFactory() 一次。它设置了一个默认工厂,它将影响任何选项菜单的后续创建。它不会影响已经创建的选项菜单(之前的黑客使用了 setMenuBackground() 的函数名称,这是非常具有误导性的,因为该函数在返回之前没有设置任何菜单属性)。

@SuppressWarnings("rawtypes")
static Class       IconMenuItemView_class = null;
@SuppressWarnings("rawtypes")
static Constructor IconMenuItemView_constructor = null;

// standard signature of constructor expected by inflater of all View classes
@SuppressWarnings("rawtypes")
private static final Class[] standard_inflater_constructor_signature = 
new Class[] { Context.class, AttributeSet.class };

protected void addOptionsMenuHackerInflaterFactory()
{
    final LayoutInflater infl = getLayoutInflater();

    infl.setFactory(new Factory()
    {
        public View onCreateView(final String name, 
                                 final Context context,
                                 final AttributeSet attrs)
        {
            if (!name.equalsIgnoreCase("com.android.internal.view.menu.IconMenuItemView"))
                return null; // use normal inflater

            View view = null;

            // "com.android.internal.view.menu.IconMenuItemView" 
            // - is the name of an internal Java class 
            //   - that exists in Android <= 3.2 and possibly beyond
            //   - that may or may not exist in other Android revs
            // - is the class whose instance we want to modify to set background etc.
            // - is the class we want to instantiate with the standard constructor:
            //     IconMenuItemView(context, attrs)
            // - this is what the LayoutInflater does if we return null
            // - unfortunately we cannot just call:
            //     infl.createView(name, null, attrs);
            //   here because on Android 3.2 (and possibly later):
            //   1. createView() can only be called inside inflate(),
            //      because inflate() sets the context parameter ultimately
            //      passed to the IconMenuItemView constructor's first arg,
            //      storing it in a LayoutInflater instance variable.
            //   2. we are inside inflate(),
            //   3. BUT from a different instance of LayoutInflater (not infl)
            //   4. there is no way to get access to the actual instance being used
            // - so we must do what createView() would have done for us
            //
            if (IconMenuItemView_class == null)
            {
                try
                {
                    IconMenuItemView_class = getClassLoader().loadClass(name);
                }
                catch (ClassNotFoundException e)
                {
                    // this OS does not have IconMenuItemView - fail gracefully
                    return null; // hack failed: use normal inflater
                }
            }
            if (IconMenuItemView_class == null)
                return null; // hack failed: use normal inflater

            if (IconMenuItemView_constructor == null)
            {
                try
                {
                    IconMenuItemView_constructor = 
                    IconMenuItemView_class.getConstructor(standard_inflater_constructor_signature);
                }
                catch (SecurityException e)
                {
                    return null; // hack failed: use normal inflater
                }
                catch (NoSuchMethodException e)
                {
                    return null; // hack failed: use normal inflater
                }
            }
            if (IconMenuItemView_constructor == null)
                return null; // hack failed: use normal inflater

            try
            {
                Object[] args = new Object[] { context, attrs };
                view = (View)(IconMenuItemView_constructor.newInstance(args));
            }
            catch (IllegalArgumentException e)
            {
                return null; // hack failed: use normal inflater
            }
            catch (InstantiationException e)
            {
                return null; // hack failed: use normal inflater
            }
            catch (IllegalAccessException e)
            {
                return null; // hack failed: use normal inflater
            }
            catch (InvocationTargetException e)
            {
                return null; // hack failed: use normal inflater
            }
            if (null == view) // in theory handled above, but be safe... 
                return null; // hack failed: use normal inflater


            // apply our own View settings after we get back to runloop
            // - android will overwrite almost any setting we make now
            final View v = view;
            new Handler().post(new Runnable()
            {
                public void run()
                {
                    v.setBackgroundColor(Color.BLACK);

                    try
                    {
                        // in Android <= 3.2, IconMenuItemView implemented with TextView
                        // guard against possible future change in implementation
                        TextView tv = (TextView)v;
                        tv.setTextColor(Color.WHITE);
                    }
                    catch (ClassCastException e)
                    {
                        // hack failed: do not set TextView attributes
                    }
                }
            });

            return view;
        }
    });
}

感谢阅读和享受!

于 2011-12-12T13:51:41.247 回答
20

菜单背景的样式属性是android:panelFullBackground.

不管文档怎么说,它必须是一个资源(例如@android:color/blackor @drawable/my_drawable),如果你直接使用颜色值,它会崩溃。

这也将摆脱我无法使用 primalpop 的解决方案更改或删除的项目边框。

至于文本颜色,我还没有找到通过 2.2 中的样式设置它的任何方法,而且我确信我已经尝试了所有方法(这就是我发现菜单背景属性的方式)。您需要为此使用 primalpop 的解决方案。

于 2011-03-05T18:40:31.090 回答
17

这就是我解决我的问题的方法。我只是在样式中指定了背景颜色和文本颜色。即 res > values > styles.xml 文件。

<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
    <item name="android:itemBackground">#ffffff</item>
    <item name="android:textColor">#000000</item>
</style>
于 2018-04-24T20:03:40.910 回答
13

在一个必须与 Gingerbread 兼容的应用程序上也遇到了这个问题,并且仍然尽可能多地保留支持 Holo 的设备的样式。

我找到了一个相对干净的解决方案,对我来说效果很好。

在主题中,我使用 9-patch 可绘制背景来获得自定义背景颜色:

<style name="Theme.Styled" parent="Theme.Sherlock">
   ...
   <item name="android:panelFullBackground">@drawable/menu_hardkey_panel</item>
</style>

我放弃了尝试设置文本颜色的样式,而只是使用 Spannable 在代码中为我的项目设置文本颜色:

@Override
public boolean onCreateOptionsMenu(Menu menu) {
   MenuInflater inflater = getSupportMenuInflater();
   inflater.inflate(R.menu.actions_main, menu);

   if (android.os.Build.VERSION.SDK_INT < 
        android.os.Build.VERSION_CODES.HONEYCOMB) {

        SpannableStringBuilder text = new SpannableStringBuilder();
        text.append(getString(R.string.action_text));
        text.setSpan(new ForegroundColorSpan(Color.WHITE), 
                0, text.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

        MenuItem item1 = menu.findItem(R.id.action_item1);
        item1.setTitle(text);
   }

   return true;
}
于 2013-01-28T01:56:41.967 回答
13

对于 Android 2.3,这可以通过一些非常严重的黑客攻击来完成:

Android 2.3 问题的根本原因是在 LayoutInflater 中 mConstructorArgs[0] = mContext 仅在运行调用期间设置

http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/2.3.3_r1/android/view/LayoutInflater.java/#352

protected void setMenuBackground(){

    getLayoutInflater().setFactory( new Factory() {

        @Override
        public View onCreateView (final String name, final Context context, final AttributeSet attrs ) {

            if ( name.equalsIgnoreCase( "com.android.internal.view.menu.IconMenuItemView" ) ) {

                try { // Ask our inflater to create the view
                    final LayoutInflater f = getLayoutInflater();
                    final View[] view = new View[1]:
                    try {
                        view[0] = f.createView( name, null, attrs );
                    } catch (InflateException e) {
                        hackAndroid23(name, attrs, f, view);
                    }
                    // Kind of apply our own background
                    new Handler().post( new Runnable() {
                        public void run () {
                            view.setBackgroundResource( R.drawable.gray_gradient_background);
                        }
                    } );
                    return view;
                }
                catch ( InflateException e ) {
                }
                catch ( ClassNotFoundException e ) {
                }
            }
            return null;
        }
    });
}

static void hackAndroid23(final String name,
    final android.util.AttributeSet attrs, final LayoutInflater f,
    final TextView[] view) {
    // mConstructorArgs[0] is only non-null during a running call to inflate()
    // so we make a call to inflate() and inside that call our dully XmlPullParser get's called
    // and inside that it will work to call "f.createView( name, null, attrs );"!
    try {
        f.inflate(new XmlPullParser() {
            @Override
            public int next() throws XmlPullParserException, IOException {
                try {
                    view[0] = (TextView) f.createView( name, null, attrs );
                } catch (InflateException e) {
                } catch (ClassNotFoundException e) {
                }
                throw new XmlPullParserException("exit");
            }   
        }, null, false);
    } catch (InflateException e1) {
        // "exit" ignored
    }
}

我测试了它可以在 Android 2.3 上运行,并且仍然可以在早期版本上运行。如果在以后的 Android 版本中再次出现任何问题,您只会看到默认的菜单样式

于 2011-04-13T10:33:23.013 回答
8

需要注意的一件事是,你们就像许多其他帖子一样使问题过于复杂!您需要做的就是创建具有您需要的任何背景的可绘制选择器,并将它们设置为实际项目。我只花了两个小时尝试您的解决方案(所有建议都在此页面上),但都没有奏效。更不用说在你拥有的那些 try/catch 块中有大量的错误会降低你的性能。

无论如何,这里是一个菜单 xml 文件:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@+id/m1"
          android:icon="@drawable/item1_selector"
          />
    <item android:id="@+id/m2"
          android:icon="@drawable/item2_selector"
          />
</menu>

现在在您的 item1_selector 中:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true" android:drawable="@drawable/item_highlighted" />
    <item android:state_selected="true" android:drawable="@drawable/item_highlighted" />
    <item android:state_focused="true" android:drawable="@drawable/item_nonhighlighted" />
    <item android:drawable="@drawable/item_nonhighlighted" />
</selector>

下次您决定通过加拿大去超市时,请尝试使用谷歌地图!

于 2011-05-28T01:07:10.027 回答
4

如果您想设置任意颜色,这似乎对androidx. 在 KitKat 和 Pie 上测试。把它放进你的AppCompatActivity

@Override public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
    if (name.equals("androidx.appcompat.view.menu.ListMenuItemView") &&
            parent.getParent() instanceof FrameLayout) {
            ((View) parent.getParent()).setBackgroundColor(yourFancyColor);
    }
    return super.onCreateView(parent, name, context, attrs);
}

这会设置 的颜色android.widget.PopupWindow$PopupBackgroundView,正如您可能已经猜到的那样,它会绘制背景颜色。没有过度绘制,您也可以使用半透明颜色。

于 2018-10-30T03:51:02.840 回答
4
 <style name="AppThemeLight" parent="Theme.AppCompat.Light.NoActionBar">
    <item name="android:itemBackground">#000000</item>
</style>

这对我来说很好

于 2016-04-05T08:48:29.027 回答
3
    /* 
     *The Options Menu (the one that pops up on pressing the menu button on the emulator) 
     * can be customized to change the background of the menu 
     *@primalpop  
   */ 

    package com.pop.menu;

    import android.app.Activity;
    import android.content.Context;
    import android.os.Bundle;
    import android.os.Handler;
    import android.util.AttributeSet;
    import android.util.Log;
    import android.view.InflateException;
    import android.view.LayoutInflater;
    import android.view.Menu;
    import android.view.MenuInflater;
    import android.view.View;
    import android.view.LayoutInflater.Factory;

    public class Options_Menu extends Activity {

        private static final String TAG = "DEBUG";

        /** Called when the activity is first created. */
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.main);

        }

        /* Invoked when the menu button is pressed */

        @Override
        public boolean onCreateOptionsMenu(Menu menu) {
            // TODO Auto-generated method stub
            super.onCreateOptionsMenu(menu);
            MenuInflater inflater = new MenuInflater(getApplicationContext());
            inflater.inflate(R.menu.options_menu, menu);
            setMenuBackground();
            return true;
        }

        /*IconMenuItemView is the class that creates and controls the options menu 
         * which is derived from basic View class. So We can use a LayoutInflater 
         * object to create a view and apply the background.
         */
        protected void setMenuBackground(){

            Log.d(TAG, "Enterting setMenuBackGround");
            getLayoutInflater().setFactory( new Factory() {

                @Override
                public View onCreateView ( String name, Context context, AttributeSet attrs ) {

                    if ( name.equalsIgnoreCase( "com.android.internal.view.menu.IconMenuItemView" ) ) {

                        try { // Ask our inflater to create the view
                            LayoutInflater f = getLayoutInflater();
                            final View view = f.createView( name, null, attrs );
                            /* 
                             * The background gets refreshed each time a new item is added the options menu. 
                             * So each time Android applies the default background we need to set our own 
                             * background. This is done using a thread giving the background change as runnable
                             * object
                             */
                            new Handler().post( new Runnable() {
                                public void run () {
                                    view.setBackgroundResource( R.drawable.background);
                                }
                            } );
                            return view;
                        }
                        catch ( InflateException e ) {}
                        catch ( ClassNotFoundException e ) {}
                    }
                    return null;
                }
            });
        }
    }
于 2010-05-31T14:50:24.300 回答
3
protected void setMenuBackground() {
    getLayoutInflater().setFactory(new Factory() {
        @Override
        public View onCreateView (String name, Context context, AttributeSet attrs) {
            if (name.equalsIgnoreCase("com.android.internal.view.menu.IconMenuItemView")) {
                try {
                    // Ask our inflater to create the view
                    LayoutInflater f = getLayoutInflater();
                    final View view = f.createView(name, null, attrs);
                    // Kind of apply our own background
                    new Handler().post( new Runnable() {
                        public void run () {
                            view.setBackgroundResource(R.drawable.gray_gradient_background);
                        }
                    });
                    return view;
                }
                catch (InflateException e) {
                }
                catch (ClassNotFoundException e) {
                }
            }
            return null;
        }
    });
}

这是 XML 文件

gradient 
    android:startColor="#AFAFAF" 
    android:endColor="#000000"
    android:angle="270"
shape
于 2011-04-07T11:18:55.250 回答
3

谢谢马库斯!通过修复一些语法错误,它可以在 2.3 上顺利运行,这是修复的代码

    protected void setMenuBackground() {
    getLayoutInflater().setFactory(new Factory() {

        @Override
        public View onCreateView(final String name, final Context context,
                final AttributeSet attrs) {

            if (name.equalsIgnoreCase("com.android.internal.view.menu.IconMenuItemView")) {

                try { // Ask our inflater to create the view
                    final LayoutInflater f = getLayoutInflater();
                    final View[] view = new View[1];
                    try {
                        view[0] = f.createView(name, null, attrs);
                    } catch (InflateException e) {
                        hackAndroid23(name, attrs, f, view);
                    }
                    // Kind of apply our own background
                    new Handler().post(new Runnable() {
                        public void run() {
                            view[0].setBackgroundColor(Color.WHITE);

                        }
                    });
                    return view[0];
                } catch (InflateException e) {
                } catch (ClassNotFoundException e) {

                }
            }
            return null;
        }
    });
}

static void hackAndroid23(final String name,
        final android.util.AttributeSet attrs, final LayoutInflater f,
        final View[] view) {
    // mConstructorArgs[0] is only non-null during a running call to
    // inflate()
    // so we make a call to inflate() and inside that call our dully
    // XmlPullParser get's called
    // and inside that it will work to call
    // "f.createView( name, null, attrs );"!
    try {
        f.inflate(new XmlPullParser() {
            @Override
            public int next() throws XmlPullParserException, IOException {
                try {
                    view[0] = (TextView) f.createView(name, null, attrs);
                } catch (InflateException e) {
                } catch (ClassNotFoundException e) {
                }
                throw new XmlPullParserException("exit");
            }
        }, null, false);
    } catch (InflateException e1) {
        // "exit" ignored
    }
}
于 2011-05-15T09:34:30.247 回答
0

Kotlin Androidx

override fun onCreateView(
    parent: View?,
    name: String,
    context: Context,
    attrs: AttributeSet
): View? {

    if (parent?.parent is FrameLayout) {
        (parent?.parent as View).setBackgroundColor(Color.parseColor("#33B5E5"))
    }

    return super.onCreateView(parent, name, context!!, attrs)

}
于 2021-09-21T06:15:01.340 回答