溢出菜单和子菜单中的色调菜单图标
Tint menu icons in overflow menu and submenus
我设法在工具栏的溢出菜单和子菜单中显示图标,但我找不到如何根据图标的位置给图标着色。这是我使用的代码:
@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
值,它非常有限制。
有没有办法在所有情况下正确调整菜单项的颜色?
更重要的是,是否有一种内置方法可以使用主题或样式为项目着色,从而使我免于使用这段复杂的代码?即使没有覆盖溢出菜单中图标的解决方案,我也想知道。
如果没有其他办法,我完全可以使用反射。
遗憾的是,无法使用主题或样式设置菜单项图标颜色的色调。您需要一种方法来检查 MenuItem
是否在 ActionBar
或溢出菜单中可见。本机和支持 MenuItemImpl
class 都有一个方法,但它们要么被限制在库中,要么被隐藏。这需要反思。您可以使用以下方法检查菜单项是否可见,然后设置颜色过滤器:
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);
}
这是我写的 class 帮助对菜单项图标进行着色的文章:https://gist.github.com/jaredrummler/7816b13fcd5fe1ac61cb0173a1878d4f
感谢@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
中定义了两个属性:toolbarTheme
和 toolbarPopupTheme
,我在 XML 的工具栏布局中设置了它们。它们的值在 themes.xml
中的我的应用程序主题中定义。 ThemeUtils.getToolbarColors(Context)
使用这些属性来获取用于着色图标的颜色,因为工具栏通常使用主题覆盖。通过这样做,我可以仅通过更改这 2 个属性的值来更改每个工具栏的主题。
剩下的就是在 activity 的 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
中获取工具栏颜色并全局存储以重复使用它们。
这是一个适用于 material 组件的解决方案 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)
}
}
我设法在工具栏的溢出菜单和子菜单中显示图标,但我找不到如何根据图标的位置给图标着色。这是我使用的代码:
@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
值,它非常有限制。
有没有办法在所有情况下正确调整菜单项的颜色?
更重要的是,是否有一种内置方法可以使用主题或样式为项目着色,从而使我免于使用这段复杂的代码?即使没有覆盖溢出菜单中图标的解决方案,我也想知道。
如果没有其他办法,我完全可以使用反射。
遗憾的是,无法使用主题或样式设置菜单项图标颜色的色调。您需要一种方法来检查 MenuItem
是否在 ActionBar
或溢出菜单中可见。本机和支持 MenuItemImpl
class 都有一个方法,但它们要么被限制在库中,要么被隐藏。这需要反思。您可以使用以下方法检查菜单项是否可见,然后设置颜色过滤器:
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);
}
这是我写的 class 帮助对菜单项图标进行着色的文章:https://gist.github.com/jaredrummler/7816b13fcd5fe1ac61cb0173a1878d4f
感谢@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
中定义了两个属性:toolbarTheme
和 toolbarPopupTheme
,我在 XML 的工具栏布局中设置了它们。它们的值在 themes.xml
中的我的应用程序主题中定义。 ThemeUtils.getToolbarColors(Context)
使用这些属性来获取用于着色图标的颜色,因为工具栏通常使用主题覆盖。通过这样做,我可以仅通过更改这 2 个属性的值来更改每个工具栏的主题。
剩下的就是在 activity 的 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
中获取工具栏颜色并全局存储以重复使用它们。
这是一个适用于 material 组件的解决方案 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)
}
}