Android align menu items to left in action bar

26,165

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.

Share:
26,165
hendrix
Author by

hendrix

2007-2012 student at Comenius University in Bratislava - Faculty of Mathematics, Physics and Informatics

Updated on July 21, 2021

Comments

  • hendrix
    hendrix almost 3 years

    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?

    • Sababado
      Sababado over 11 years
      You may want to look into the Split action bar. Android users are familiar with ActionBar icons on the right, or on the split bar which is on the bottom of the screen. You don't want to be redesigning something that Android users are already familiar and comfortable with. developer.android.com/guide/topics/ui/actionbar.html#SplitBa‌​r
    • pepyakin
      pepyakin over 11 years
      Check this post. It should help you. stackoverflow.com/questions/7454102/…
    • hendrix
      hendrix over 10 years
      I dont get the downvotes. I mean question is clear enough, if the answer is NO, that doesnt make question bad...
    • Prabs
      Prabs almost 9 years
      @hendrix hello..How you have achieved this task..I have the same requirement..
  • Deniz C.
    Deniz C. over 9 years
    It is not a custom layout, he just adds a layout parameter to the existing layout, he doesn't change something else. But for that he must write custom layout because otherwise he wouldn't be able to edit the layout.
  • Deniz C.
    Deniz C. over 9 years
    But if you really need to do it without any actionbar layout editting, then you will be unable to do it.