111

When I use drawables from the AppCompat library for my Toolbar menu items the tinting works as expected. Like this:

<item
    android:id="@+id/action_clear"
    android:icon="@drawable/abc_ic_clear_mtrl_alpha"  <-- from AppCompat
    android:title="@string/clear" />

But if I use my own drawables or actually even copy the drawables from the AppCompat library to my own project it will not tint at all.

<item
    android:id="@+id/action_clear"
    android:icon="@drawable/abc_ic_clear_mtrl_alpha_copy"  <-- copy from AppCompat
    android:title="@string/clear" />

Is there some special magic in the AppCompat Toolbar that only tint drawables from that library? Any way to get this to work with my own drawables?

Running this on API Level 19 device with compileSdkVersion = 21 and targetSdkVersion = 21, and also using everything from AppCompat

abc_ic_clear_mtrl_alpha_copy is an exact copy of the abc_ic_clear_mtrl_alpha png from AppCompat

Edit:

The tinting is based on the value I have set for android:textColorPrimary in my theme.

E.g. <item name="android:textColorPrimary">#00FF00</item> would give me a green tint color.

Screenshots

Tinting working as expected with drawable from AppCompat Tinting working as expected with drawable from AppCompat

Tinting not working with drawable copied from AppCompat Tinting not working with drawable copied from AppCompat

mariusgreve
  • 2,621
  • 6
  • 23
  • 31

9 Answers9

105

After the new Support library v22.1, you can use something similar to this:

  @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_home, menu);
        Drawable drawable = menu.findItem(R.id.action_clear).getIcon();

        drawable = DrawableCompat.wrap(drawable);
        DrawableCompat.setTint(drawable, ContextCompat.getColor(this,R.color.textColorPrimary));
        menu.findItem(R.id.action_clear).setIcon(drawable);
        return true;
    }
Gene Bo
  • 11,284
  • 8
  • 90
  • 137
Mahdi Hijazi
  • 4,424
  • 3
  • 24
  • 29
  • 1
    I'd say that in this case the old `setColorFilter()` is way preferable. – natario Apr 28 '15 at 10:15
  • @mvai , why setColorFilter() is more prefferable? – wilddev Sep 09 '15 at 11:48
  • 4
    @wilddev brevity. Why bother the support DrawableCompat class when you can go menu.findItem().getIcon().setColorFilter() ? One liner and clear. – natario Sep 09 '15 at 14:42
  • 5
    The one-liner argument is irrelevant when you abstract away the entire logic into your own TintingUtils.tintMenuIcon(...) method or whatever you want to call it. If you need to change or adjust the logic in the future you do it in one place, not all over the application. – Dan Dar3 Sep 27 '15 at 16:27
  • There is no need for different logics to tint and no need to not change in future. – Jemshit Sep 13 '17 at 14:33
  • Two notes here - (1) you need to call `unwrap` to get the original drawable type; and more importantly, (2) wrapping a drawable stores the colored instance in a (small but relevant) cache for appcompat resource loader - this means that the next time you try to load the same drawable, you get the recolored instance. – milosmns Oct 22 '17 at 14:55
98

app:iconTint attribute is implemented in SupportMenuInflater from the support library (at least in 28.0.0).

Tested successfully with API 15 and up.

Menu resource file:

<menu
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
        android:id="@+id/menu_settings"
        android:icon="@drawable/ic_settings_white_24dp"
        app:iconTint="?attr/appIconColorEnabled"        <!-- using app name space instead of android -->
        android:menuCategory="system"
        android:orderInCategory="1"
        android:title="@string/menu_settings"
        app:showAsAction="never"
        />

    <item
        android:id="@+id/menu_themes"
        android:icon="@drawable/ic_palette_white_24dp"
        app:iconTint="?attr/appIconColorEnabled"
        android:menuCategory="system"
        android:orderInCategory="2"
        android:title="@string/menu_themes"
        app:showAsAction="never"
        />

    <item
        android:id="@+id/action_help"
        android:icon="@drawable/ic_help_white_24dp"
        app:iconTint="?attr/appIconColorEnabled"
        android:menuCategory="system"
        android:orderInCategory="3"
        android:title="@string/menu_help"
        app:showAsAction="never"
        />

</menu>

(In this case ?attr/appIconColorEnabled was a custom color attribute in the app's themes, and the icon resources were vector drawables.)

Afilu
  • 1,199
  • 9
  • 4
  • 18
    This should be the new accepted answer! Also, please note `android:iconTint` and `android:iconTintMode` do not work, but prefixing with `app:` instead of `android:` works like a charm (on my own vector drawables, API >=21) – Sebastiaan Alvarez Rodriguez Aug 19 '19 at 22:39
  • If calling programmatically: note that `SupportMenuInflater` won't apply any custom logic if the menu isn't a `SupportMenu` like `MenuBuilder`, it just falls back to regular `MenuInflater`. – geekley Feb 05 '20 at 21:39
  • In this case, use `AppCompatActivity.startSupportActionMode(callback)` and the appropriate support implementations from `androidx.appcompat` will be passed into the callback. – geekley Feb 06 '20 at 00:18
  • This is the right answer! – jameseronious Nov 22 '21 at 19:06
86

Setting a ColorFilter (tint) on a MenuItem is simple. Here is an example:

Drawable drawable = menuItem.getIcon();
if (drawable != null) {
    // If we don't mutate the drawable, then all drawable's with this id will have a color
    // filter applied to it.
    drawable.mutate();
    drawable.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
    drawable.setAlpha(alpha);
}

The above code is very helpful if you want to support different themes and you don't want to have extra copies just for the color or transparency.

Click here for a helper class to set a ColorFilter on all the drawables in a menu, including the overflow icon.

In onCreateOptionsMenu(Menu menu) just call MenuColorizer.colorMenu(this, menu, color); after inflating your menu and voila; your icons are tinted.

Jared Rummler
  • 37,824
  • 19
  • 133
  • 148
31

Because if you take a look at the source code of the TintManager in AppCompat, you will see:

/**
 * Drawables which should be tinted with the value of {@code R.attr.colorControlNormal},
 * using the default mode.
 */
private static final int[] TINT_COLOR_CONTROL_NORMAL = {
        R.drawable.abc_ic_ab_back_mtrl_am_alpha,
        R.drawable.abc_ic_go_search_api_mtrl_alpha,
        R.drawable.abc_ic_search_api_mtrl_alpha,
        R.drawable.abc_ic_commit_search_api_mtrl_alpha,
        R.drawable.abc_ic_clear_mtrl_alpha,
        R.drawable.abc_ic_menu_share_mtrl_alpha,
        R.drawable.abc_ic_menu_copy_mtrl_am_alpha,
        R.drawable.abc_ic_menu_cut_mtrl_alpha,
        R.drawable.abc_ic_menu_selectall_mtrl_alpha,
        R.drawable.abc_ic_menu_paste_mtrl_am_alpha,
        R.drawable.abc_ic_menu_moreoverflow_mtrl_alpha,
        R.drawable.abc_ic_voice_search_api_mtrl_alpha,
        R.drawable.abc_textfield_search_default_mtrl_alpha,
        R.drawable.abc_textfield_default_mtrl_alpha
};

/**
 * Drawables which should be tinted with the value of {@code R.attr.colorControlActivated},
 * using the default mode.
 */
private static final int[] TINT_COLOR_CONTROL_ACTIVATED = {
        R.drawable.abc_textfield_activated_mtrl_alpha,
        R.drawable.abc_textfield_search_activated_mtrl_alpha,
        R.drawable.abc_cab_background_top_mtrl_alpha
};

/**
 * Drawables which should be tinted with the value of {@code android.R.attr.colorBackground},
 * using the {@link android.graphics.PorterDuff.Mode#MULTIPLY} mode.
 */
private static final int[] TINT_COLOR_BACKGROUND_MULTIPLY = {
        R.drawable.abc_popup_background_mtrl_mult,
        R.drawable.abc_cab_background_internal_bg,
        R.drawable.abc_menu_hardkey_panel_mtrl_mult
};

/**
 * Drawables which should be tinted using a state list containing values of
 * {@code R.attr.colorControlNormal} and {@code R.attr.colorControlActivated}
 */
private static final int[] TINT_COLOR_CONTROL_STATE_LIST = {
        R.drawable.abc_edit_text_material,
        R.drawable.abc_tab_indicator_material,
        R.drawable.abc_textfield_search_material,
        R.drawable.abc_spinner_mtrl_am_alpha,
        R.drawable.abc_btn_check_material,
        R.drawable.abc_btn_radio_material
};

/**
 * Drawables which contain other drawables which should be tinted. The child drawable IDs
 * should be defined in one of the arrays above.
 */
private static final int[] CONTAINERS_WITH_TINT_CHILDREN = {
        R.drawable.abc_cab_background_top_material
};

Which pretty much means they have particular resourceIds whitelisted to be tinted.

But I guess you can always see how they're tinting those images and do the same. It's as easy as set the ColorFilter on a drawable.

EvilDuck
  • 4,386
  • 23
  • 32
  • Ugh, that's what I was afraid of. I didn't find the source code for AppCompat in the SDK, that's why I didn't find this part myself. Guess I'll have to browse on googlesource.com then. Thanks! – mariusgreve Nov 08 '14 at 14:47
  • 8
    I know it's a tangential question but why is there a white list? If it can do the tinting with these icons then why can't we tint our own icons? Plus, what's the point in making almost everything backward compatible (with AppCompat) when you leave out one of the most important thing: having action bar icons (with custom color). – Zsolt Safrany Mar 09 '15 at 12:02
  • 1
    There is an issue for this in Google's issue tracker that has been marked as fixed, but it doesn't work for me, but you can track it here: https://issuetracker.google.com/issues/37127128 – niknetniko Aug 10 '17 at 19:06
  • They claim this is fixed, but it’s not. Gosh, I loathe Android theme engine, AppCompat and all the crap associated with it. It only works for “Github repo browser” sample apps. – Martin Marconcini Jul 28 '18 at 00:32
31

I personally preferred this approach from this link

Create an XML layout with the following:

<?xml version="1.0" encoding="utf-8"?>
<bitmap
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:src="@drawable/ic_action_something"
    android:tint="@color/color_action_icons_tint"/>

and reference this drawable from your menu:

<item
    android:id="@+id/option_menu_item_something"
    android:icon="@drawable/ic_action_something_tined"
N Jay
  • 1,774
  • 1
  • 17
  • 36
  • 4
    This is my preferred solution. However, it's important to note that, for now, this solution doesn't appear to work when you're using the new vector drawable types as the source. – Michael De Soto Nov 23 '15 at 21:09
  • 1
    @haagmm this solution needs API >= 21. Also it works for vectors too. – Neurotransmitter May 11 '16 at 21:51
  • 1
    And it should not work with vectors, the root tag is `bitmap`. There are other ways to color vectors. Perhaps someone could add vector coloring here too... – milosmns Oct 22 '17 at 13:41
11

Most of the solutions in this thread either use a newer API, or use reflection, or use intensive view lookup to get to the inflated MenuItem.

However, there's a more elegant approach to do that. You need a custom Toolbar, as your "apply custom tint" use case does not play well with public styling/theming API.

public class MyToolbar extends Toolbar {
    ... some constructors, extracting mAccentColor from AttrSet, etc

    @Override
    public void inflateMenu(@MenuRes int resId) {
        super.inflateMenu(resId);
        Menu menu = getMenu();
        for (int i = 0; i < menu.size(); i++) {
            MenuItem item = menu.getItem(i);
            Drawable icon = item.getIcon();
            if (icon != null) {
                item.setIcon(applyTint(icon));
            }
        }
    }
    void applyTint(Drawable icon){
        icon.setColorFilter(
           new PorterDuffColorFilter(mAccentColor, PorterDuff.Mode.SRC_IN)
        );
    }

}

Just make sure you call in your Activity/Fragment code:

toolbar.inflateMenu(R.menu.some_menu);
toolbar.setOnMenuItemClickListener(someListener);

No reflection, no view lookup, and not so much code, huh?

And now you can ignore the ridiculous onCreateOptionsMenu/onOptionsItemSelected.

Steve Liddle
  • 3,685
  • 3
  • 27
  • 39
Drew
  • 3,307
  • 22
  • 33
  • Technically, you are doing a view lookup. You're iterating the views and ensuring they are not null. ;) – Martin Marconcini Mar 28 '17 at 01:49
  • You're definitely right in a way :-) Nonetheless, `Menu#getItem()` complexity is O(1) in the toolbar, because items are stored in ArrayList. Which is different from `View#findViewById` traversal (which I referred to as _view lookup_ in my answer), complexity of which is far from being constant :-) – Drew Mar 28 '17 at 06:32
  • Agreed, in fact, I did a very similar thing. I'm still shocked that Android hasn't streamlined all this after so many years… – Martin Marconcini Mar 28 '17 at 17:19
  • How can I change the overflow icon and hamburger icon's colors with this approach? – Sandra Sep 27 '17 at 08:46
8

Here is the solution that I use; you can call it after onPrepareOptionsMenu() or the equivalent place. The reason for mutate() is if you happen to use the icons in more than one location; without the mutate, they will all take on the same tint.

public class MenuTintUtils {
    public static void tintAllIcons(Menu menu, final int color) {
        for (int i = 0; i < menu.size(); ++i) {
            final MenuItem item = menu.getItem(i);
            tintMenuItemIcon(color, item);
            tintShareIconIfPresent(color, item);
        }
    }

    private static void tintMenuItemIcon(int color, MenuItem item) {
        final Drawable drawable = item.getIcon();
        if (drawable != null) {
            final Drawable wrapped = DrawableCompat.wrap(drawable);
            drawable.mutate();
            DrawableCompat.setTint(wrapped, color);
            item.setIcon(drawable);
        }
    }

    private static void tintShareIconIfPresent(int color, MenuItem item) {
        if (item.getActionView() != null) {
            final View actionView = item.getActionView();
            final View expandActivitiesButton = actionView.findViewById(R.id.expand_activities_button);
            if (expandActivitiesButton != null) {
                final ImageView image = (ImageView) expandActivitiesButton.findViewById(R.id.image);
                if (image != null) {
                    final Drawable drawable = image.getDrawable();
                    final Drawable wrapped = DrawableCompat.wrap(drawable);
                    drawable.mutate();
                    DrawableCompat.setTint(wrapped, color);
                    image.setImageDrawable(drawable);
                }
            }
        }
    }
}

This won't take care of the overflow, but for that, you can do this:

Layout:

<android.support.v7.widget.Toolbar
    ...
    android:theme="@style/myToolbarTheme" />

Styles:

<style name="myToolbarTheme">
        <item name="colorControlNormal">#FF0000</item>
</style>

This works as of appcompat v23.1.0.

Learn OpenGL ES
  • 4,759
  • 1
  • 36
  • 38
3

This worked for me:

override fun onCreateOptionsMenu(menu: Menu?): Boolean {

        val inflater = menuInflater
        inflater.inflate(R.menu.player_menu, menu)

        //tinting menu item:
        val typedArray = theme.obtainStyledAttributes(IntArray(1) { android.R.attr.textColorSecondary })
        val textColor = typedArray.getColor(0, 0)
        typedArray.recycle()

        val item = menu?.findItem(R.id.action_chapters)
        val icon = item?.icon

        icon?.setColorFilter(textColor, PorterDuff.Mode.SRC_IN);
        item?.icon = icon
        return true
    }

Or you can use tint in drawable xml:

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:tint="?android:textColorSecondary"
    android:viewportWidth="384"
    android:viewportHeight="384">
    <path
        android:fillColor="#FF000000"

        android:pathData="M0,277.333h384v42.667h-384z" />
    <path
        android:fillColor="#FF000000"
        android:pathData="M0,170.667h384v42.667h-384z" />
    <path
        android:fillColor="#FF000000"
        android:pathData="M0,64h384v42.667h-384z" />
</vector>
0
@Override
public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.menu_home, menu);
    //One item tint
    menu.get(itemId).getIcon().setTint(Color);
   //or all
    for(int i=0;i<menu.size();i++){
    menu.get(i).getIcon().setTint(Color);
    }
    return true;
}
  • 1
    Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Jan 09 '22 at 20:51