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

android, android-layout

Solution

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

You have to replace the `MenuItem`s 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.

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!

Problem

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?

Original source