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)
}
}
Related
What I am trying to do is showing a PopupWindow pointing to the overflow icon (the three dots) on the Toolbar. So I need to get a reference to the View object with the id of the icon. But what is the id?
The PopupWindow is used to tell the users that there are new entries added to the overflow menu. And suggest users to check it out.
You should create the button id
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item type="id" name="overflowActionButton"/>
</resources>
then create the button style
<style name="Widget.ActionButton.Overflow" parent="Widget.AppCompat.ActionButton.Overflow">
<item name="android:id">#id/overflowActionButton</item>
</style>
and add this style in the theme
<style name="Theme.App" parent="Theme.MaterialComponents.Light.NoActionBar">
<item name="actionOverflowButtonStyle">#style/Widget.ActionButton.Overflow</item>
</style>
finally you should find the button view by id
activity.findViewById(R.id.overflowActionButton)
and do what you want
The overflow menu item doesn't have a resource id. I found the overflow view by traversing the toolbar. The debugger showed an id of -1 and the Hierarchy Viewer showed no resource-id.
Here is how I found the overflow view without a resource id:
/**
* Get the OverflowMenuButton.
*
* #param activity
* the Activity
* #return the OverflowMenuButton or {#code null} if it doesn't exist.
*/
public static ImageView getOverflowMenuButton(Activity activity) {
return findOverflowMenuButton(activity, findActionBar(activity));
}
static ImageView findOverflowMenuButton(Activity activity, ViewGroup viewGroup) {
if (viewGroup == null) {
return null;
}
ImageView overflow = null;
for (int i = 0, count = viewGroup.getChildCount(); i < count; i++) {
View v = viewGroup.getChildAt(i);
if (v instanceof ImageView && (v.getClass().getSimpleName().equals("OverflowMenuButton") ||
v instanceof ActionMenuView.ActionMenuChildView)) {
overflow = (ImageView) v;
} else if (v instanceof ViewGroup) {
overflow = findOverflowMenuButton(activity, (ViewGroup) v);
}
if (overflow != null) {
break;
}
}
return overflow;
}
static ViewGroup findActionBar(Activity activity) {
try {
int id = activity.getResources().getIdentifier("action_bar", "id", "android");
ViewGroup actionBar = null;
if (id != 0) {
actionBar = (ViewGroup) activity.findViewById(id);
}
if (actionBar == null) {
return findToolbar((ViewGroup) activity.findViewById(android.R.id.content).getRootView());
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
static ViewGroup findToolbar(ViewGroup viewGroup) {
ViewGroup toolbar = null;
for (int i = 0, len = viewGroup.getChildCount(); i < len; i++) {
View view = viewGroup.getChildAt(i);
if (view.getClass() == android.support.v7.widget.Toolbar.class ||
view.getClass().getName().equals("android.widget.Toolbar")) {
toolbar = (ViewGroup) view;
} else if (view instanceof ViewGroup) {
toolbar = findToolbar((ViewGroup) view);
}
if (toolbar != null) {
break;
}
}
return toolbar;
}
Calling getOverflowMenuButton(activity) will return null in onCreate because the overflow menu isn't laid out yet. To get the overflow menu in onCreate I did the following:
findViewById(android.R.id.content).post(new Runnable() {
#Override public void run() {
ImageView overflow = getOverflowMenuButton(MainActivity.this);
}
});
I found a library called TapTarget and a function TapTarget.forToolbarOverflow(). It presents a solution: https://github.com/KeepSafe/TapTargetView/blob/master/taptargetview/src/main/java/com/getkeepsafe/taptargetview/TapTarget.java#L96
The way how it finds the overflow view is not neat but should be stable.
you want to create custom DropDown menu? consider this "native" way
or use android:showAsAction="never" in your menu.xml. doc of showAsAction attribute HERE. when one of MenuItems have set never value then you will get overflow three-dot icon automatically and these MenuItems will be hidding there
also you may try to use Hierarchy Viewer to investigate this id if really needed
Instead of using expensive and complicated layout traversal to find the overflow menu, I have achieved showing the PopupWindow under the overflow menu by using the Toolbar view as anchor and setting gravity to Gravity.END:
/**
* Sets the anchor view and shows the popup. In case of narrow display the menu items may be hidden in an overflow
* menu, in that case anchorView may be null and the popup will be anchored to the end of the toolbar.
*/
public void show(#Nullable View anchorView, #NonNull View toolbarView) {
if (anchorView == null) {
setDropDownGravity(Gravity.END);
setAnchorView(toolbarView);
} else {
setAnchorView(anchorView);
}
show();
}
I have been looking a way to change the color of all elements in a toolbar working like an ActionBar dynamically.
Specifications:
Using parent="Theme.AppCompat.Light.NoActionBar" on styles.xml
Appcompat v7 22
setting setSupportActionBar() in my AppCompatActivity
I got the colors from a POST request (usually #FF------ format)
I have read following post:
How do I change the color of the ActionBar hamburger icon?
How to change color of hamburger icon in material design navigation drawer
Can't change navigation drawer icon color in android
ActionBarDrawerToggle v7 arrow color
Android Toolbar color change
Android burger/arrow icon dynamic change color (this one worked in someway but I don't like using own image wihtout animation).
And others links related to this topic... none of them worked for me.
What I'm doing right now is searching for ImageButton on the toolbar (Get reference to drawer toggle in support actionbar), and applying setColorFilter() to all of them like the following code:
for (int i = 0; i < toolbar.getChildCount(); i++){
if (toolbar.getChildAt(i) instanceof ImageButton) {
ImageButton ib = (ImageButton) toolbar.getChildAt(i);
ib.setColorFilter(Color.parseColor("#A74231"), PorterDuff.Mode.SRC_ATOP);
}
}
I'm changing background and text color with: toolbar.setBackgroundColor and toolbar.setTitleTextColor.
For menu icons (including overflow menu icon):
MenuItem item2 = mMenu.findItem(R.id.actionbar_group_moreoverflow);
item2.getIcon().setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
the QUESTION: is there a better way to do it (change toolbar's elements color dynamically)?
I was facing same problem here. What I did for ToolBar's elements:
For background color: toolbar.setBackgroundColor(Color.parseColor("#xxxxxxxx"));
For text color: toolbar.setTitleTextColor(Color.parseColor("#xxxxxxxx"));
For hamburger/drawer button:
int color = Color.parseColor("#xxxxxxxx");
final PorterDuffColorFilter colorFilter = new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP);
for (int i = 0; i < toolbar.getChildCount(); i++){
final View v = toolbar.getChildAt(i);
if(v instanceof ImageButton) {
((ImageButton)v).setColorFilter(colorFilter);
}
}
For ActionMenuItemView (toolbar's buttons including overflow button):
private void colorizeToolBarItem(AppCompatActivity activity, final PorterDuffColorFilter colorFilter, final String description) {
final ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
final ViewTreeObserver viewTreeObserver = decorView.getViewTreeObserver();
viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
final ArrayList<View> outViews = new ArrayList<>();
decorView.findViewsWithText(outViews, description,
View.FIND_VIEWS_WITH_CONTENT_DESCRIPTION);
if (outViews.isEmpty())
return;
ActionMenuItemView overflow = (ActionMenuItemView)outViews.get(0);
overflow.getCompoundDrawables()[0].setColorFilter(colorFilter);
removeOnGlobalLayoutListener(decorView,this);
}
});
}
For overflow menu's items text: take a look at this link
To get all Toolbar views, iterate through all it’s child views and colorize them separately. The loop code for it looks like this:
public static void colorizeToolbar(Toolbar toolbarView, int toolbarIconsColor, Activity activity) {
final PorterDuffColorFilter colorFilter
= new PorterDuffColorFilter(toolbarIconsColor, PorterDuff.Mode.MULTIPLY);
for(int i = 0; i < toolbarView.getChildCount(); i++) {
final View v = toolbarView.getChildAt(i);
//Step 1 : Changing the color of back button (or open drawer button).
if(v instanceof ImageButton) {
//Action Bar back button
((ImageButton)v).getDrawable().setColorFilter(colorFilter);
}
if(v instanceof ActionMenuView) {
for(int j = 0; j < ((ActionMenuView)v).getChildCount(); j++) {
//Step 2: Changing the color of any ActionMenuViews - icons that
//are not back button, nor text, nor overflow menu icon.
final View innerView = ((ActionMenuView)v).getChildAt(j);
if(innerView instanceof ActionMenuItemView) {
int drawablesCount = ((ActionMenuItemView)innerView).getCompoundDrawables().length;
for(int k = 0; k < drawablesCount; k++) {
if(((ActionMenuItemView)innerView).getCompoundDrawables()[k] != null) {
final int finalK = k;
//Important to set the color filter in seperate thread,
//by adding it to the message queue
//Won't work otherwise.
innerView.post(new Runnable() {
#Override
public void run() {
((ActionMenuItemView) innerView).getCompoundDrawables()[finalK].setColorFilter(colorFilter);
}
});
}
}
}
}
}
//Step 3: Changing the color of title and subtitle.
toolbarView.setTitleTextColor(toolbarIconsColor);
toolbarView.setSubtitleTextColor(toolbarIconsColor);
//Step 4: Changing the color of the Overflow Menu icon.
setOverflowButtonColor(activity, colorFilter);
}
}
Second,implement the method responsible for finding and colorizing the Overflow Icon:
private static void setOverflowButtonColor(final Activity activity, final PorterDuffColorFilter colorFilter) {
final String overflowDescription = activity.getString(R.string.abc_action_menu_overflow_description);
final ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
final ViewTreeObserver viewTreeObserver = decorView.getViewTreeObserver();
viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
final ArrayList<View> outViews = new ArrayList<View>();
decorView.findViewsWithText(outViews, overflowDescription,
View.FIND_VIEWS_WITH_CONTENT_DESCRIPTION);
if (outViews.isEmpty()) {
return;
}
TintImageView overflow=(TintImageView) outViews.get(0);
overflow.setColorFilter(colorFilter);
removeOnGlobalLayoutListener(decorView,this);
}
});
}
Toolbar background color
mToolbarView.setBackgroundColor(color);
ToolbarColorizeHelper.colorizeToolbar(mToolbarView, mToolbarIconsColor, getActivity());
Have a look at this link, it may help you https://snow.dog/blog/how-to-dynamicaly-change-android-toolbar-icons-color/
So I have a menu item, that's defined as:
<item
android:id="#+id/action_live"
android:title="#string/action_live"
android:orderInCategory="1"
app:showAsAction="ifRoom|withText" />
It shows as text, as you can see below:
And I want to programmatically change the "LIVE" text color. I've searched for a while and I found a method:
With globally defined:
private Menu mOptionsMenu;
and:
#Override
public boolean onCreateOptionsMenu(Menu menu) {
mOptionsMenu = menu;
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
I do:
MenuItem liveitem = mOptionsMenu.findItem(R.id.action_live);
SpannableString s = new SpannableString(liveitem.getTitle().toString());
s.setSpan(new ForegroundColorSpan(Color.RED), 0, s.length(), 0);
liveitem.setTitle(s);
But nothing happens!
If I do the same for an item of the overflow menu, it works:
Is there some limitation for app:showAsAction="ifRoom|withText" items? Is there any workaround?
Thanks in advance.
Bit late to the party with this one, but I spent a while working on this and found a solution, which may be of use to anyone else trying to do the same thing. Some credit goes to Harish Sridharan for steering me in the right direction.
You can use findViewById(R.id.MY_MENU_ITEM_ID) to locate the menu item (provided that the menu had been created and prepared), and cast it to a TextView instance as suggested by Harish, which can then be styled as required.
public class MyAwesomeActivity extends Activity {
#Override
protected void onCreate(Bundle savedInstanceState) {
getWindow().requestFeature(Window.FEATURE_ACTION_BAR);
super.onCreate(savedInstanceState);
// Force invalidatation of the menu to cause onPrepareOptionMenu to be called
invalidateOptionsMenu();
}
private void styleMenuButton() {
// Find the menu item you want to style
View view = findViewById(R.id.YOUR_MENU_ITEM_ID_HERE);
// Cast to a TextView instance if the menu item was found
if (view != null && view instanceof TextView) {
((TextView) view).setTextColor( Color.BLUE ); // Make text colour blue
((TextView) view).setTextSize(TypedValue.COMPLEX_UNIT_SP, 24); // Increase font size
}
}
#Override
public boolean onPrepareOptionsMenu(Menu menu) {
boolean result = super.onPrepareOptionsMenu(menu);
styleMenuButton();
return result;
}
}
The trick here is to force the menu to be invalidated in the activity's onCreate event (thereby causing the onPrepareMenuOptions to be called sooner than it would normally). Inside this method, we can locate the menu item and style as required.
#RRP give me a clue ,but his solution does not work for me. And #Box give a another, but his answer looks a little not so cleaner. Thanks them. So according to them, I have a total solution.
private static void setMenuTextColor(final Context context, final Toolbar toolbar, final int menuResId, final int colorRes) {
toolbar.post(new Runnable() {
#Override
public void run() {
View settingsMenuItem = toolbar.findViewById(menuResId);
if (settingsMenuItem instanceof TextView) {
if (DEBUG) {
Log.i(TAG, "setMenuTextColor textview");
}
TextView tv = (TextView) settingsMenuItem;
tv.setTextColor(ContextCompat.getColor(context, colorRes));
} else { // you can ignore this branch, because usually there is not the situation
Menu menu = toolbar.getMenu();
MenuItem item = menu.findItem(menuResId);
SpannableString s = new SpannableString(item.getTitle());
s.setSpan(new ForegroundColorSpan(ContextCompat.getColor(context, colorRes)), 0, s.length(), 0);
item.setTitle(s);
}
}
});
}
In order to change the colour of menu item you can find that item, extract the title from it, put it in a Spannable String and set the foreground colour to it. Try out this code piece
#Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu, menu);
MenuItem mColorFullMenuBtn = menu.findItem(R.id.action_submit); // extract the menu item here
String title = mColorFullMenuBtn.getTitle().toString();
if (title != null) {
SpannableString s = new SpannableString(title);
s.setSpan(new ForegroundColorSpan(Color.parseColor("#FFFFFF")), 0, s.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); // provide whatever color you want here.
mColorFullMenuBtn.setTitle(s);
}
return super.onCreateOptionsMenu(menu);
}
It only becomes a text view after inspection, its real class is ActionMenuItemView, on which we can further set the text color like this:
public static void setToolbarMenuItemTextColor(final Toolbar toolbar,
final #ColorRes int color,
#IdRes final int resId) {
if (toolbar != null) {
for (int i = 0; i < toolbar.getChildCount(); i++) {
final View view = toolbar.getChildAt(i);
if (view instanceof ActionMenuView) {
final ActionMenuView actionMenuView = (ActionMenuView) view;
// view children are accessible only after layout-ing
actionMenuView.post(new Runnable() {
#Override
public void run() {
for (int j = 0; j < actionMenuView.getChildCount(); j++) {
final View innerView = actionMenuView.getChildAt(j);
if (innerView instanceof ActionMenuItemView) {
final ActionMenuItemView itemView = (ActionMenuItemView) innerView;
if (resId == itemView.getId()) {
itemView.setTextColor(ContextCompat.getColor(toolbar.getContext(), color));
}
}
}
}
});
}
}
}
}
You could put the change of the color in the onPrepareOptionsMenu:
override fun onPrepareOptionsMenu(menu: Menu?): Boolean
{
val signInMenuItem = menu?.findItem(R.id.menu_main_sign_in)
val title = signInMenuItem?.title.toString()
val spannable = SpannableString(title)
spannable.setSpan(
ForegroundColorSpan(Color.GREEN),
0,
spannable.length,
Spannable.SPAN_INCLUSIVE_INCLUSIVE)
SgnInMenuItem?.title = spannable
return super.onPrepareOptionsMenu(menu)
}
of course you can make it shorter above...
now you can change the color appearance upon other (ie. viewmodel) values...
RG
I spent a lot of hours on this and finally got it into work. There is easy solusion for Android 6 and 7 but it doesn't work on Android 5. This code works on all of them. So, if you are doing it in Kotlin this is my suggestion:
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.my_menu, menu)
setToolbarActionTextColor(menu, R.color.black)
this.menu = menu
return true
}
private fun setToolbarActionTextColor(menu: Menu, color: Int) {
val tb = findViewById<Toolbar>(R.id.toolbar)
tb?.let { toolbar ->
toolbar.post {
val view = findViewById<View>(R.id.my_tag)
if (view is TextView) {
view.setTextColor(ContextCompat.getColor(this, color))
} else {
val mi = menu.findItem(R.id.my_tag)
mi?.let {
val newTitle: Spannable = SpannableString(it.title.toString())
val newColor = ContextCompat.getColor(this, color)
newTitle.setSpan(ForegroundColorSpan(newColor),
0, newTitle.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
it.title = newTitle
}
}
}
}
}
It's complicated, but you can use the app:actionLayout attribute. For example,
my_menu.xml:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:id="#+id/englishList"
android:orderInCategory="1"
app:showAsAction="ifRoom|withText"
app:actionLayout="#layout/custom_menu_item_english_list"
android:title=""/>
</menu>
custom_menu_item_english_list.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical">
<androidx.appcompat.widget.AppCompatTextView
android:id="#+id/englishListWhiteText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#FFFFFF"
android:lineHeight="16dp"
android:textSize="16sp"
android:text="英文"
android:layout_marginRight="8dp"/>
</LinearLayout>
MainActivity.java:
#Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.my_menu, menu);
MenuItem item = menu.findItem(R.id.englishList);
item.getActionView().findViewById(R.id.englishListWhiteText)
.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v){
//Handle button click.
}
});
return true;
}
Result:
More Detailed Example=
https://medium.com/#info.anikdey003/custom-menu-item-using-action-layout-7a25118b9d5
if you are using popup menu function to show the menu items in the application and trying to change the design or color of your text items in the menu list, first create a style item in your style.xml file:
<style name="PopupMenuStyle" parent="Widget.AppCompat.PopupMenu">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_gravity">center</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:textColor">#color/ColorPrimary</item>
<item name="android:textSize">#dimen/textsize</item>
<item name="android:fontFamily">#font/myfonts</item></style>
and use this style in your code as:
val popupWrapper = ContextThemeWrapper(this, R.style.PopupMenuStyle)
val popup = PopupMenu(popupWrapper, your_menu_view)
MenuItem as defined by documentation is an interface. It will definitely be implemented with a view widget before being portrayed as an menu. Most cases these menu items are implemented as TextView. You can use UiAutomatorViewer to see the view hierarchy or even use hierarchyviewer which will be found in [sdk-home]/tools/. Attached one sample uiautomatorviewer screenshot for a MenuItem
So you can always typecast your MenuItem and set the color.
TextView liveitem = (TextView)mOptionsMenu.findItem(R.id.action_live);
liveitem.setTextColor(Color.RED);
EDIT:
Since there was request to see how to use this tool, I'm adding a few more contents.
Make sure you have set environment variable $ANDROID_HOME pointing to your SDK HOME.
In your terminal:
cd $ANDROID_HOME
./tools/uiautomatorviewer
This tool will open up.
The second or third button (refer screenshot) in the menu will capture the screenshot of your attached device or emulator and you can inspect the view and their hierarchy. Clicking on the view will describe the view and their information. It is tool purposely designed for testing and you can inspect any application.
Refer developer site for more info: uiautomatorviewer
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.** { *; }
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;
}
});
}