0

I am developing a tutorial for an application and I need to point at a particular icon in the toolbar.

Here is an extract of the XML for the action menu:

<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" tools:context=".MainActivity">

<item android:id="@+id/AbErase"
    android:title="@string/Erase"
    android:icon="@android:drawable/ic_delete"
    android:orderInCategory="10"
    app:showAsAction="ifRoom|collapseActionView" />

<item android:id="@+id/AbSuggest"
    android:title="@string/Suggest"
    android:icon="@mipmap/ic_lightbulb_outline_white_48dp"
    android:orderInCategory="50"
    app:showAsAction="ifRoom|collapseActionView" />
<item android:id="@+id/AbUndo"
    android:title="@string/ActionBarUndo"
    android:icon="@android:drawable/ic_menu_revert"
    android:orderInCategory="51"
    app:showAsAction="ifRoom|collapseActionView" />
...

Here is my code:

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

    isBeingRestored = (savedInstanceState != null);

    Toolbar scToolbar = (Toolbar) findViewById(R.id.Sc_toolbar);
    setSupportActionBar(scToolbar);

    scToolbar.post(new Runnable() {
        @Override
        public void run() {
            if (!isBeingRestored) {
                //View mErase = findViewById(R.id.AbErase);
                View mErase = overflowMenu.getItem(0).getActionView();
                int[] location = new int[2];
                mErase.getLocationOnScreen(location);
                eraseIconLeft = location[0];
            }
        }
    }

With View mErase = findViewById(R.id.AbErase); mErase is set to null, **** start EDIT **** which is not suprising as AbErase is the id of a MenuItem, not the id of a View. **** end EDIT **** With View mErase = overflowMenu.getItem(0).getActionView(); location is set to (0, 24), which is wrong as there already is a logo icon and a title in the toolbar.

How can I get the absolute X coordinate of the AbErase view in the toolbar?

**** EDIT **** here is the code where the initialisation of the static variable overflowMenu can be found:

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

    actionBar.collapseActionView();

    overflowMenu = menu;

    isInitializedMenuItem = menu.findItem(R.id.AbInitialized);
    isInitializedMenuItem.setChecked(isInitializeCbxChecked);

    return super.onCreateOptionsMenu(menu);
}
ema3272
  • 1,021
  • 2
  • 13
  • 28

2 Answers2

4

Try the following code snippet

@Nullable 
View getMenuItemView(Toolbar toolbar, @IdRes int menuItemId) throws IllegalAccessException,NoSuchFieldException {
    Field mMenuView = Toolbar.class.getDeclaredField("mMenuView");
    mMenuView.setAccessible(true);
    Object menuView = mMenuView.get(toolbar);
    Field mChildren = menuView.getClass()  //android.support.v7.internal.view.menu.ActionMenuView
                          .getSuperclass() //android.support.v7.widget.LinearLayoutCompat
                          .getSuperclass() //android.view.ViewGroup
                          .getDeclaredField("mChildren");
    mChildren.setAccessible(true);
    View[] children = (View[]) mChildren.get(menuView);
    for (View child : children) {
        if (child.getId() == menuItemId) {
            return child;
        }                          
    }
    return null;                      
}

assuming android.support.v7.widget has been used. The above method would return a View corresponding to you menu item.

View menuItemView = getMenuItemView(scToolbar,R.id.AbErase);
int x = menuItemView.getX();
int y = menuItemView.getY();

and you have your coordinates.

EDIT

after a bit more research I found out reflection isn't required.

Before fetching the menuItemView we must wait for the Toolbar to finish its layout pass

scToolbar.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            scToolbar.getViewTreeObserver().removeOnGlobalLayoutListener(this);
            View menuItem = findViewById(R.id.AbErase);
            if (menuItem != null) {
                int[] location = new int[2];
                menuItem.getLocationOnScreen(location);
                int x = location[0]; //x coordinate
                int y = location[1]; //y coordinate
            }
        }
    });

the above snippet is tested to work, it may be placed in any convenient lifecycle callback method.

EDIT

On looking at the source for the activity the key things that must be remembered is that View menuItem = findViewById(R.id.AbErase); should only be called from a ViewTreeObserver.OnGlobalLayoutListener, onGlobalLayout callback.

Sakchham
  • 1,689
  • 12
  • 22
  • I inserted the 3 instructions above relating to `menuItemView` in my Runnable, which should be executed after the toolbar has been laid out if I am not mistaken. After deleting `.getSuperclass() //android.support.v7.internal.widget.LinearLayoutICS` the `menuItemView` is correctly set. However, its Left, Top, Right, Bottom fields are all equal to zero. Is this code executed too early? When should I execute it, assuming that it must be executed before any user interaction? – ema3272 Jun 29 '16 at 08:59
  • @ema3272 which version of android support library are you using? I'll look into the relevant sources to see if I missed anything. Technically it should be executed when the views are in a visible state, "onCreate" would be a bit too early for that, try "onResume" that should work. – Sakchham Jun 29 '16 at 09:21
  • sorry for the '.getSuperclass() //android.support.v7.internal.widget.LinearLayoutICS' the 'ActionMenuView' is now a child of 'LinearLayoutCompat' hence the error, edited the answer. – Sakchham Jun 29 '16 at 09:29
  • I use com.android.support:appcompat-v7:24.0.0 When calling `getMenuItemView(scToolbar,R.id.AbErase)` from `onResume()`, `mMenuView.get(toolbar)` now returns `null`. – ema3272 Jun 29 '16 at 09:49
  • `R.id.AbErase` is the id of a MenuItem, not the id of a View, see the XML in the question. Hence, when I run the code in the EDIT, `findViewById(R.id.AbErase)` returns null. – ema3272 Jun 29 '16 at 16:02
  • I made a sample application to demonstrate the usage of the above code, hope that resolves the issue [Github](https://github.com/sak-venom1997/MenuItemSample) – Sakchham Jun 29 '16 at 17:53
  • Yes your code runs; when I copy it into my application it gives the same result as the code in my question: `location` is set to (0,24). Which means that the menu items seem to be **left-aligned** in the toolbar. However, when the app shows on the device, the menu is **right-aligned**, with an icon and a title on the left side, plus a spinner. Hence the x-coordinate, i.e. location[0] can not be zero. Where has the action bar returned by ´getSupportActionBar()` disappeared? – ema3272 Jun 30 '16 at 18:20
  • Mind sharing the complete code for your activity ? @ema3272 – Sakchham Jun 30 '16 at 18:35
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/116129/discussion-between-ema3272-and-sakchham-sharma). – ema3272 Jun 30 '16 at 18:57
0

In onCreate method is too early. The view is not measured yet. Override onPrepareOptionMenu() and put your code in it. Also remove your Runnable, its unnecessary.

Borislav Kamenov
  • 561
  • 4
  • 12
  • I use the Runnable because I find the solution described in [2. Add a runnable to the layout queue: View.post()](http://stackoverflow.com/questions/3591784/getwidth-and-getheight-of-view-returns-0) very elegant and performance-oriented. Anyway, when I move the code to `onPrepareOptionMenu` as suggested, `mErase` is `null` whatever the way to initialize it. So this does not work either. – ema3272 Jun 29 '16 at 07:53