Skip to content

Instantly share code, notes, and snippets.

@jaredrummler
Last active April 13, 2022 03:58
Show Gist options
  • Star 51 You must be signed in to star a gist
  • Fork 7 You must be signed in to fork a gist
  • Save jaredrummler/ec7dfb73f3235ad8e951 to your computer and use it in GitHub Desktop.
Save jaredrummler/ec7dfb73f3235ad8e951 to your computer and use it in GitHub Desktop.
Helper class to set the color and transparency for menu icons in an ActionBar or Toolbar.
/*
* Copyright (C) 2015. Jared Rummler <jared.rummler@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.jrummyapps.android.tinting;
import android.app.Activity;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.v4.view.MenuItemCompat;
import android.support.v7.view.menu.MenuItemImpl;
import android.support.v7.widget.ActionMenuView;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MenuItem.OnActionExpandListener;
import android.view.SubMenu;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.SearchView;
import java.lang.reflect.Method;
/**
* <p>Apply colors and/or transparency to menu icons in a {@link Menu}.</p>
*
* <p>Example usage:</p>
*
* <pre class="prettyprint">
* public boolean onCreateOptionsMenu(Menu menu) {
* ...
* int color = getResources().getColor(R.color.your_awesome_color);
* int alpha = 204; // 80% transparency
* MenuTint.on(menu).setMenuItemIconColor(color).setMenuItemIconAlpha(alpha).apply(this);
* ...
* }
* </pre>
*/
public class MenuTint {
private static final String TAG = "MenuTint";
private static Method nativeIsActionButton;
/**
* 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) {
if (item instanceof MenuItemImpl) {
return ((MenuItemImpl) item).isActionButton();
}
if (nativeIsActionButton == null) {
try {
Class<?> MenuItemImpl = Class.forName("com.android.internal.view.menu.MenuItemImpl");
nativeIsActionButton = MenuItemImpl.getDeclaredMethod("isActionButton");
if (!nativeIsActionButton.isAccessible()) {
nativeIsActionButton.setAccessible(true);
}
} catch (Exception ignored) {
}
}
try {
return (boolean) nativeIsActionButton.invoke(item, (Object[]) null);
} catch (Exception e) {
e.printStackTrace();
}
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);
}
/**
* Sets the color filter and/or the alpha transparency on a {@link MenuItem}'s icon.
*
* @param menuItem
* The {@link MenuItem} to theme.
* @param color
* The color to set for the color filter or {@code null} for no changes.
* @param alpha
* The alpha value (0...255) to set on the icon or {@code null} for no changes.
*/
public static void colorMenuItem(MenuItem menuItem, Integer color, Integer alpha) {
if (color == null && alpha == null) {
return; // nothing to do.
}
Drawable drawable = menuItem.getIcon();
if (drawable != null) {
// If we don't mutate the drawable, then all drawables with this id will have the ColorFilter
drawable.mutate();
if (color != null) {
drawable.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
}
if (alpha != null) {
drawable.setAlpha(alpha);
}
}
}
/**
* Set the menu to show MenuItem icons in the overflow window.
*
* @param menu
* the menu to force icons to show
*/
public static void forceMenuIcons(Menu menu) {
try {
Class<?> MenuBuilder = menu.getClass();
Method setOptionalIconsVisible =
MenuBuilder.getDeclaredMethod("setOptionalIconsVisible", boolean.class);
if (!setOptionalIconsVisible.isAccessible()) {
setOptionalIconsVisible.setAccessible(true);
}
setOptionalIconsVisible.invoke(menu, true);
} catch (Exception ignored) {
}
}
public static Builder on(Menu menu) {
return new Builder(menu);
}
/**
* Apply a ColorFilter with the specified color to all icons in the menu.
*
* @param activity
* the Activity.
* @param menu
* the menu after items have been added.
* @param color
* the color for the ColorFilter.
*/
public static void colorIcons(Activity activity, Menu menu, int color) {
MenuTint.on(menu).setMenuItemIconColor(color).apply(activity);
}
/**
* @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));
}
private 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;
}
private static ViewGroup findActionBar(Activity activity) {
int id = activity.getResources().getIdentifier("action_bar", "id", "android");
ViewGroup actionBar = null;
if (id != 0) {
actionBar = (ViewGroup) activity.findViewById(id);
}
if (actionBar == null) {
actionBar = findToolbar((ViewGroup)
activity.findViewById(android.R.id.content).getRootView());
}
return actionBar;
}
private 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;
}
private final Menu menu;
private final Integer originalMenuItemIconColor;
private final Integer menuItemIconAlpha;
private final Integer subMenuIconColor;
private final Integer subMenuIconAlpha;
private final Integer overflowDrawableId;
private final boolean reApplyOnChange;
private final boolean forceIcons;
private Integer menuItemIconColor;
private ImageView overflowButton;
private ViewGroup actionBarView;
private MenuTint(Builder builder) {
menu = builder.menu;
originalMenuItemIconColor = builder.originalMenuItemIconColor;
menuItemIconColor = builder.menuItemIconColor;
menuItemIconAlpha = builder.menuItemIconAlpha;
subMenuIconColor = builder.subMenuIconColor;
subMenuIconAlpha = builder.subMenuIconAlpha;
overflowDrawableId = builder.overflowDrawableId;
reApplyOnChange = builder.reApplyOnChange;
forceIcons = builder.forceIcons;
}
/**
* <p>Sets a ColorFilter and/or alpha on all the {@link MenuItem}s in the menu, including the
* OverflowMenuButton.</p>
*
* <p>Call this method after inflating/creating your menu in
* {@link Activity#onCreateOptionsMenu(Menu)}.</p>
*
* <p>Note: This is targeted for the native ActionBar/Toolbar, not AppCompat.</p>
*
* @param activity
* the activity to apply the menu tinting on.
*/
public void apply(final Activity activity) {
if (forceIcons) {
forceMenuIcons(menu);
}
for (int i = 0, size = menu.size(); i < size; i++) {
MenuItem item = menu.getItem(i);
colorMenuItem(item, menuItemIconColor, menuItemIconAlpha);
if (reApplyOnChange) {
View view = item.getActionView();
if (view != null) {
if (item instanceof MenuItemImpl) {
((MenuItemImpl) item).setSupportOnActionExpandListener(
new SupportActionExpandListener(this));
} else {
item.setOnActionExpandListener(new NativeActionExpandListener(this));
}
}
}
}
actionBarView = findActionBar(activity);
if (actionBarView == null) {
Log.w(TAG, "Could not find the ActionBar");
return;
}
// We must wait for the view to be created to set a color filter on the drawables.
actionBarView.post(new Runnable() {
@Override public void run() {
for (int i = 0, size = menu.size(); i < size; i++) {
MenuItem menuItem = menu.getItem(i);
if (isInOverflow(menuItem)) {
colorMenuItem(menuItem, subMenuIconColor, subMenuIconAlpha);
}
if (menuItem.hasSubMenu()) {
SubMenu subMenu = menuItem.getSubMenu();
for (int j = 0; j < subMenu.size(); j++) {
colorMenuItem(subMenu.getItem(j), subMenuIconColor, subMenuIconAlpha);
}
}
}
if (menuItemIconColor != null || menuItemIconAlpha != null) {
overflowButton = findOverflowMenuButton(activity, actionBarView);
colorOverflowMenuItem(overflowButton);
}
}
});
}
/**
* <p>Sets a ColorFilter and/or alpha on all the {@link MenuItem}s in the menu, including the
* OverflowMenuButton.</p>
*
* <p>This should only be called after calling {@link #apply(Activity)}. It is useful for when
* {@link MenuItem}s might be re-arranged due to an action view being collapsed or expanded.</p>
*/
public void reapply() {
for (int i = 0, size = menu.size(); i < size; i++) {
MenuItem item = menu.getItem(i);
if (isActionButton(item)) {
colorMenuItem(menu.getItem(i), menuItemIconColor, menuItemIconAlpha);
}
}
if (actionBarView == null) {
return;
}
actionBarView.post(new Runnable() {
@Override public void run() {
for (int i = 0, size = menu.size(); i < size; i++) {
MenuItem menuItem = menu.getItem(i);
if (isInOverflow(menuItem)) {
colorMenuItem(menuItem, subMenuIconColor, subMenuIconAlpha);
} else {
colorMenuItem(menu.getItem(i), menuItemIconColor, menuItemIconAlpha);
}
if (menuItem.hasSubMenu()) {
SubMenu subMenu = menuItem.getSubMenu();
for (int j = 0; j < subMenu.size(); j++) {
colorMenuItem(subMenu.getItem(j), subMenuIconColor, subMenuIconAlpha);
}
}
}
if (menuItemIconColor != null || menuItemIconAlpha != null) {
colorOverflowMenuItem(overflowButton);
}
}
});
}
private void colorOverflowMenuItem(ImageView overflow) {
if (overflow != null) {
if (overflowDrawableId != null) {
overflow.setImageResource(overflowDrawableId);
}
if (menuItemIconColor != null) {
overflow.setColorFilter(menuItemIconColor);
}
if (menuItemIconAlpha != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
overflow.setImageAlpha(menuItemIconAlpha);
} else {
overflow.setAlpha(menuItemIconAlpha);
}
}
}
}
public Menu getMenu() {
return menu;
}
public ImageView getOverflowMenuButton() {
return overflowButton;
}
public void setMenuItemIconColor(Integer color) {
menuItemIconColor = color;
}
public static class NativeActionExpandListener implements OnActionExpandListener {
private final MenuTint menuTint;
public NativeActionExpandListener(MenuTint menuTint) {
this.menuTint = menuTint;
}
@Override public boolean onMenuItemActionExpand(MenuItem item) {
int color = menuTint.originalMenuItemIconColor == null ? menuTint.menuItemIconColor :
menuTint.originalMenuItemIconColor;
menuTint.setMenuItemIconColor(color);
menuTint.reapply();
return true;
}
@Override public boolean onMenuItemActionCollapse(MenuItem item) {
int color = menuTint.originalMenuItemIconColor == null ? menuTint.menuItemIconColor :
menuTint.originalMenuItemIconColor;
menuTint.setMenuItemIconColor(color);
menuTint.reapply();
return true;
}
}
public static class SupportActionExpandListener implements
MenuItemCompat.OnActionExpandListener {
private final MenuTint menuTint;
public SupportActionExpandListener(MenuTint menuTint) {
this.menuTint = menuTint;
}
@Override public boolean onMenuItemActionExpand(MenuItem item) {
int color = menuTint.originalMenuItemIconColor == null ? menuTint.menuItemIconColor :
menuTint.originalMenuItemIconColor;
menuTint.setMenuItemIconColor(color);
menuTint.reapply();
return true;
}
@Override public boolean onMenuItemActionCollapse(MenuItem item) {
int color = menuTint.originalMenuItemIconColor == null ? menuTint.menuItemIconColor :
menuTint.originalMenuItemIconColor;
menuTint.setMenuItemIconColor(color);
menuTint.reapply();
return true;
}
}
// --------------------------------------------------------------------------------------------
public static final class Builder {
private final Menu menu;
private Integer menuItemIconColor;
private Integer menuItemIconAlpha;
private Integer subMenuIconColor;
private Integer subMenuIconAlpha;
private Integer overflowDrawableId;
private Integer originalMenuItemIconColor;
private boolean reApplyOnChange;
private boolean forceIcons;
private Builder(Menu menu) {
this.menu = menu;
}
/**
* <p>Sets an {@link OnActionExpandListener} on all {@link MenuItem}s with views, so when the
* menu is updated, the colors will be also.</p>
*
* <p>This is useful when the overflow menu is showing icons and {@link MenuItem}s might be
* pushed to the overflow menu when a action view is expanded e.g. android.widget.SearchView.
* </p>
*
* @param reapply
* {@code true} to set the listeners on all {@link MenuItem}s with action views.
* @return this Builder object to allow for chaining of calls to set methods
*/
public Builder reapplyOnChange(boolean reapply) {
reApplyOnChange = reapply;
return this;
}
/**
* Specify an alpha value for visible MenuItem icons, including the OverflowMenuButton.
*
* @param alpha
* the alpha value for the drawable. 0 means fully transparent, and 255 means fully
* opaque.
* @return this Builder object to allow for chaining of calls to set methods
*/
public Builder setMenuItemIconAlpha(int alpha) {
menuItemIconAlpha = alpha;
return this;
}
/**
* Specify a color for visible MenuItem icons, including the OverflowMenuButton.
*
* @param color
* the color to apply on visible MenuItem icons, including the OverflowMenuButton.
* @return this Builder object to allow for chaining of calls to set methods
*/
public Builder setMenuItemIconColor(int color) {
menuItemIconColor = color;
return this;
}
/**
* Specify a color that is applied when an action view is expanded or collapsed.
*
* @param color
* the color to apply on MenuItems when an action-view is expanded or collapsed.
* @return this Builder object to allow for chaining of calls to set methods
*/
public Builder setOriginalMenuItemIconColor(int color) {
originalMenuItemIconColor = color;
return this;
}
/**
* Set the drawable id to set on the OverflowMenuButton.
*
* @param drawableId
* the resource identifier of the drawable
* @return this Builder object to allow for chaining of calls to set methods
*/
public Builder setOverflowDrawableId(int drawableId) {
overflowDrawableId = drawableId;
return this;
}
/**
* Specify an alpha value for MenuItems that are in a SubMenu or in the Overflow menu.
*
* @param alpha
* the alpha value for the drawable. 0 means fully transparent, and 255 means fully
* opaque.
* @return this Builder object to allow for chaining of calls to set methods
*/
public Builder setSubMenuIconAlpha(int alpha) {
subMenuIconAlpha = alpha;
return this;
}
/**
* Specify a color for MenuItems that are in a SubMenu or in the Overflow menu.
*
* @param color
* the color to apply on visible MenuItem icons, including the OverflowMenuButton.
* @return this Builder object to allow for chaining of calls to set methods
*/
public Builder setSubMenuIconColor(int color) {
subMenuIconColor = color;
return this;
}
/**
* Set the menu to show MenuItem icons in the overflow window.
*
* @return this Builder object to allow for chaining of calls to set methods
*/
public Builder forceIcons() {
forceIcons = true;
return this;
}
/**
* <p>Sets a ColorFilter and/or alpha on all the MenuItems in the menu, including the
* OverflowMenuButton.</p>
*
* <p>Call this method after inflating/creating your menu in</p>
* {@link Activity#onCreateOptionsMenu(Menu)}.</p>
*
* <p>Note: This is targeted for the native ActionBar/Toolbar, not AppCompat.</p>
*/
public MenuTint apply(Activity activity) {
MenuTint theme = new MenuTint(this);
theme.apply(activity);
return theme;
}
/**
* <p>Creates a {@link MenuTint} with the arguments supplied to this builder.</p>
*
* <p>It does not apply the theme. Call {@link MenuTint#apply(Activity)} to do so.</p>
*
* @see #apply(Activity)
*/
public MenuTint create() {
return new MenuTint(this);
}
}
// --------------------------------------------------------------------------------------------
/**
* Auto collapses the SearchView when the soft keyboard is dismissed.
*/
public static class SearchViewFocusListener implements View.OnFocusChangeListener {
private final MenuItem item;
public SearchViewFocusListener(MenuItem item) {
this.item = item;
}
@Override public void onFocusChange(View v, boolean hasFocus) {
if (!hasFocus && item != null) {
item.collapseActionView();
if (v instanceof SearchView) {
((SearchView) v).setQuery("", false);
}
}
}
}
}
@efemoney
Copy link

efemoney commented Mar 9, 2015

This is great. Thank You.

@aldefy
Copy link

aldefy commented Apr 19, 2015

Getting a crash with colorOverflow , getchildcount

@KennyGoers
Copy link

Works great for my needs, I just was moving up to the O build target and it won't build now. The SupportActionExpandListener and related calls have been removed and not replaced. Just thought I'd give you a heads up.

@Sandrichka22
Copy link

This worked for me too. How can we overcome the problem with the missing listener from Android O?

@sujayabraham
Copy link

I just moved to the O buildtarget and it's not building now. SupportActionExpandListene is removed and it's not updated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment