在溢出菜单和子菜单中为菜单图标着色

Posted

技术标签:

【中文标题】在溢出菜单和子菜单中为菜单图标着色【英文标题】:Tint menu icons in overflow menu and submenus 【发布时间】:2018-06-10 02:22:32 【问题描述】:

我设法在工具栏的溢出菜单和子菜单中显示图标,但我找不到如何根据图标的位置为它们着色。这是我正在使用的代码:

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

使用MenuItem.requiresActionButton() 可以知道项目是否在XML 中的showAsAction 属性中具有neveralways 值,但如果它具有ifRoom 值则不知道。因此,如果我想要正确的着色,我不能在项目中使用 ifRoom 值,这是非常严格的。

有没有办法在所有情况下都正确地为菜单项着色?

更重要的是,是否有一种内置方法可以使用主题或样式为项目着色,从而避免我使用这段复杂的代码?即使没有覆盖溢出菜单中的图标的解决方案,我也想知道它。

如果没有其他方法,我完全可以使用反射。

【问题讨论】:

【参考方案1】:

很遗憾,无法使用主题或样式设置菜单项图标颜色的色调。您需要一种方法来检查MenuItem 是否在ActionBar 或溢出菜单中可见。本机和支持MenuItemImpl 类都有一个方法,但它们要么被限制在库中,要么被隐藏。这需要反思。您可以通过以下方法检查菜单项是否可见,然后设置颜色过滤器:

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;
    
  

您还需要等到菜单膨胀后才能为项目着色。为此,您可以获得对ActionBar 的引用,并在绘制ActionBar 后为MenuItem 着色。

例子:

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

这是我为帮助着色菜单项图标而编写的一个类:https://gist.github.com/jaredrummler/7816b13fcd5fe1ac61cb0173a1878d4f

【讨论】:

当我在onCreateOptionsMenu 中使用它时,它会为ifRoom 项目着色,就像它们溢出一样。但是,当我在onPrepareOptionsMenu 中使用它时,它会正确着色,但仅在打开溢出菜单后才会出现着色。 (每次打开溢出菜单时都会调用 onPrepare)。我尝试从那里拨打invalidateOptionsMenu(),但没有成功。有什么想法吗? onCreateOptioinsMenu 一直对我有用。您应该在应用颜色过滤器后改变图标并重新设置。 if (icon != null) Drawable drawable = icon.mutate(); /* Set color filter */ item.setIcon(drawable) 不,还是不行,我在onCreateonPrepare都试过了。 @Nicolas 您需要在布局完 ActionBar 后为菜单项图标着色。请参阅我的编辑。如果您在布置ActionBar 之后使用isActionButton,它应该可以工作。 感谢它很好用,但仅适用于工具栏。我尝试以您的方式和another way 获取操作栏视图,但它始终返回 null。有什么线索吗?我 80% 的时间都在使用 ActionBar,不必用工具栏替换它们会很棒。【参考方案2】:

感谢@JaredRummler,我找到了一种方法来确定图标是否在溢出菜单中。我在这里发布了完整的代码,收集了他的答案的元素。我还添加了一个辅助方法来为图标着色获得正确的颜色。这是我目前使用的:

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;
    


我还在attrs.xml 中定义了两个属性:toolbarThemetoolbarPopupTheme,它们是我在 XML 中的工具栏布局上设置的。它们的值在我的应用主题themes.xml 中定义。 ThemeUtils.getToolbarColors(Context) 使用这些属性来获取用于为图标着色的颜色,因为工具栏经常使用主题覆盖。通过这样做,我可以仅通过更改这两个属性的值来更改每个工具栏的主题。

剩下的就是在活动的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]);

同样可以在片段中通过将this 替换为getActivity() 来完成。

更新 MenuItem 图标时,可以调用另一个方法,ThemeUtils.changeMenuIconColor()。在这种情况下,工具栏颜色可以在onCreate 中获取并全局存储以供重复使用。

【讨论】:

【参考方案3】:

这是一个适用于材料组件MaterialToolbar的解决方案:

说明

代码检查工具栏的所有子视图 => 那些是可见项 它递归地迭代所有菜单项并检查菜单 id 是否是可见视图 id 的一部分,如果是,这意味着菜单项在工具栏上,否则它在溢出菜单内 然后它会根据其位置为图标着色 它还会为溢出图标着色 要正确着色子菜单箭头指示器,请查看以下问题:https://github.com/material-components/material-components-android/issues/553

代码

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)
    

【讨论】:

以上是关于在溢出菜单和子菜单中为菜单图标着色的主要内容,如果未能解决你的问题,请参考以下文章

在纵向和横向之间更改操作栏溢出菜单图标

如何更改工具栏导航和溢出菜单图标(appcompat v7)?

CardView 内的工具栏,用于创建弹出菜单(溢出图标)

如何在 ActionBar 的溢出菜单中显示图标

如何在swift / SwiftUI中为滑动手势设置菜单栏图标的操作

如何在Android ListView中为菜单项创建图标?