Check If MenuItem is In ActionBar Overflow - android

Question: Is there a way to check in code if a MenuItem (or which MenuItems) are in the overflow menu of the ActionBar?
I'm using ActionBarSherlock
The reason I need this is because I have a bunch of icons that will show up in the ActionBar if there is room. I have a holo dark theme so the icons are made to fit that.
My problem comes when the menu items are put into the overflow menu. On Pre-Honeycomb devices this means they will show when the user presses the menu button. This menu is the exact opposite background as my ActionBar and I want to have a different set of icons to fit that.

I might have found a solution to this problem: In the Design Guide (here), there is a table that shows how many action bar items are shown depending on the width in dip.
Based on that table I have written the following code:
#Override
public boolean onPrepareOptionsMenu(Menu menu) {
MenuItem search = menu.findItem(R.id.menu_search);
// Get width in dp
DisplayMetrics metrics = new DisplayMetrics();
Display display = getWindowManager().getDefaultDisplay();
display.getMetrics(metrics);
float logicalDensity = metrics.density;
int dp = (int) (display.getWidth() / logicalDensity + 0.5);
if (dp < 360) { // only two icons
search.setIcon(R.drawable.ic_menu_search); // Show menu icon for pre-3.0 menu
} else {
search.setIcon(R.drawable.ic_action_search); // Show action bar icon for action bar
}
return true;
}

If you are using the Toolbar then you can create a simple extension in Kotlin or static function in Java to find if the toolbar menu item is visible on Toolbar or hidden into the overflow option menu.
Here is a sample function and it's use:
fun Toolbar.isMenuItemOverflowing(#IdRes id: Int): Boolean {
return this.findViewById<View>(id) == null
}
Using the function with toolbar instance:-
toolbar.isMenuItemOverflowing(R.id.action_search)

I posted an answer to a similar question that will solve your problem, see:
https://stackoverflow.com/a/18884872/1299562
Basically you can use onPrepareOptionsMenu to remove the non-action item icons.

You can use reflection. Put the following code in a class and then invoke Foo.isInOverflow(yourMenuItem);
protected static final String SUPPORTCLASS = "android.support.v7.internal.view.menu.MenuItemImpl";
protected static final String NATIVECLASS = "com.android.internal.view.menu.MenuItemImpl";
protected static Method sSupportIsActionButton;
protected static Method sNativeIsActionButton;
static {
try {
Class<?> MenuItemImpl = Class.forName(NATIVECLASS);
sNativeIsActionButton = MenuItemImpl.getDeclaredMethod("isActionButton");
sNativeIsActionButton.setAccessible(true);
} catch (Exception ignored) {
}
try {
Class<?> MenuItemImpl = Class.forName(SUPPORTCLASS);
sSupportIsActionButton = MenuItemImpl.getDeclaredMethod("isActionButton");
sSupportIsActionButton.setAccessible(true);
} catch (Exception ignored) {
}
}
// --------------------------------------------------------------------------------------------
/**
* Check if an item is showing (not in the overflow menu).
*
* #param item
* the MenuItem.
* #return {#code true} if the MenuItem is visible on the ActionBar.
*/
public static boolean isActionButton(MenuItem item) {
switch (item.getClass().getName()) {
case SUPPORTCLASS:
try {
return (boolean) sSupportIsActionButton.invoke(item, (Object[]) null);
} catch (Exception e) {
// fall through
}
case NATIVECLASS:
try {
return (boolean) sNativeIsActionButton.invoke(item, (Object[]) null);
} catch (Exception e) {
// fall through
}
default:
return true;
}
}
/**
* Check if an item is in the overflow menu.
*
* #param item
* the MenuItem
* #return {#code true} if the MenuItem is in the overflow menu.
* #see #isActionButton(MenuItem)
*/
public static boolean isInOverflow(MenuItem item) {
return !isActionButton(item);
}
Note: you need to add the following line to your proguard configuration file so reflection works in production builds:
-keep public class android.support.v7.internal.view.menu.** { *; }

Related

Tint menu icons in overflow menu and submenus

I managed to show icons in the toolbar's overflow menu and submenus, but I couldn't find how to tint the icons according to their position. Here the code I'm using:
#Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.toolbar_main, menu);
// Show icons in overflow menu
if (menu instanceof MenuBuilder) {
MenuBuilder m = (MenuBuilder) menu;
m.setOptionalIconsVisible(true);
}
// Change icons color
changeIconsColor(menu, colorNormal, colorInMenu, false);
return super.onCreateOptionsMenu(menu);
}
public static void changeIconsColor(Menu menu, int colorNormal, int colorInMenu, boolean isInSubMenu) {
// Change icons color
for (int i = 0; i < menu.size(); i++) {
MenuItem item = menu.getItem(i);
Drawable icon = item.getIcon();
if (icon != null) {
int color = (((MenuItemImpl) item).requiresActionButton() ? colorNormal : colorInMenu);
icon.setColorFilter(color, PorterDuff.Mode.SRC_IN);
icon.setAlpha(item.isEnabled() ? 255 : 128);
}
if (item.hasSubMenu()) {
changeIconsColor(item.getSubMenu(), colorNormal, colorInMenu, true);
}
}
}
The use of MenuItem.requiresActionButton() allows to know if an item has the values never or always in the showAsAction attribute in XML, but not if it has the ifRoom value. Because of this, I cannot use the ifRoom value in items if I want proper tinting, it's very restrictive.
Is there a way to tint menu items properly in all cases?
More importantly, is there a built-in way to tint items with themes or styles that would save me from using this complex piece of code? Even if a solution that doesn't cover icons in overflow menu, I would like to know about it.
I am perfectly fine with using reflection if there is no other way.
Unfortunately, there is no way to set the menu item icon color's tint using a theme or style. You need a method to check if the MenuItem is visible on the ActionBar or in the overflow menu. Both the native and support MenuItemImpl class have a method for this but they are either restricted to the library or hidden. This requires reflection. You can use the following method to check if the menu item is visible or not, and then set the color filter:
public static boolean isActionButton(#NonNull MenuItem item) {
if (item instanceof MenuItemImpl) {
return ((MenuItemImpl) item).isActionButton();
} else {
// Not using the support library. This is a native MenuItem. Reflection is needed.
try {
Method m = item.getClass().getDeclaredMethod("isActionButton");
if (!m.isAccessible()) m.setAccessible(true);
return (boolean) m.invoke(item);
} catch (Exception e) {
return false;
}
}
}
You also need to wait until the menu is inflated before tinting the items. To accomplish this you can get a reference to the ActionBar and tint the MenuItem after the ActionBar has been drawn.
Example:
#Override public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_main, menu);
int id = getResources().getIdentifier("action_bar", "id", "android");
ViewGroup actionBar;
if (id != 0) {
actionBar = (ViewGroup) findViewById(id);
} else {
// You must be using a custom Toolbar. Use the toolbar view instead.
// actionBar = yourToolbar
}
actionBar.post(new Runnable() {
#Override public void run() {
// Add code to tint menu items here
}
});
return super.onCreateOptionsMenu(menu);
}
Here is a class I wrote to help with tinting menu item icons: https://gist.github.com/jaredrummler/7816b13fcd5fe1ac61cb0173a1878d4f
Thanks to #JaredRummler, I found a way to determine whether an icon is in the overflow menu or not. I posted the complete code here that gathers the elements of his answer. I also added a helper methods for getting the right colors for tinting icons. Here's what I currently use:
ThemeUtils
public final class ThemeUtils {
/**
* Obtain colors of a context's theme from attributes
* #param context themed context
* #param colorAttrs varargs of color attributes
* #return array of colors in the same order as the array of attributes
*/
public static int[] getColors(Context context, int... colorAttrs) {
TypedArray ta = context.getTheme().obtainStyledAttributes(colorAttrs);
int[] colors = new int[colorAttrs.length];
for (int i = 0; i < colorAttrs.length; i++) {
colors[i] = ta.getColor(i, 0);
}
ta.recycle();
return colors;
}
/**
* Get the two colors needed for tinting toolbar icons
* The colors are obtained from the toolbar's theme and popup theme
* These themes are obtained from {#link R.attr#toolbarTheme} and {#link R.attr#toolbarPopupTheme}
* The two color attributes used are:
* - {#link android.R.attr#textColorPrimary} for the normal color
* - {#link android.R.attr#textColorSecondary} for the color in a menu
* #param context activity context
* #return int[2]{normal color, color in menu}
*/
public static int[] getToolbarColors(Context context) {
// Get the theme and popup theme of a toolbar
TypedArray ta = context.getTheme().obtainStyledAttributes(
new int[]{R.attr.toolbarTheme, R.attr.toolbarPopupTheme});
Context overlayTheme = new ContextThemeWrapper(context, ta.getResourceId(0, 0));
Context popupTheme = new ContextThemeWrapper(context, ta.getResourceId(1, 0));
ta.recycle();
// Get toolbar colors from these themes
int colorNormal = ThemeUtils.getColors(overlayTheme, android.R.attr.textColorPrimary)[0];
int colorInMenu = ThemeUtils.getColors(popupTheme, android.R.attr.textColorSecondary)[0];
return new int[]{colorNormal, colorInMenu};
}
/**
* Change the color of the icons of a menu
* Disabled items are set to 50% alpha
* #param menu targeted menu
* #param colorNormal normal icon color
* #param colorInMenu icon color for popup menu
* #param isInSubMenu whether menu is a sub menu
*/
private static void changeIconsColor(View toolbar, Menu menu, int colorNormal, int colorInMenu, boolean isInSubMenu) {
toolbar.post(() -> {
// Change icons color
for (int i = 0; i < menu.size(); i++) {
MenuItem item = menu.getItem(i);
changeMenuIconColor(item, colorNormal, colorInMenu, isInSubMenu);
if (item.hasSubMenu()) {
changeIconsColor(toolbar, item.getSubMenu(), colorNormal, colorInMenu, true);
}
}
});
}
public static void changeIconsColor(View toolbar, Menu menu, int colorNormal, int colorInMenu) {
changeIconsColor(toolbar, menu, colorNormal, colorInMenu, false);
}
/**
* Change the color of a single menu item icon
* #param item targeted menu item
* #param colorNormal normal icon color
* #param colorInMenu icon color for popup menu
* #param isInSubMenu whether item is in a sub menu
*/
#SuppressLint("RestrictedApi")
public static void changeMenuIconColor(MenuItem item, int colorNormal, int colorInMenu, boolean isInSubMenu) {
if (item.getIcon() != null) {
Drawable icon = item.getIcon().mutate();
int color = (((MenuItemImpl) item).isActionButton() && !isInSubMenu ? colorNormal : colorInMenu);
icon.setColorFilter(color, PorterDuff.Mode.SRC_IN);
icon.setAlpha(item.isEnabled() ? 255 : 128);
item.setIcon(icon);
}
}
}
ActivityUtils
public final class ActivityUtils {
/**
* Force show the icons in the overflow menu and submenus
* #param menu target menu
*/
public static void forceShowMenuIcons(Menu menu) {
if (menu instanceof MenuBuilder) {
MenuBuilder m = (MenuBuilder) menu;
m.setOptionalIconsVisible(true);
}
}
/**
* Get the action bar or toolbar view in activity
* #param activity activity to get from
* #return the toolbar view
*/
public static ViewGroup findActionBar(Activity activity) {
int id = activity.getResources().getIdentifier("action_bar", "id", "android");
ViewGroup actionBar = null;
if (id != 0) {
actionBar = activity.findViewById(id);
}
if (actionBar == null) {
return findToolbar((ViewGroup) activity.findViewById(android.R.id.content).getRootView());
}
return actionBar;
}
private static ViewGroup findToolbar(ViewGroup viewGroup) {
ViewGroup toolbar = null;
for (int i = 0; i < viewGroup.getChildCount(); i++) {
View view = viewGroup.getChildAt(i);
if (view.getClass() == android.support.v7.widget.Toolbar.class ||
view.getClass() == android.widget.Toolbar.class) {
toolbar = (ViewGroup) view;
} else if (view instanceof ViewGroup) {
toolbar = findToolbar((ViewGroup) view);
}
if (toolbar != null) {
break;
}
}
return toolbar;
}
}
I also defined two attributes in attrs.xml: toolbarTheme and toolbarPopupTheme that I set on my toolbar layout in XML. Their values are defined in my app theme in themes.xml. These attributes are used by ThemeUtils.getToolbarColors(Context) to obtain the colors to use for tinting icons, because toolbars often use theme overlays. By doing this, I can change every toolbar's theme only by changing the value of these 2 attributes.
All that is left is calling the following in the activity's onCreateOptionsMenu(Menu menu):
ActivityUtils.forceShowMenuIcons(menu); // Optional, show icons in overflow and submenus
View toolbar = ActivityUtils.findActionBar(this); // Get the action bar view
int[] toolbarColors = ThemeUtils.getToolbarColors(this); // Get the icons colors
ThemeUtils.changeIconsColor(toolbar, menu, toolbarColors[0], toolbarColors[1]);
The same can be done in a fragment by replacing this with getActivity().
When updating a MenuItem icon, another method can be called, ThemeUtils.changeMenuIconColor(). In this case, toolbar colors can be obtained in onCreate and stored globally to reuse them.
Here's a solution that works with the material components MaterialToolbar:
Explanation
The code checks all sub views of the toolbar => those are the visible items
it iterates all menu items recursively and checks if the menu id is part of the visible view ids, if so, this means the menu item is on the toolbar, otherwise it is inside the overflow menu
it then tints the icons based on its position
it also tints the overflow icon
to tint the sub menu arrow indicator correctly, check out following issue: https://github.com/material-components/material-components-android/issues/553
Code
fun View.getAllChildrenRecursively(): List<View> {
val result = ArrayList<View>()
if (this !is ViewGroup) {
result.add(this)
} else {
for (index in 0 until this.childCount) {
val child = this.getChildAt(index)
result.addAll(child.getAllChildrenRecursively())
}
}
return result
}
#SuppressLint("RestrictedApi")
fun MaterialToolbar.tintAndShowIcons(colorOnToolbar: Int, colorInOverflow: Int) {
(menu as? MenuBuilder)?.setOptionalIconsVisible(true)
val c1 = ColorStateList.valueOf(colorOnToolbar)
val c2 = PorterDuffColorFilter(colorInOverflow, PorterDuff.Mode.SRC_IN)
val idsShowing = ArrayList<Int>()
getAllChildrenRecursively().forEach {
// Icon in Toolbar
(it as? ActionMenuItemView)?.let {
idsShowing.add(it.id)
}
// Overflow Icon
(it as? ImageView)?.imageTintList = c1
}
menu.forEach {
checkOverflowMenuItem(it, c2, idsShowing)
}
}
private fun checkOverflowMenuItem(menuItem: MenuItem, iconColor: ColorFilter, idsShowing: ArrayList<Int>) {
// Only change Icons inside the overflow
if (!idsShowing.contains(menuItem.itemId)) {
menuItem.icon?.colorFilter = iconColor
}
menuItem.subMenu?.forEach {
checkOverflowMenuItem(it, iconColor, idsShowing)
}
}

android detect Samsung Galaxy S8 navigation bar hide or show programmatically

On some devices such as Samsung S8, navigation bar can be hide or show, that's a question in some condition.
Samsung S8's navigation bar can be hide or show by click left bottom button
I didn't find straight way to determine even if in the Android sources code.
And I google some issues, such as A good solution to check for navigation bar , but it doesn't help.
Any help is very appreciated.
First, credit the original author: https://www.jianshu.com/p/ddfbabd614b6
For the Samsung phones (i.e, S8, S9, etc) you can detect if the navigation bar is showing via listening to a Samsung event.
private static final String SAMSUNG_NAVIGATION_EVENT = "navigationbar_hide_bar_enabled";
Then just listen to this event, and do your thing:
private void checkNavigationBar() {
if (isSamsungVersionNougat()) { // check if Samsung phones
// detect navigation bar
try {
// there are navigation bar
if (Settings.Global.getInt(activity.getContentResolver(), SAMSUNG_NAVIGATION_EVENT) == 0) {
// Your code
// example: galleryViewModel.navigationBarHeight.set(getNavigationBarHeight());
} else { // there are no navigation bar
// Your code
// example: galleryViewModel.navigationBarHeight.set(0);
}
} catch (Exception ignored) {
}
barHideEnableObserver = new BarHideEnableObserver(new Handler());
activity.getContentResolver().registerContentObserver(
Settings.Global.getUriFor(SAMSUNG_NAVIGATION_EVENT),
true, barHideEnableObserver);
} else {
galleryViewModel.navigationBarHeight.set(getNavigationBarHeight());
}
}
Use this method, worked for me. Make sure the view has been rendered first to make sure that getHeight() doesn't return 0. Also make sure that the rootView you are using is meant to take up the whole screen.
public static boolean hasNavBar (Activity activity, View rootView) {
if (activity == null || rootView == null)
return true;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1)
return true;
Display d = activity.getWindowManager().getDefaultDisplay();
DisplayMetrics realDisplayMetrics = new DisplayMetrics();
d.getRealMetrics(realDisplayMetrics);
int viewHeight = rootView.getHeight();
if (viewHeight == 0)
return true;
int realHeight = realDisplayMetrics.heightPixels;
return realHeight != viewHeight;
}

Change option menu background dynamically

I have a "Cart" item in a menu option, and I'd like to display the number of products in the cart, as shown below.
To achieve this I want to create several images with numbers from 1 to 9 and 9+ and set the correct image as the background of the corresponding menu option item when opening the menu.
How can I do this, i.e. how can I change the background of a menu option item dynamically?
Thanks
Override onPrepareOptionsMenu() menu method. It is called each time user clicks Menu button.
#Override
public boolean onPrepareOptionsMenu(Menu menu) {
// Let's find id of resource for drawable with required count
// assuming you have cartIcon1.png, cartIcon2.png etc
// in your `drawable` folder
int resId = getResources().getIdentifier("cartIcon" + numberOfElementsInCart, "drawable", getPackageName());
if (resId != 0)
menu.findItem(R.id.cart).setIcon(resId);
// If resource was not found, set default icon
else
menu.findItem(R.id.cart).setIcon(R.drawable.defaultCart);
return true;
}
Overriding background is much harder, and I believe it's more convinient to have icons with cart and number, since you already have .png for each number
Below is a horrible, horrible hack to change the background image of menus - note that it'll change all the menu item backgrounds. There is probably an easier way to do this now, but this was the only way I found a year or so ago.
Setting the icon of the menu item is a lot easier, and can be done in onPrepareOptionsMenu
// Hack to make the menu item selector blue
protected void setMenuBackground(final int id)
{
if(getLayoutInflater().getFactory() != null)
return;
getLayoutInflater().setFactory(new Factory()
{
#Override
public View onCreateView(String name, Context context, AttributeSet attrs)
{
if(name.equalsIgnoreCase("com.android.internal.view.menu.IconMenuItemView"))
{
try
{
LayoutInflater li = getLayoutInflater();
final View view = li.createView(name, null, attrs);
//What?
//Well the Android system is going to set the background after this is method is done
//so we run it later to override the override. Simples?
new Handler().post(new Runnable()
{
public void run()
{
view.setBackgroundResource(id);
}
});
return view;
}
catch(InflateException e)
{
}
catch(ClassNotFoundException e)
{
}
}
return null;
}
});
}

android: how to change Text size, color of Options Menu Items and options menu height and width

I have used the following code to customize the background of options menu successfully.
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.blue_bg_pixel);
}
});
return view;
}
catch (InflateException e) {
}
catch (ClassNotFoundException e) {
}
}
return null;
}
});
Now I have to customize menu size, text size/color. Anybody plz help me in this.
Thanks in advance.
This fails on Android 2.3
See
http://code.google.com/p/android/issues/detail?id=4441#c6
and
Change menu background color on android 2.3
for details
Width , Heigth you need hack action bar to that.
But if you need customize, use : ActionBarGenerator (http://jgilfelt.github.io/android-actionbarstylegenerator/)

How to change the background color of 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 element 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)
}

Categories

Resources