Changing the Android Overflow menu icon programmatically

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.


Solution 1:

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

Solution 2:

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

Solution 3:

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);
    }
}

Solution 4:

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

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

Viola!