Search code examples
androidandroid-actionbarandroid-menu

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.


Solution

  • 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.