What I am trying to do is showing a PopupWindow pointing to the overflow icon (the three dots) on the Toolbar. So I need to get a reference to the View object with the id of the icon. But what is the id?
The PopupWindow is used to tell the users that there are new entries added to the overflow menu. And suggest users to check it out.
You should create the button id
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item type="id" name="overflowActionButton"/>
</resources>
then create the button style
<style name="Widget.ActionButton.Overflow" parent="Widget.AppCompat.ActionButton.Overflow">
<item name="android:id">#id/overflowActionButton</item>
</style>
and add this style in the theme
<style name="Theme.App" parent="Theme.MaterialComponents.Light.NoActionBar">
<item name="actionOverflowButtonStyle">#style/Widget.ActionButton.Overflow</item>
</style>
finally you should find the button view by id
activity.findViewById(R.id.overflowActionButton)
and do what you want
The overflow menu item doesn't have a resource id. I found the overflow view by traversing the toolbar. The debugger showed an id of -1 and the Hierarchy Viewer showed no resource-id.
Here is how I found the overflow view without a resource id:
/**
* Get the OverflowMenuButton.
*
* #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));
}
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;
}
static ViewGroup findActionBar(Activity activity) {
try {
int id = activity.getResources().getIdentifier("action_bar", "id", "android");
ViewGroup actionBar = null;
if (id != 0) {
actionBar = (ViewGroup) activity.findViewById(id);
}
if (actionBar == null) {
return findToolbar((ViewGroup) activity.findViewById(android.R.id.content).getRootView());
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
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;
}
Calling getOverflowMenuButton(activity) will return null in onCreate because the overflow menu isn't laid out yet. To get the overflow menu in onCreate I did the following:
findViewById(android.R.id.content).post(new Runnable() {
#Override public void run() {
ImageView overflow = getOverflowMenuButton(MainActivity.this);
}
});
I found a library called TapTarget and a function TapTarget.forToolbarOverflow(). It presents a solution: https://github.com/KeepSafe/TapTargetView/blob/master/taptargetview/src/main/java/com/getkeepsafe/taptargetview/TapTarget.java#L96
The way how it finds the overflow view is not neat but should be stable.
you want to create custom DropDown menu? consider this "native" way
or use android:showAsAction="never" in your menu.xml. doc of showAsAction attribute HERE. when one of MenuItems have set never value then you will get overflow three-dot icon automatically and these MenuItems will be hidding there
also you may try to use Hierarchy Viewer to investigate this id if really needed
Instead of using expensive and complicated layout traversal to find the overflow menu, I have achieved showing the PopupWindow under the overflow menu by using the Toolbar view as anchor and setting gravity to Gravity.END:
/**
* Sets the anchor view and shows the popup. In case of narrow display the menu items may be hidden in an overflow
* menu, in that case anchorView may be null and the popup will be anchored to the end of the toolbar.
*/
public void show(#Nullable View anchorView, #NonNull View toolbarView) {
if (anchorView == null) {
setDropDownGravity(Gravity.END);
setAnchorView(toolbarView);
} else {
setAnchorView(anchorView);
}
show();
}
Related
I managed to show icons in the toolbar's overflow menu and submenus, but I couldn't find how to tint the icons according to their position. Here the code I'm using:
#Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.toolbar_main, menu);
// Show icons in overflow menu
if (menu instanceof MenuBuilder) {
MenuBuilder m = (MenuBuilder) menu;
m.setOptionalIconsVisible(true);
}
// Change icons color
changeIconsColor(menu, colorNormal, colorInMenu, false);
return super.onCreateOptionsMenu(menu);
}
public static void changeIconsColor(Menu menu, int colorNormal, int colorInMenu, boolean isInSubMenu) {
// Change icons color
for (int i = 0; i < menu.size(); i++) {
MenuItem item = menu.getItem(i);
Drawable icon = item.getIcon();
if (icon != null) {
int color = (((MenuItemImpl) item).requiresActionButton() ? colorNormal : colorInMenu);
icon.setColorFilter(color, PorterDuff.Mode.SRC_IN);
icon.setAlpha(item.isEnabled() ? 255 : 128);
}
if (item.hasSubMenu()) {
changeIconsColor(item.getSubMenu(), colorNormal, colorInMenu, true);
}
}
}
The use of MenuItem.requiresActionButton() allows to know if an item has the values never or always in the showAsAction attribute in XML, but not if it has the ifRoom value. Because of this, I cannot use the ifRoom value in items if I want proper tinting, it's very restrictive.
Is there a way to tint menu items properly in all cases?
More importantly, is there a built-in way to tint items with themes or styles that would save me from using this complex piece of code? Even if a solution that doesn't cover icons in overflow menu, I would like to know about it.
I am perfectly fine with using reflection if there is no other way.
Unfortunately, there is no way to set the menu item icon color's tint using a theme or style. You need a method to check if the MenuItem is visible on the ActionBar or in the overflow menu. Both the native and support MenuItemImpl class have a method for this but they are either restricted to the library or hidden. This requires reflection. You can use the following method to check if the menu item is visible or not, and then set the color filter:
public static boolean isActionButton(#NonNull MenuItem item) {
if (item instanceof MenuItemImpl) {
return ((MenuItemImpl) item).isActionButton();
} else {
// Not using the support library. This is a native MenuItem. Reflection is needed.
try {
Method m = item.getClass().getDeclaredMethod("isActionButton");
if (!m.isAccessible()) m.setAccessible(true);
return (boolean) m.invoke(item);
} catch (Exception e) {
return false;
}
}
}
You also need to wait until the menu is inflated before tinting the items. To accomplish this you can get a reference to the ActionBar and tint the MenuItem after the ActionBar has been drawn.
Example:
#Override public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_main, menu);
int id = getResources().getIdentifier("action_bar", "id", "android");
ViewGroup actionBar;
if (id != 0) {
actionBar = (ViewGroup) findViewById(id);
} else {
// You must be using a custom Toolbar. Use the toolbar view instead.
// actionBar = yourToolbar
}
actionBar.post(new Runnable() {
#Override public void run() {
// Add code to tint menu items here
}
});
return super.onCreateOptionsMenu(menu);
}
Here is a class I wrote to help with tinting menu item icons: https://gist.github.com/jaredrummler/7816b13fcd5fe1ac61cb0173a1878d4f
Thanks to #JaredRummler, I found a way to determine whether an icon is in the overflow menu or not. I posted the complete code here that gathers the elements of his answer. I also added a helper methods for getting the right colors for tinting icons. Here's what I currently use:
ThemeUtils
public final class ThemeUtils {
/**
* Obtain colors of a context's theme from attributes
* #param context themed context
* #param colorAttrs varargs of color attributes
* #return array of colors in the same order as the array of attributes
*/
public static int[] getColors(Context context, int... colorAttrs) {
TypedArray ta = context.getTheme().obtainStyledAttributes(colorAttrs);
int[] colors = new int[colorAttrs.length];
for (int i = 0; i < colorAttrs.length; i++) {
colors[i] = ta.getColor(i, 0);
}
ta.recycle();
return colors;
}
/**
* Get the two colors needed for tinting toolbar icons
* The colors are obtained from the toolbar's theme and popup theme
* These themes are obtained from {#link R.attr#toolbarTheme} and {#link R.attr#toolbarPopupTheme}
* The two color attributes used are:
* - {#link android.R.attr#textColorPrimary} for the normal color
* - {#link android.R.attr#textColorSecondary} for the color in a menu
* #param context activity context
* #return int[2]{normal color, color in menu}
*/
public static int[] getToolbarColors(Context context) {
// Get the theme and popup theme of a toolbar
TypedArray ta = context.getTheme().obtainStyledAttributes(
new int[]{R.attr.toolbarTheme, R.attr.toolbarPopupTheme});
Context overlayTheme = new ContextThemeWrapper(context, ta.getResourceId(0, 0));
Context popupTheme = new ContextThemeWrapper(context, ta.getResourceId(1, 0));
ta.recycle();
// Get toolbar colors from these themes
int colorNormal = ThemeUtils.getColors(overlayTheme, android.R.attr.textColorPrimary)[0];
int colorInMenu = ThemeUtils.getColors(popupTheme, android.R.attr.textColorSecondary)[0];
return new int[]{colorNormal, colorInMenu};
}
/**
* Change the color of the icons of a menu
* Disabled items are set to 50% alpha
* #param menu targeted menu
* #param colorNormal normal icon color
* #param colorInMenu icon color for popup menu
* #param isInSubMenu whether menu is a sub menu
*/
private static void changeIconsColor(View toolbar, Menu menu, int colorNormal, int colorInMenu, boolean isInSubMenu) {
toolbar.post(() -> {
// Change icons color
for (int i = 0; i < menu.size(); i++) {
MenuItem item = menu.getItem(i);
changeMenuIconColor(item, colorNormal, colorInMenu, isInSubMenu);
if (item.hasSubMenu()) {
changeIconsColor(toolbar, item.getSubMenu(), colorNormal, colorInMenu, true);
}
}
});
}
public static void changeIconsColor(View toolbar, Menu menu, int colorNormal, int colorInMenu) {
changeIconsColor(toolbar, menu, colorNormal, colorInMenu, false);
}
/**
* Change the color of a single menu item icon
* #param item targeted menu item
* #param colorNormal normal icon color
* #param colorInMenu icon color for popup menu
* #param isInSubMenu whether item is in a sub menu
*/
#SuppressLint("RestrictedApi")
public static void changeMenuIconColor(MenuItem item, int colorNormal, int colorInMenu, boolean isInSubMenu) {
if (item.getIcon() != null) {
Drawable icon = item.getIcon().mutate();
int color = (((MenuItemImpl) item).isActionButton() && !isInSubMenu ? colorNormal : colorInMenu);
icon.setColorFilter(color, PorterDuff.Mode.SRC_IN);
icon.setAlpha(item.isEnabled() ? 255 : 128);
item.setIcon(icon);
}
}
}
ActivityUtils
public final class ActivityUtils {
/**
* Force show the icons in the overflow menu and submenus
* #param menu target menu
*/
public static void forceShowMenuIcons(Menu menu) {
if (menu instanceof MenuBuilder) {
MenuBuilder m = (MenuBuilder) menu;
m.setOptionalIconsVisible(true);
}
}
/**
* Get the action bar or toolbar view in activity
* #param activity activity to get from
* #return the toolbar view
*/
public static ViewGroup findActionBar(Activity activity) {
int id = activity.getResources().getIdentifier("action_bar", "id", "android");
ViewGroup actionBar = null;
if (id != 0) {
actionBar = activity.findViewById(id);
}
if (actionBar == null) {
return findToolbar((ViewGroup) activity.findViewById(android.R.id.content).getRootView());
}
return actionBar;
}
private static ViewGroup findToolbar(ViewGroup viewGroup) {
ViewGroup toolbar = null;
for (int i = 0; i < viewGroup.getChildCount(); i++) {
View view = viewGroup.getChildAt(i);
if (view.getClass() == android.support.v7.widget.Toolbar.class ||
view.getClass() == android.widget.Toolbar.class) {
toolbar = (ViewGroup) view;
} else if (view instanceof ViewGroup) {
toolbar = findToolbar((ViewGroup) view);
}
if (toolbar != null) {
break;
}
}
return toolbar;
}
}
I also defined two attributes in attrs.xml: toolbarTheme and toolbarPopupTheme that I set on my toolbar layout in XML. Their values are defined in my app theme in themes.xml. These attributes are used by ThemeUtils.getToolbarColors(Context) to obtain the colors to use for tinting icons, because toolbars often use theme overlays. By doing this, I can change every toolbar's theme only by changing the value of these 2 attributes.
All that is left is calling the following in the activity's onCreateOptionsMenu(Menu menu):
ActivityUtils.forceShowMenuIcons(menu); // Optional, show icons in overflow and submenus
View toolbar = ActivityUtils.findActionBar(this); // Get the action bar view
int[] toolbarColors = ThemeUtils.getToolbarColors(this); // Get the icons colors
ThemeUtils.changeIconsColor(toolbar, menu, toolbarColors[0], toolbarColors[1]);
The same can be done in a fragment by replacing this with getActivity().
When updating a MenuItem icon, another method can be called, ThemeUtils.changeMenuIconColor(). In this case, toolbar colors can be obtained in onCreate and stored globally to reuse them.
Here's a solution that works with the material components MaterialToolbar:
Explanation
The code checks all sub views of the toolbar => those are the visible items
it iterates all menu items recursively and checks if the menu id is part of the visible view ids, if so, this means the menu item is on the toolbar, otherwise it is inside the overflow menu
it then tints the icons based on its position
it also tints the overflow icon
to tint the sub menu arrow indicator correctly, check out following issue: https://github.com/material-components/material-components-android/issues/553
Code
fun View.getAllChildrenRecursively(): List<View> {
val result = ArrayList<View>()
if (this !is ViewGroup) {
result.add(this)
} else {
for (index in 0 until this.childCount) {
val child = this.getChildAt(index)
result.addAll(child.getAllChildrenRecursively())
}
}
return result
}
#SuppressLint("RestrictedApi")
fun MaterialToolbar.tintAndShowIcons(colorOnToolbar: Int, colorInOverflow: Int) {
(menu as? MenuBuilder)?.setOptionalIconsVisible(true)
val c1 = ColorStateList.valueOf(colorOnToolbar)
val c2 = PorterDuffColorFilter(colorInOverflow, PorterDuff.Mode.SRC_IN)
val idsShowing = ArrayList<Int>()
getAllChildrenRecursively().forEach {
// Icon in Toolbar
(it as? ActionMenuItemView)?.let {
idsShowing.add(it.id)
}
// Overflow Icon
(it as? ImageView)?.imageTintList = c1
}
menu.forEach {
checkOverflowMenuItem(it, c2, idsShowing)
}
}
private fun checkOverflowMenuItem(menuItem: MenuItem, iconColor: ColorFilter, idsShowing: ArrayList<Int>) {
// Only change Icons inside the overflow
if (!idsShowing.contains(menuItem.itemId)) {
menuItem.icon?.colorFilter = iconColor
}
menuItem.subMenu?.forEach {
checkOverflowMenuItem(it, iconColor, idsShowing)
}
}
Is it possible to programmatically detect when a menu is overflowed?
My intention is to have a menu item always be visible (SHOW_AS_ACTION_ALWAYS), except for in the case where it would cause other items to overflow, in which case, don't show the menu item at all. That is:
if (overflowed) actionBarMenu.removeItem(id);
You are not saying where this menu is appearing, so I'll just give an example of what you can do with a Toolbar. What you need to do is to get the reference to the ActionMenuView from the Toolbar and then call isOverflowMenuShowing on it, something like this:
private boolean isOverflowShowing(Toolbar toolbar) {
if(toolbar == null) {
return false;
}
for(int i = 0; i < toolbarView.getChildCount(); i++) {
View v = toolbarView.getChildAt(i);
if(v instanceof ActionMenuView) {
return ((ActionMenuView)v).isOverflowMenuShowing();
}
}
return false;
}
This is crude and dirty - and I haven't tested it - but it should get you started.
I have been looking a way to change the color of all elements in a toolbar working like an ActionBar dynamically.
Specifications:
Using parent="Theme.AppCompat.Light.NoActionBar" on styles.xml
Appcompat v7 22
setting setSupportActionBar() in my AppCompatActivity
I got the colors from a POST request (usually #FF------ format)
I have read following post:
How do I change the color of the ActionBar hamburger icon?
How to change color of hamburger icon in material design navigation drawer
Can't change navigation drawer icon color in android
ActionBarDrawerToggle v7 arrow color
Android Toolbar color change
Android burger/arrow icon dynamic change color (this one worked in someway but I don't like using own image wihtout animation).
And others links related to this topic... none of them worked for me.
What I'm doing right now is searching for ImageButton on the toolbar (Get reference to drawer toggle in support actionbar), and applying setColorFilter() to all of them like the following code:
for (int i = 0; i < toolbar.getChildCount(); i++){
if (toolbar.getChildAt(i) instanceof ImageButton) {
ImageButton ib = (ImageButton) toolbar.getChildAt(i);
ib.setColorFilter(Color.parseColor("#A74231"), PorterDuff.Mode.SRC_ATOP);
}
}
I'm changing background and text color with: toolbar.setBackgroundColor and toolbar.setTitleTextColor.
For menu icons (including overflow menu icon):
MenuItem item2 = mMenu.findItem(R.id.actionbar_group_moreoverflow);
item2.getIcon().setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
the QUESTION: is there a better way to do it (change toolbar's elements color dynamically)?
I was facing same problem here. What I did for ToolBar's elements:
For background color: toolbar.setBackgroundColor(Color.parseColor("#xxxxxxxx"));
For text color: toolbar.setTitleTextColor(Color.parseColor("#xxxxxxxx"));
For hamburger/drawer button:
int color = Color.parseColor("#xxxxxxxx");
final PorterDuffColorFilter colorFilter = new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP);
for (int i = 0; i < toolbar.getChildCount(); i++){
final View v = toolbar.getChildAt(i);
if(v instanceof ImageButton) {
((ImageButton)v).setColorFilter(colorFilter);
}
}
For ActionMenuItemView (toolbar's buttons including overflow button):
private void colorizeToolBarItem(AppCompatActivity activity, final PorterDuffColorFilter colorFilter, final String 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, description,
View.FIND_VIEWS_WITH_CONTENT_DESCRIPTION);
if (outViews.isEmpty())
return;
ActionMenuItemView overflow = (ActionMenuItemView)outViews.get(0);
overflow.getCompoundDrawables()[0].setColorFilter(colorFilter);
removeOnGlobalLayoutListener(decorView,this);
}
});
}
For overflow menu's items text: take a look at this link
To get all Toolbar views, iterate through all it’s child views and colorize them separately. The loop code for it looks like this:
public static void colorizeToolbar(Toolbar toolbarView, int toolbarIconsColor, Activity activity) {
final PorterDuffColorFilter colorFilter
= new PorterDuffColorFilter(toolbarIconsColor, PorterDuff.Mode.MULTIPLY);
for(int i = 0; i < toolbarView.getChildCount(); i++) {
final View v = toolbarView.getChildAt(i);
//Step 1 : Changing the color of back button (or open drawer button).
if(v instanceof ImageButton) {
//Action Bar back button
((ImageButton)v).getDrawable().setColorFilter(colorFilter);
}
if(v instanceof ActionMenuView) {
for(int j = 0; j < ((ActionMenuView)v).getChildCount(); j++) {
//Step 2: Changing the color of any ActionMenuViews - icons that
//are not back button, nor text, nor overflow menu icon.
final View innerView = ((ActionMenuView)v).getChildAt(j);
if(innerView instanceof ActionMenuItemView) {
int drawablesCount = ((ActionMenuItemView)innerView).getCompoundDrawables().length;
for(int k = 0; k < drawablesCount; k++) {
if(((ActionMenuItemView)innerView).getCompoundDrawables()[k] != null) {
final int finalK = k;
//Important to set the color filter in seperate thread,
//by adding it to the message queue
//Won't work otherwise.
innerView.post(new Runnable() {
#Override
public void run() {
((ActionMenuItemView) innerView).getCompoundDrawables()[finalK].setColorFilter(colorFilter);
}
});
}
}
}
}
}
//Step 3: Changing the color of title and subtitle.
toolbarView.setTitleTextColor(toolbarIconsColor);
toolbarView.setSubtitleTextColor(toolbarIconsColor);
//Step 4: Changing the color of the Overflow Menu icon.
setOverflowButtonColor(activity, colorFilter);
}
}
Second,implement the method responsible for finding and colorizing the Overflow Icon:
private static void setOverflowButtonColor(final Activity 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<View>();
decorView.findViewsWithText(outViews, overflowDescription,
View.FIND_VIEWS_WITH_CONTENT_DESCRIPTION);
if (outViews.isEmpty()) {
return;
}
TintImageView overflow=(TintImageView) outViews.get(0);
overflow.setColorFilter(colorFilter);
removeOnGlobalLayoutListener(decorView,this);
}
});
}
Toolbar background color
mToolbarView.setBackgroundColor(color);
ToolbarColorizeHelper.colorizeToolbar(mToolbarView, mToolbarIconsColor, getActivity());
Have a look at this link, it may help you https://snow.dog/blog/how-to-dynamicaly-change-android-toolbar-icons-color/
Is there any way to get the Action bar overflow icon drawable at runtime to apply a color filter? I'm looking for something similar to menu.findItem(R.id.menu_id).getIcon() except for the 3 dot overflow so that I can do a .setColorFilter.
Thanks!
Yes, there is a way. You just have to find overflow icon in view hierarchy. One good way is searching in ViewGroup returned by getDecorView(). You can search for it using findViewsWithText with flag FIND_VIEWS_WITH_CONTENT_DESCRIPTION.
Sample code:
public static void setOverflowButtonColor(final Activity activity, final int visibleFontColor) {
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);
removeOnGlobalLayoutListener(decorView, this);
}
});
}
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);
}
}
For more details see my answer in similar question: https://stackoverflow.com/a/27672844/2707179
You cant get the OverflowIcon within the onCreateOptionsMenu, but you can still change the appearance of it at runtime so instead of applying a filter on it programatically you can create your own OverflowIcon image instead.
sample:
You can change the overflow icon within the style of your activity
<style name="YOUR_THEME" parent="android:style/Theme.Holo.Light">
<item name="android:actionOverflowButtonStyle">#style/CustomOverflow</item>
</style>
<style name="CustomOverflow" parent="android:style/Widget.Holo.ActionButton.Overflow">
<item name="android:src">#drawable/your_custom_overflow_image</item>
</style>
i have action bar in my application that displays menu items defined in my res/menu/activity_main.xml
My menu items are aligned to right on action bar. I want them to be aligned to left.
Only solutions i found for this used custom action bar, like this one:
Positioning menu items to the left of the ActionBar in Honeycomb
However, i dont want to create custom layout for my menu. I want to use default menu items generated from my res/menu/activity_main.xml.
Is this possible?
Well, i was curious about this, so i dug deep inside the SDK's source. I used AppCompatActivity with 3 menu item in it's XML file, and i used the default onCreateOptionMenu method, which was this:
#Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
After i move on from the inflate method with the debugger, i went through the following stack:
updateMenuView():96, BaseMenuPresenter (android.support.v7.internal.view.menu)
updateMenuView():231, ActionMenuPresenter (android.support.v7.widget)
dispatchPresenterUpdate():284, MenuBuilder (android.support.v7.internal.view.menu)
onItemsChanged():1030, MenuBuilder (android.support.v7.internal.view.menu)
startDispatchingItemsChanged():1053, MenuBuilder (android.support.v7.internal.view.menu)
preparePanel():1303, AppCompatDelegateImplV7 (android.support.v7.app)
doInvalidatePanelMenu():1541, AppCompatDelegateImplV7 (android.support.v7.app)
access$100():92, AppCompatDelegateImplV7 (android.support.v7.app)
run():130, AppCompatDelegateImplV7$1 (android.support.v7.app)
handleCallback():739, Handler (android.os)
dispatchMessage():95, Handler (android.os)
loop():148, Looper (android.os)
main():5417, ActivityThread (android.app)
invoke():-1, Method (java.lang.reflect)
run():726, ZygoteInit$MethodAndArgsCaller (com.android.internal.os)
main():616, ZygoteInit (com.android.internal.os)
It ended in BaseMenuPresenter's updateMenuView method, this is where the revelant work is done.
the method's code:
public void updateMenuView(boolean cleared) {
final ViewGroup parent = (ViewGroup) mMenuView;
if (parent == null) return;
int childIndex = 0;
if (mMenu != null) {
mMenu.flagActionItems();
ArrayList<MenuItemImpl> visibleItems = mMenu.getVisibleItems();
final int itemCount = visibleItems.size();
for (int i = 0; i < itemCount; i++) {
MenuItemImpl item = visibleItems.get(i);
if (shouldIncludeItem(childIndex, item)) {
final View convertView = parent.getChildAt(childIndex);
final MenuItemImpl oldItem = convertView instanceof MenuView.ItemView ?
((MenuView.ItemView) convertView).getItemData() : null;
final View itemView = getItemView(item, convertView, parent);
if (item != oldItem) {
// Don't let old states linger with new data.
itemView.setPressed(false);
ViewCompat.jumpDrawablesToCurrentState(itemView);
}
if (itemView != convertView) {
addItemView(itemView, childIndex);
}
childIndex++;
}
}
}
// Remove leftover views.
while (childIndex < parent.getChildCount()) {
if (!filterLeftoverView(parent, childIndex)) {
childIndex++;
}
}
}
Here the getItemView and the addItemView methods do what their's name say. The first inflate a new view, and the second add it to parent. What is more important, under the debugger the parent object can be checked, it's an ActionMenuView, which inherits from the LinearLayout and inflated form abc_action_menu_layout.xml.
This means if you can get this view, you can do what you want. Theoretically, i think it can be done with lots of reflection, but it would painful.
Instead of that, you can reproduce it in your code. Implementations can be found here.
According to the things above, the answer for your question is YES, it can be done, but it will be tricky.
Edit:
I created a proof of concept for doing this with reflection. I've used com.android.support:appcompat-v7:23.1.0.
I've tried this on an emulator(Android 6.0) and on my Zuk Z1(CM Android 5.1.1), on both it works fine.
Menu XML:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="#+id/action_settings" android:title="#string/action_settings"
android:orderInCategory="100" app:showAsAction="always" />
<item android:id="#+id/action_settings2" android:title="TEST1"
android:orderInCategory="100" app:showAsAction="always" />
<item android:id="#+id/action_settings3" android:title="TEST2"
android:orderInCategory="100" app:showAsAction="always" />
</menu>
Activty XML:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="New Button"
android:id="#+id/button"
android:layout_gravity="center_vertical" />
</LinearLayout>
Activity:
public class Main2Activity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//only a linear layout with one button
setContentView(R.layout.activity_main2);
Button b = (Button) findViewById(R.id.button);
// do the whole process for a click, everything is inited so we dont run into NPE
b.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
AppCompatDelegate delegate = getDelegate();
Class delegateImpClass = null;
Field menu = null;
Method[] methods = null;
try {
//get objects based on the stack trace
delegateImpClass = Class.forName("android.support.v7.app.AppCompatDelegateImplV7");
//get delegate->mPreparedPanel
Field mPreparedPanelField = delegateImpClass.getDeclaredField("mPreparedPanel");
mPreparedPanelField.setAccessible(true);
Object mPreparedPanelObject = mPreparedPanelField.get(delegate);
//get delegate->mPreparedPanel->menu
Class PanelFeatureStateClass = Class.forName("android.support.v7.app.AppCompatDelegateImplV7$PanelFeatureState");
Field menuField = PanelFeatureStateClass.getDeclaredField("menu");
menuField.setAccessible(true);
Object menuObjectRaw = menuField.get(mPreparedPanelObject);
MenuBuilder menuObject = (MenuBuilder) menuObjectRaw;
//get delegate->mPreparedPanel->menu->mPresenter(0)
Field mPresentersField = menuObject.getClass().getDeclaredField("mPresenters");
mPresentersField.setAccessible(true);
CopyOnWriteArrayList<WeakReference<MenuPresenter>> mPresenters = (CopyOnWriteArrayList<WeakReference<MenuPresenter>>) mPresentersField.get(menuObject);
ActionMenuPresenter presenter0 = (ActionMenuPresenter) mPresenters.get(0).get();
//get the view from the presenter
Field mMenuViewField = presenter0.getClass().getSuperclass().getDeclaredField("mMenuView");
mMenuViewField.setAccessible(true);
MenuView menuView = (MenuView) mMenuViewField.get(presenter0);
ViewGroup menuViewParentObject = (ViewGroup) ((View) menuView);
//check the menu items count
int a = menuViewParentObject.getChildCount();
Log.i("ChildNum", a + "");
//set params as you want
Toolbar.LayoutParams params = (Toolbar.LayoutParams) menuViewParentObject.getLayoutParams();
params.gravity = Gravity.LEFT;
menuViewParentObject.setLayoutParams(params);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
});
}
#Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
}
Although the gravity has been changed here, on the screen this does not make any revelant difference. To get a real visible change other layout params(e.g. width ) should be tuned.
All in all, a custom layout is much easier to use.
You can do it like this:
activity.getActionBar().setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM,
ActionBar.DISPLAY_SHOW_CUSTOM);
activity.getActionBar().setCustomView(mAutoSyncSwitch, new ActionBar.LayoutParams(
ActionBar.LayoutParams.WRAP_CONTENT,
ActionBar.LayoutParams.WRAP_CONTENT,
Gravity.CENTER_VERTICAL | Gravity.LEFT));