17

I've been looking for a method to programmatically change the color of the overflow menu icon in android.

The only option I have found is to change the icon permanently by adding a custom style. The problem is that in the nearby future we will need to change this during the use of our app.

Our app is an extension to a series of online-platforms and therefore a user can enter their platform's web-url. These have their own styles and will be fetched by an API call towards the app.

These might adress me to change the color of the icon...

Currently I change other icons in the Actionbar like this:

if (ib != null){
            Drawable resIcon = getResources().getDrawable(R.drawable.navigation_refresh);
            resIcon.mutate().setColorFilter(StyleClass.getColor("color_navigation_icon_overlay"), PorterDuff.Mode.SRC_ATOP);
            ib.setIcon(resIcon);
}

For now I'll have to use the styles.

Mathijs Segers
  • 6,168
  • 9
  • 51
  • 75

10 Answers10

33

You actually can programmatically change the overflow icon using a little trick. Here's an example:

Create a style for the overflow menu and pass in a content description

<style name="Widget.ActionButton.Overflow" parent="@android:style/Widget.Holo.ActionButton.Overflow">
    <item name="android:contentDescription">@string/accessibility_overflow</item>
</style>

<style name="Your.Theme" parent="@android:style/Theme.Holo.Light.DarkActionBar">
    <item name="android:actionOverflowButtonStyle">@style/Widget.ActionButton.Overflow</item>
</style>

Now call ViewGroup.findViewsWithText and pass in your content description. So, something like:

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // The content description used to locate the overflow button
    final String overflowDesc = getString(R.string.accessibility_overflow);
    // The top-level window
    final ViewGroup decor = (ViewGroup) getWindow().getDecorView();
    // Wait a moment to ensure the overflow button can be located
    decor.postDelayed(new Runnable() {

        @Override
        public void run() {
            // The List that contains the matching views
            final ArrayList<View> outViews = new ArrayList<>();
            // Traverse the view-hierarchy and locate the overflow button
            decor.findViewsWithText(outViews, overflowDesc,
                    View.FIND_VIEWS_WITH_CONTENT_DESCRIPTION);
            // Guard against any errors
            if (outViews.isEmpty()) {
                return;
            }
            // Do something with the view
            final ImageButton overflow = (ImageButton) outViews.get(0);
            overflow.setImageResource(R.drawable.ic_action_overflow_round_red);

        }

    }, 1000);
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    // Add a dummy item to the overflow menu
    menu.add("Overflow");
    return super.onCreateOptionsMenu(menu);
}

View.findViewsWithText was added in API level 14, so you'll have to use your own compatibility method:

static void findViewsWithText(List<View> outViews, ViewGroup parent, String targetDescription) {
    if (parent == null || TextUtils.isEmpty(targetDescription)) {
        return;
    }
    final int count = parent.getChildCount();
    for (int i = 0; i < count; i++) {
        final View child = parent.getChildAt(i);
        final CharSequence desc = child.getContentDescription();
        if (!TextUtils.isEmpty(desc) && targetDescription.equals(desc.toString())) {
            outViews.add(child);
        } else if (child instanceof ViewGroup && child.getVisibility() == View.VISIBLE) {
            findViewsWithText(outViews, (ViewGroup) child, targetDescription);
        }
    }
}

Results

Example

adneal
  • 30,484
  • 10
  • 122
  • 151
  • 1
    @MathijsSegers I edited my answer, recently realized that you can dynamically change it. – adneal Jun 04 '14 at 12:51
  • Interesting, I'll look into this but I don't know if I'll ever need it, but the chances are there that I do. Thanks! – Mathijs Segers Jun 05 '14 at 08:17
  • Instead of the timeout you could probably also use a `ViewTreeObserver.OnPreDrawListener`: http://stackoverflow.com/questions/4393612/when-can-i-first-measure-a-view – Hannes Struß Nov 24 '14 at 14:05
  • such a bad programming calling delayed method (in 1 sec) – user25 Feb 02 '19 at 19:39
22

Adneal's answer is great and I was using it until recently. But then I wanted my app to make use of material design and thus Theme.AppCompat.* style and android.support.v7.widget.Toolbar.

Yes, it stopped working and I was trying to fix it by setting Your.Theme's parent to @style/Widget.AppCompat.ActionButton.Overflow. It worked by propertly setting contentDescription but then it failed when casting to ImageButton. It turned out in latest (version 23) android.support.v7class OverflowMenuButton extends from AppCompatImageView. Changing casting class was enought to make it work with Toolbar on Nexus 5 running Lollipop.

Then I ran it on Galaxy S4 with KitKat and no matter what I tried I couldn't set overflow's contentDescription to my custom value. But in AppCompat styles I found it already has default value:

<item name="android:contentDescription">@string/abc_action_menu_overflow_description</item>

So why not use it? Also by Hannes idea (in comments) I implemented listener, to get rid of some random time for delay in postDelayed. And as overflow icon is already in AppCompat library, then I would use it as well - I am applying color filter, so I don't need any icon resource on my own.

My code based on Adneal's work with Android Lollipop improvements:

public static void setOverflowButtonColor(final Activity activity) {
    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;
            }
            AppCompatImageView overflow=(AppCompatImageView) outViews.get(0);
            overflow.setColorFilter(Color.CYAN);
            removeOnGlobalLayoutListener(decorView,this);
        }
    });
}

and as per another StackOverflow answer:

public static void removeOnGlobalLayoutListener(View v, ViewTreeObserver.OnGlobalLayoutListener listener) {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
        v.getViewTreeObserver().removeGlobalOnLayoutListener(listener);
    }
    else {
        v.getViewTreeObserver().removeOnGlobalLayoutListener(listener);
    }
}

of course instead of Color.CYAN you can use your own color - activity.getResources().getColor(R.color.black);

EDIT: Added support for latest AppCompat library (23), which uses AppCompatImageView For AppCompat 22 you should cast overflow button to TintImageView

Community
  • 1
  • 1
michalbrz
  • 3,354
  • 1
  • 30
  • 41
20

As of support 23.1 Toolbar now has getOverflowIcon() and setOverflowIcon() methods, so we can do this much more easily:

public static void setOverflowButtonColor(final Toolbar toolbar, final int color) {
    Drawable drawable = toolbar.getOverflowIcon();
    if(drawable != null) {
        drawable = DrawableCompat.wrap(drawable);
        DrawableCompat.setTint(drawable.mutate(), color);
        toolbar.setOverflowIcon(drawable);
    }
}
Lorne Laliberte
  • 6,261
  • 2
  • 35
  • 36
  • I updated to 23.1 and tried your code but it is not working for me :S. Any additional advice or help to achieve it? – MiguelHincapieC Nov 05 '15 at 21:14
  • Hmmm...I've tested it working on several devices running 6.0, 5.0.1, and 4.4.4. I have targetSdkVersion set to 23. – Lorne Laliberte Nov 05 '15 at 21:39
  • Tried what you suggest (in my answer) and this one on 4.2.2 device without success... what I'm missing O_O' – MiguelHincapieC Nov 05 '15 at 21:56
  • 1
    @0mahc0 you updated to 23.1, but did you also set your compiler options/targetSdkVersion to 23? I actually have appcompat and other libraries building to 23 instead of 19 (by changing the targets in their project.properties, build settings, etc.). Aside from that, are you sure you're actually using the toolbar in your layout, and not the support actionbar? (If you're working from old code it's possible the activity might still be using the actionbar, with a toolbar defined but going unused. Which one you actually see at run time will depend on what you assign for the activity in code.) – Lorne Laliberte Nov 17 '15 at 22:35
  • Guess you are right, I'm going to update all those things. – MiguelHincapieC Nov 18 '15 at 14:42
  • Using the original icon with setTint is a clever little hack! – Rupert Rawnsley Dec 09 '15 at 11:35
  • Can you please guide me to change the whole image, not just the color, I'm trying to change that to a tick. – Ari Apr 29 '16 at 15:55
  • @Ari Just using `setOverflowIcon()` with the desired image as drawable should work. – Boris Sep 20 '16 at 16:00
  • Way easier than the other solutions posted here, thanks! – Paul Woitaschek Dec 08 '16 at 11:29
  • @PaulWoitaschek thanks -- it wasn't so easy to begin with, but I submitted a ticket to Google and they added those methods. :) – Lorne Laliberte Dec 08 '16 at 17:51
13

There is much better solution. You can do it programmatically in the runtime

toolbar.overflowIcon?.setColorFilter(colorInt, PorterDuff.Mode.SRC_ATOP)

Viola!

Jacek Kwiecień
  • 12,397
  • 20
  • 85
  • 157
  • Sure there is now, this question however is three years old, pre KitKat :-). Back then we didn't work with toolbars I believe. – Mathijs Segers Aug 21 '17 at 06:14
  • I wish I could give you 10 thumbs up. I've been working on this problem for hours. Some people said to create your own three dot icon and have one set with one color and one set with another color, and to swap between them. Absolute overkill. The answer turned out to be something so simple. – Peter Griffin Aug 18 '18 at 12:49
7

There is the less-hacky solution for changing the overflow icon. There is an example how to change overflow icon's color, but you can adapt it to change the image:

 private void setOverflowIconColor(int color) {
        Drawable overflowIcon = toolbar.getOverflowIcon();

        if (overflowIcon != null) {
            Drawable newIcon = overflowIcon.mutate();
            newIcon.setColorFilter(color, PorterDuff.Mode.MULTIPLY);
            toolbar.setOverflowIcon(newIcon);
        }
    }
artem
  • 16,382
  • 34
  • 113
  • 189
  • 1
    FYI this requires API v23 I believe – John Gibb May 23 '16 at 19:11
  • @JohnGibb nope, tested on API level 19 – artem May 24 '16 at 17:20
  • 1
    says "Added in API Level 23" here: https://developer.android.com/reference/android/widget/Toolbar.html#setOverflowIcon(android.graphics.drawable.Drawable). Also gives me a warning in Android studio. – John Gibb May 24 '16 at 20:27
  • 1
    @JohnGibb make sure you're using AppCompat version of the Toolbar. https://developer.android.com/reference/android/support/v7/widget/Toolbar.html#setOverflowIcon(android.graphics.drawable.Drawable) – artem May 24 '16 at 20:38
1

Based on @michalbrz answer, I used the below to change the icon itself. :)

public static void setOverflowButtonColor(final Activity activity) {
        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(Color.CYAN); //changes color
            overflow.setImageResource(R.drawable.dots);
            removeOnGlobalLayoutListener(decorView, this);
        }
    });
Atul O Holic
  • 6,692
  • 4
  • 39
  • 74
1

Using appcompat-v7:23.0.1 none of @adneal or @michalbrz worked for me. I had to change 2 lines of code of @michalbrz's answer to make it works.

I'm adding an answer because both current answers can be useful for someone, but if you are using last appcompat version like me you should use this one based on @michalbrz:

private static void setOverflowButtonColor(final AppCompatActivity 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<>();
            decorView.findViewsWithText(outViews, overflowDescription,
                    View.FIND_VIEWS_WITH_CONTENT_DESCRIPTION);
            if (outViews.isEmpty()) {
                return;
            }
            ActionMenuItemView overflow = (ActionMenuItemView)outViews.get(0);
            overflow.getCompoundDrawables()[0].setColorFilter(colorFilter);
            removeOnGlobalLayoutListener(decorView,this);
        }
    });
}

Using michalbrz code I was getting this error:

java.lang.ClassCastException: android.support.v7.internal.view.menu.ActionMenuItemView cannot be cast to android.support.v7.internal.widget.TintImageView

So, after digging a little in ActionMenuItemView's code, I found how to get the icon's drawable (looking in setIcon()), then I just changed casting to ActionMenuItemView, applied color filter to left drawable got from getCompoundDrawables() and Voila! it works!

MiguelHincapieC
  • 5,445
  • 7
  • 41
  • 72
  • Makes sense since it's a newer support library doing other things as the original question. But this might prove valueable to many. – Mathijs Segers Sep 17 '15 at 06:23
  • This doesn't work (yet again!) as of 23.1, you need to cast to `ImageView` and use `overflow.setColorFilter(colorFilter)` instead. (The button is now implemented as an `ActionMenuPresenter.OverflowMenuButton` but that has private access.) – Lorne Laliberte Nov 04 '15 at 22:48
  • OMG!! they change it so much :S, I'm gonna try and update the answer (or modify it if u want) – MiguelHincapieC Nov 04 '15 at 22:53
  • Actually there's an easier way to do it as of 23.1 since they've added getOverflowIcon() and setOverflowIcon() -- but even so, I've upvoted your answer as I found it helpful while it lasted. :) – Lorne Laliberte Nov 04 '15 at 23:32
  • Thx @Lorne Laliberte, I'm trying with `toolbar.getOverflowIcon().setColorFilter()` but it isn't working, can you give me and advice?. I updated to 23.1 by the way and my code is still working with a warning about a private string resource.... NVM I just realized about your answer hahaha ;) – MiguelHincapieC Nov 05 '15 at 20:55
  • Are you also calling `toolbar.setOverflowIcon()` to update the drawable? Something like `toolbar.setOverflowIcon(toolbar.getOverflowIcon().mutate().setColorFilter(colorFilter))` is probably closer to what you need. You might want to use the overload of setColorFilter that lets you pass the filter, e.g. so you can make it use PorterDuff.Mode.SRC_IN instead of SRC_ATOP (which generally makes more sense for this, but it depends on what you want to do with it). – Lorne Laliberte Nov 05 '15 at 21:06
1

Guys I've done this in a simple way, Please look on this snippet as follows:

@Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_dashboard, menu);
        MenuItem item = menu.findItem(R.id.help);
        Drawable drawable = item.getIcon();
        drawable.setColorFilter(Color.RED, PorterDuff.Mode.SRC_ATOP);
        return super.onCreateOptionsMenu(menu);
    }

Please let me know if any more clarification here.

Deva
  • 2,386
  • 1
  • 16
  • 16
1

Don't need to create the new style resource, just use setOvewflowIcon(drawable) method to the toolbar object and pass the drawable that you want to use as icon

Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
       toolbar.setOverflowIcon(getResources().getDrawable(R.drawable.ic_notifications_black_24dp));
Sumit Monapara
  • 840
  • 7
  • 13
  • 1
    Besides that this post is 4 years old, I needed a solution which does not require me to add all possible colours icons to my app. Which was supported. – Mathijs Segers Aug 07 '18 at 10:54
  • But brother this is overflow icon, not an color that you have to change it on every activity. and other thing in dynamically what if my theme is "Theme.AppCompat.Light.DarkActionBar" ,then it don't find "holo actionbar" theme. so just think about it. – Sumit Monapara Aug 07 '18 at 15:56
  • The theme is based on color strings fetched from an API. Again your answer doesn't answer the actual question. – Mathijs Segers Aug 08 '18 at 08:27
  • It's ok no problem. Because it work perfect in my app.Thanks. – Sumit Monapara Aug 11 '18 at 16:52
0

toolBar.overflowIcon?.setTint(Color.WHITE)

in kotlin, change any color your want :)

subrata sharma
  • 344
  • 4
  • 17