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

4

我成功地在工具栏的溢出菜单和子菜单中显示了图标,但我找不到如何根据它们的位置对图标进行着色的方法。以下是我正在使用的代码:

@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属性是否有never或者always这两个值,但是无法知道它是否有ifRoom这个值。因此,如果我想要正确的着色,就不能在项中使用ifRoom的值,这非常限制。

  • 有没有一种方法可以在所有情况下正确地着色菜单项?

  • 更重要的是,是否有内置的方法可以使用主题或样式来着色项,这会节省我使用这个复杂的代码吗?即使是一个不包括溢出菜单图标的解决方案,我也想知道。

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

3个回答

3
很遗憾,没有办法通过主题或样式设置菜单项图标颜色的色调。您需要一种方法来检查 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() ,但没有成功。 有什么想法吗? - Nicolas
不,那还是不行,我在onCreateonPrepare两个方法中都试过了。 - Nicolas
@Nicolas 在ActionBar布局完成后,你需要对菜单项图标进行着色。请参考我的编辑。如果在ActionBar布局完成后使用isActionButton,它应该可以工作。 - Jared Rummler
谢谢,它很好用,但只适用于工具栏。我尝试按照你的方式获取操作栏视图和另一种方式,但它总是返回null。有什么线索吗?我80%的时间都在使用ActionBar,不用将它们替换为工具栏会更好。 - Nicolas
@Nicolas 请尝试此链接:https://gist.github.com/jaredrummler/d49b3e94002275ea4e3701d9d0eb6e45 - Jared Rummler
显示剩余3条评论

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)使用,以获取用于着色图标的颜色,因为工具栏通常使用主题覆盖。通过这样做,我只需更改这2个属性的值,就可以更改每个工具栏的主题。
最后,在活动的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()来完成相同的操作。

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


0

这里有一个适用于材料组件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)
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接