Pieter888
Pieter888

Reputation: 4992

How can I get the current location of an ActionBar MenuItem?

I want to show a little info label underneath a MenuItem in the app's ActionBar. To position this correctly I need to know the x-position of a MenuItem.

Google brought me no useful results so far.

Is it even possible?

Upvotes: 21

Views: 14216

Answers (4)

Timmmm
Timmmm

Reputation: 96586

I finally found an answer to this! It is a bit hacky, but not too much. The only downsides are:

  1. You have to replace the MenuItems with your own View. An ImageView is fairly similar to the standard one but it misses features like long-pressing to see a tool-tip, and probably other things.

  2. I haven't tested it a lot.

Ok, here's how we do it. Change your Activity's onCreateOptionsMenu() to something like this:

View mAddListingButton;

@Override
public boolean onCreateOptionsMenu(Menu menu)
{
    getMenuInflater().inflate(R.menu.activity_main, menu);
    
    // Find the menu item we are interested in.
    MenuItem it = menu.findItem(R.id.item_new_listing);
    
    // Create a new view to replace the standard one.
    // It would be nice if we could just *get* the standard one
    // but unfortunately it.getActionView() returns null.
    
    // actionButtonStyle makes the 'pressed' state work.
    ImageView button = new ImageView(this, null, android.R.attr.actionButtonStyle);
    button.setImageResource(R.drawable.ic_action_add_listing);
    // The onClick attributes in the menu resource no longer work (I assume since
    // we passed null as the attribute list when creating this button.
    button.setOnClickListener(this);
    
    // Ok, now listen for when layouting is finished and the button is in position.
    button.addOnLayoutChangeListener(new OnLayoutChangeListener() {
        @Override
        public void onLayoutChange(View v, int left, int top, int right, int bottom,
                int oldLeft, int oldTop, int oldRight, int oldBottom)
        {
            // Apparently this can be called before layout is finished, so ignore it.
            // Remember also that it might never be called with sane values, for example
            // if your action button is pushed into the "more" menu thing.
            if (left == 0 && top == 0 && right == 0 && bottom == 0)
                return;
            
            // This is the only way to get the absolute position on the screen.
            // The parameters passed to this function are relative to the containing View
            // and v.getX/Y() return 0.
            int[] location = new int[2];
            v.getLocationOnScreen(location);
            
            // Ok, now we know the centre location of the button in screen coordinates!
            informButtonLocation(location[0] + (right-left)/2, location[1] + (bottom-top)/2);
        }
    });
    
    it.setActionView(button);
    mAddListingButton = it.getActionView();
    return true;
}

And then you have a function that updates your helpful instructions view with the location of the button and redraws it:

private void informButtonLocation(int cx, int cy)
{
    MyHelpView v = (MyHelpView)findViewById(R.id.instructions);
    v.setText("Click this button.");
            v.setTarget(cx, cy);
}

Finally make your fancy schmancy custom view that draws an arrow to (near) the button. In your onDraw method you can convert the screen coordinates back to Canvas coordinates using the same getLocationOnScreen function. Something like this:

@Override
public void onDraw(Canvas canvas)
{
    int[] location = new int[2];
    getLocationOnScreen(location);
    
    canvas.drawColor(Color.WHITE);

    mPaint.setColor(Color.BLACK);
    mPaint.setTextSize(40);
    mPaint.setAntiAlias(true);
    mPaint.setTextAlign(Align.CENTER);
    canvas.drawText(mText, getWidth()/2, getHeight()/2, mPaint);

    // Crappy line that isn't positioned right at all and has no arrowhead.
    if (mTargetX != -1 && mTargetY != -1)
        canvas.drawLine(getWidth()/2, getHeight()/2,
                mTargetX - location[0], mTargetY - location[1], mPaint);
}

(But obviously you have to clip the target location to the Canvas size). If you go for the same method as Google where you overlay a view over the entire screen you won't have to worry about that, but it is more intrusive. Google's method definitely doesn't use any private APIs (amazingly). Check out Cling.java in the Launcher2 code.

Incidentally, if you look at the source for Google's similar implementation of this for the launcher (they call the instructions screen a Cling for some reason), you can see that it is even more hacky. They basically use absolute screen positions determined by which layout is being used.

Hope this helps people!

Upvotes: 14

Alécio Carvalho
Alécio Carvalho

Reputation: 13647

Just handle the Menu Item as a normal view..like this:

    View myActionView = findViewById(R.id.my_menu_item_id);
    if (myActionView != null) {
        int[] location = new int[2];
        myActionView.getLocationOnScreen(location);

        int x = location[0];
        int y = location[1];
        Log.d(TAG, "menu item location --> " + x + "," + y);
    }

Note: I've tested with my activity configured using ToolBar, not directly as ActionBar...but i guess it should work similarly.

Upvotes: 12

BoD
BoD

Reputation: 11035

I found another way to get a hook on an action bar button, which may be a bit simpler than the accepted solution. You can simply call findViewById, with the id of the menu item!

Now the hard part is when can you call this? In onCreate and onResume, it is too early. Well one way to do this is to use the ViewTreeObserver of the window:

    final ViewTreeObserver viewTreeObserver = getWindow().getDecorView().getViewTreeObserver();
    viewTreeObserver.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            View menuButton = findViewById(R.id.menu_refresh);
            // This could be called when the button is not there yet, so we must test for null
            if (menuButton != null) {
                // Found it! Do what you need with the button
                int[] location = new int[2];
                menuButton.getLocationInWindow(location);
                Log.d(TAG, "x=" + location[0] + " y=" + location[1]);

                // Now you can get rid of this listener
                viewTreeObserver.removeGlobalOnLayoutListener(this);
            }
        }
    });

This goes in onCreate.

Note: this was tested on a project using ActionBarSherlock, so it is possible that it doesn't work with a 'real' action bar.

Upvotes: 34

Pieter888
Pieter888

Reputation: 4992

This answer is now outdated, Look for the accepted answer above

I found out that (at this time) there is no way of figuring out the x/y value of a menu item. See menu items could just as well be hidden from the screen until the user presses the menu button. In Android 3.0 and up you have the option to implement the menu in the app's ActionBar. You can add a lot of menu items in the actionbar, when there is no more place to position an item on the bar it will simply create an "Overflow" button which will contain all menu items that could not be placed on the action bar because of a shortage of space or because the programmer explicitly set the menu item to appear in the overflow menu.

All this behavior is probably why menu items don't have their x/y position you can retrieve (or set).

How I eventually solved this problem is by using the Paint.measureText() function to determine how wide each menu item would be in the action bar should be and use it to calculate the offset from the right side of the screen where the label should be.

It's a workaround but if anyone ever has the same problem as me, know that this works just fine. Just remember to set the appropriate font-size on the Paint object or else when you measure the text's length it will most probably calculate the text width using a different font-size than your menu item's font-size.

Upvotes: 0

Related Questions