Changing the Android Overflow menu icon programmatically
Asked Answered
L

10

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.

Laural answered 26/2, 2014 at 16:9 Comment(2)
I believe that the only way is with a custom Style. I don't believe that you can chang it programmatically.Hawes
I kinda have the same feeling, just hoping I'm wrong.Laural
S
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

Sexism answered 28/2, 2014 at 21:48 Comment(4)
@MathijsSegers I edited my answer, recently realized that you can dynamically change it.Sexism
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!Laural
Instead of the timeout you could probably also use a ViewTreeObserver.OnPreDrawListener: #4394112Polygon
such a bad programming calling delayed method (in 1 sec)Mielke
S
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

Stites answered 28/12, 2014 at 0:50 Comment(1)
Brilliant! Thanks :)Synoptic
W
21

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);
    }
}
Wile answered 4/11, 2015 at 23:26 Comment(10)
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?Metathesize
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.Wile
Tried what you suggest (in my answer) and this one on 4.2.2 device without success... what I'm missing O_O'Metathesize
@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.)Wile
Guess you are right, I'm going to update all those things.Metathesize
Using the original icon with setTint is a clever little hack!Genseric
Can you please guide me to change the whole image, not just the color, I'm trying to change that to a tick.Depressed
@Depressed Just using setOverflowIcon() with the desired image as drawable should work.Payton
Way easier than the other solutions posted here, thanks!Haehaecceity
@PaulWoitaschek thanks -- it wasn't so easy to begin with, but I submitted a ticket to Google and they added those methods. :)Wile
G
13

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

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

Viola!

Grant answered 18/8, 2017 at 10:25 Comment(2)
Sure there is now, this question however is three years old, pre KitKat :-). Back then we didn't work with toolbars I believe.Laural
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.Gleeman
U
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);
        }
    }
Undesirable answered 2/3, 2016 at 20:15 Comment(4)
FYI this requires API v23 I believeStanton
@JohnGibb nope, tested on API level 19Undesirable
says "Added in API Level 23" here: developer.android.com/reference/android/widget/…. Also gives me a warning in Android studio.Stanton
@JohnGibb make sure you're using AppCompat version of the Toolbar. developer.android.com/reference/android/support/v7/widget/…Undesirable
C
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);
        }
    });
Curule answered 27/8, 2015 at 12:6 Comment(0)
M
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!

Metathesize answered 16/9, 2015 at 19:44 Comment(6)
Makes sense since it's a newer support library doing other things as the original question. But this might prove valueable to many.Laural
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.)Wile
OMG!! they change it so much :S, I'm gonna try and update the answer (or modify it if u want)Metathesize
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. :)Wile
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 ;)Metathesize
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).Wile
D
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.

Deviation answered 15/4, 2016 at 6:54 Comment(1)
This is for changing a menu item color, not the overflow iconMontes
A
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));
Aquiline answered 4/8, 2018 at 9:13 Comment(4)
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.Laural
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.Aquiline
The theme is based on color strings fetched from an API. Again your answer doesn't answer the actual question.Laural
It's ok no problem. Because it work perfect in my app.Thanks.Aquiline
D
0

toolBar.overflowIcon?.setTint(Color.WHITE)

in kotlin, change any color your want :)

Decolorant answered 18/2, 2021 at 4:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.