开发者

How to change the background color of the options menu?

开发者 https://www.devze.com 2023-01-01 10:12 出处:网络
I\'m trying to change the default color for the options menu which is white: I want a black background for every item on the options menu.

I'm trying to change the default color for the options menu which is white: I want a black background for every item on the options menu.

I've tried some shoots like android:itemBackground="#000000" on the item element within the menu el开发者_高级运维ement but it didn't work.

How can I accomplish this?


After spending a considerable amount of time trying all the options, the only way I was able to get an app using AppCompat v7 to change the overflow menu background was using the itemBackground attribute:

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

Tested from API 4.2 to 5.0.


This is clearly a problem that a lot of programmers have and to which Google has yet to provide a satisfactory, supported solution.

There are a lot of crossed intentions and misunderstandings floating around posts on this topic, so please read this whole answer before responding.

Below I include a more "refined" and well-commented version of the hack from other answers on this page, also incorporating ideas from these very closely related questions:

Change background color of android menu

How to change the background color of the options menu?

Android: customize application's menu (e.g background color)

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

Android MenuItem Toggle Button

Is it possible to make the Android options menu background non-translucent?

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

Setting the menu background to be opaque

I tested this hack on 2.1 (simulator), 2.2 (2 real devices), and 2.3 (2 real devices). I don't have any 3.X tablets to test on yet but will post any needed changes here when/if I do. Given that 3.X tablets use Action Bars instead of Options Menus, as explained here:

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

this hack will almost certainly do nothing (no harm and no good) on 3.X tablets.

STATEMENT OF THE PROBLEM (read this before trigger-replying with a negative comment):

The Options menu has vastly different styles on different devices. Pure black with white text on some, pure white with black text on some. I and many other developers wish to control the background color of the Options menu cells as well as the color of the Options menu text.

Certain app developers only need to set the cell background color (not the text color), and they can do this in a cleaner manner using the android:panelFullBackground style described in another answer. However, there is currently no way to control the Options menu text color with styles, and so one can only use this method to change the background to another color that won't make the text "disappear."

We would love to do this with a documented, future-proof solution, but one is simply not available as of Android <= 2.3. So we have to use a solution that works in current versions and is designed to minimize the chances of crashing/breaking in future versions. We want a solution that fails gracefully back to the default behavior if it has to fail.

There are many legitimate reasons why one may need to control the look of Options menus (typically to match a visual style for the rest of the app) so I won't dwell on that.

There is a Google Android bug posted about this: please add your support by starring this bug (note Google discourages "me too" comments: just a star is enough):

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

SUMMARY OF SOLUTIONS SO FAR:

Several posters have suggested a hack involving LayoutInflater.Factory. The suggested hack worked for Android <= 2.2 and failed for Android 2.3 because the hack made an undocumented assumption: that one could call LayoutInflater.getView() directly without currently being inside a call to LayoutInflater.inflate() on the same LayoutInflater instance. New code in Android 2.3 broke this assumption and led to a NullPointerException.

My slightly refined hack below does not rely on this assumption.

Furthermore, the hacks also rely on using an internal, undocumented class name "com.android.internal.view.menu.IconMenuItemView" as a string (not as a Java type). I do not see any way to avoid this and still accomplish the stated goal. However, it is possible to do the hack in a careful way that will fall back if "com.android.internal.view.menu.IconMenuItemView" does not appear on the current system.

Again, understand that this is a hack and by no means am I claiming this will work on all platforms. But we developers are not living in a fantasy academic world where everything has to be by the book: we have a problem to solve and we have to solve it as best we can. For example, it seems unlikely that "com.android.internal.view.menu.IconMenuItemView" will exist on 3.X tablets since they use Action Bars instead of Options Menus.

Finally, some developers have solved this problem by totally suppressing the Android Options Menu and writing their own menu class (see some of the links above). I haven't tried this, but if you have time to write your own View and figure out how to replace Android's view (I'm sure the devil's in the details here) then it might be a nice solution that doesn't require any undocumented hacks.

HACK:

Here is the code.

To use this code, call addOptionsMenuHackerInflaterFactory() ONCE from your activity onCreate() or your activity onCreateOptionsMenu(). It sets a default factory that will affect subsequent creation of any Options Menu. It does not affect Options Menus that have already been created (the previous hacks used a function name of setMenuBackground(), which is very misleading since the function doesn't set any menu properties before it returns).

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

Thanks for reading and enjoy!


The style attribute for the menu background is android:panelFullBackground.

Despite what the documentation says, it needs to be a resource (e.g. @android:color/black or @drawable/my_drawable), it will crash if you use a color value directly.

This will also get rid of the item borders that I was unable to change or remove using primalpop's solution.

As for the text color, I haven't found any way to set it through styles in 2.2 and I'm sure I've tried everything (which is how I discovered the menu background attribute). You would need to use primalpop's solution for that.


This is how i solved mine. I just specified the background color and text color in styles. ie res > values > styles.xml file.

<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
    <item name="android:itemBackground">#ffffff</item>
    <item name="android:textColor">#000000</item>
</style>


Just ran into this issue too, on an App that had to be compatible with Gingerbread and still retain as much of the styling from Holo-enabled devices as possible.

I found a relatively clean solution, that worked OK for me.

In the theme I use a 9-patch drawable background to get a custom background color:

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

I gave up trying to style the text color, and just used a Spannable to set the text color for my item in code:

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


For Android 2.3 this can be done with some very heavy hacking:

The root cause for the issues with Android 2.3 is that in LayoutInflater the mConstructorArgs[0] = mContext is only set during running calls to

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

I tested it to work on Android 2.3 and to still work on earlier versions. If anything breaks again in later Android versions you'll simply see the default menu-style instead


One thing to note that you guys are over-complicating the problem just like a lot of other posts! All you need to do is create drawable selectors with whatever backgrounds you need and set them to actual items. I just spend two hours trying your solutions (all suggested on this page) and none of them worked. Not to mention that there are tons of errors that essentially slow your performance in those try/catch blocks you have.

Anyways here is a menu xml file:

<?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>

Now in your 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>

Next time you decide to go to the supermarket through Canada try google maps!


 <style name="AppThemeLight" parent="Theme.AppCompat.Light.NoActionBar">
    <item name="android:itemBackground">#000000</item>
</style>

this works fine for me


If you want to set an arbitrary color, this seem to work rather well for androidx. Tested on KitKat and Pie. Put this into your 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);
}

This sets the color of android.widget.PopupWindow$PopupBackgroundView, which, as you might have guessed, draws the background color. There's no overdraw and you can use semi-transparent colors as well.


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


Thanks Marcus! It works on 2.3 smoothly by fixing some syntax errors, here's the fixed code

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


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

this is XML file

gradient 
    android:startColor="#AFAFAF" 
    android:endColor="#000000"
    android:angle="270"
shape


When using Material3, try this:

<item name="popupMenuBackground">@color/white</item>


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)

}
0

精彩评论

暂无评论...
验证码 换一张
取 消