6

I'm inflating the menu and trying to find the view of one of the menu items in the following way:

 @Override
 public boolean onCreateOptionsMenu(final Menu menu) {
     getMenuInflater().inflate(R.menu.main, menu);

     // will print `null`
     Log.i("TAG", String.valueOf(findViewById(R.id.action_hello)));
     return true;
 }

In the result null is printed in Logcat. However if I add some delay before calling findViewById, it returns correct View object:

@Override
public boolean onCreateOptionsMenu(final Menu menu) {
    getMenuInflater().inflate(R.menu.main, menu);
    new AsyncTask<Void, Void, Void>() {
        @Override
        protected Void doInBackground(final Void... voids) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return null;
        }

        @Override
        protected void onPostExecute(final Void aVoid) {
            // will print correctly android.support.v7.view.menu.ActionMenuItemView...
            Log.i("TAG", String.valueOf(findViewById(R.id.action_hello)));
        }
    }.execute();
    return true;
}

Of course this solution is very dirty and the minimal delay is unknown. Is there a way to register some callback for the menu inflated event. In other words: how can I call findViewById with the menu item id to be sure that the view is already there and this call won't return null?

  • 1
    Do you need the reference to the view? `MenuItem` from `Menu` wouldn't be enough? The view of `Menu` is created after `onCreateOptionsMenu`, that's why it's not inflated yet in this method. – Nominalista Jun 12 '18 at 15:21
  • I think I need it because I want to attach a tooltip to the menu (with this library: https://github.com/sephiroth74/android-target-tooltip). I've tried `menu.findItem(R.id.action_hello).getActionView()` however it is the same issue: `getActionView()` returns `null` when called in the `onCreateOptionsMenu` – Piotr Aleksander Chmielowski Jun 12 '18 at 15:26
  • Moreover - `getActionView()` returns `null` even with the `AsyncTask` approach – Piotr Aleksander Chmielowski Jun 12 '18 at 15:29
  • Are you working with a menu that contains an actionViewClass (i.e. a searchbar) ? `` and 'findViewById' belongs the the actionview? if yes the actionview has a livecycle of it-s own and only exists when actionview is expanded. – k3b Jun 12 '18 at 16:13
  • @k3b No, my `item` has just 3 attributes: `android:id`, `android:title` and `app:showAsAction="always"` – Piotr Aleksander Chmielowski Jun 12 '18 at 17:37
  • 1
    since you do this because you want a tooltip; have you tried https://stackoverflow.com/questions/36267859/android-tooltips-on-menuitem ? – k3b Jun 13 '18 at 08:53
  • I've seen this answer, but it requires creating a toolbar with custom views, while I'm trying to find a solution which will easily integrate with my existing codebase without big changes in the toolbar itself. – Piotr Aleksander Chmielowski Jun 13 '18 at 09:44

9 Answers9

2

Just override public void onPrepareOptionsMenu(Menu menu).

Documentation says:

This is called right before the menu is shown, every time it is shown. You can use this method to efficiently enable/disable items or otherwise dynamically modify the contents.

The view for Menu is created after calling onCreateOptionsMenu(Menu), that's why you can't access it subviews.

Nominalista
  • 4,632
  • 11
  • 43
  • 102
1

There are two ways to find the menu item view.

Frist way:-

Add a actionViewClass in your menu item, so that you can get view returned by getActionView. As getActionView() only works if there's a actionView defined for menuItem.

Add this in your menu item xml:-

<item
    android:id="@+id/menuAdd"
    android:icon="@android:drawable/ic_menu_add"
    android:title="Add"
    app:showAsAction="always"
    app:actionViewClass="android.widget.ImageButton" />

In onCreateOptionsMenu method:-

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.menu_item,menu);
    View menuView=menu.findItem(R.id.menuAdd).getActionView();
    Log.e("TAG", "onCreate: "+menuView );
    return true;
}

Second way:-

The second way is to use a handler. Using this method you won't need to specify the time for the delay. Check the answer given by @davehenry here

Nainal
  • 1,728
  • 14
  • 27
1

This way you get the menu item's id and its actionview:

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.menu, menu);

    MenuItem mi = menu.findItem(R.id.action_hello);

    int id = mi.getItemId();

    Log.i("TAG", String.valueOf(id));

    View actionView = mi.getActionView();

    if (actionView == null) {
        Log.i("TAG", "ActionView is null");
    } else {
        Log.i("TAG", "ActionView is NOT null");
    }

    return true;
}
1

You must call the super method when your code is finished or things just don't work as expected.

@Override
     public boolean onCreateOptionsMenu(final Menu menu) {
         getMenuInflater().inflate(R.menu.main, menu);
    super.onCreationOptionsMenu(menu);
         // calling the super completes the method now you code.
         Log.i("TAG", String.valueOf(findViewById(R.id.action_hello)));
         return true;
     }
danny117
  • 5,581
  • 1
  • 26
  • 35
0

Posting Runnable to the Handler queue would usually have that Runnable executed after the main UI thread finished with the currently being executed method part of the Activity's lifecycle, hence it would be a good chance to get what you want there. I do need to note that it's a trick which could fail if underestimated and not well tested but it has worked for me ever since I figured it out.

@Override
public boolean onCreateOptionsMenu(final Menu menu) {
    getMenuInflater().inflate(R.menu.main, menu);
    new Handler(getMainLooper()).post(new Runnable() {

        @Override
        public void run() {
            Logger.LogI(TAG, "run: " + findViewById(R.id.action_hello));
        }
    });
    return true;
}
ahasbini
  • 6,761
  • 2
  • 29
  • 45
  • There are at least 3 mistakes with this answer. This is guesswork without understanding how android menus or android lifecycle works. – Nick Cardoso Aug 01 '18 at 09:09
0

Create a global variable for future use:

private ImageButton actionHelloView;

Then, in your onCreateOptionsMenu:

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        super.onCreateOptionsMenu(menu);
        getMenuInflater().inflate(R.menu.menu_main, menu);
        actionHelloView = (ImageButton) menu.findItem(R.id.action_hello).getActionView();


        Log.i("the view is: ", String.valueOf(actionHelloView));
        return true;
    }

Put this in your XML:

<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="com.test.teststack.MainActivity">
    <item
        android:id="@+id/action_hello"
        android:title="@string/action_settings"
        app:showAsAction="never"
        app:actionViewClass="android.widget.ImageButton"/>
</menu>

Note:
depending on your API version, you can switch between app:actionViewClass and android:actionViewClass in your xml.

Result in LOGCAT:

07-25 17:55:07.138 9491-9491/? I/the view is:: android.widget.ImageButton{5542a5c VFED..C.. ......I. 0,0-0,0 #7f080011 app:id/action_hello}

Red M
  • 2,609
  • 3
  • 30
  • 50
0

You don't mention why you want to find the menu item view and that may have some bearing on the answer that you are looking for. However, if you want to use findViewById() to find a menu view then this is one way to do it. The following example just changes a menu icon from an "X" to a check mark.

ViewTreeObserver.OnGlobalLayoutListener will be invoked right after layout of the toolbar in the following code. It is along the same lines as your delay, but it is the acceptable way to do this type of processing.

Alternately, the program can invoke menu.findItem(R.id.action_hello) in onPrepareOptionsMenu(). Unfortunately, the toolbar is not fully formed at this point, so a findViewById() will fail.

MainActivity.xml

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final Toolbar toolbar = findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        setTitle("");
        toolbar.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                ActionMenuItemView view = toolbar.findViewById(R.id.action_hello);
                if (view != null) {
                    // onGlobalLayout may be called before toolbar is fully defined.
                    Log.d("onGlobalLayout", "<<<<view is not null");
                    // Uncomment this view to make the change to the icon here. Android Studio
                    // will complain about a library group, but that can be ignored for this demo.
                    // view.animate() might be a better demo here.
                    view.setIcon(getResources().getDrawable(R.drawable.ic_check));
                    toolbar.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                }
            }
        });
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

    @Override
    public boolean onPrepareOptionsMenu(Menu menu) {
        // Uncomment the following line to change the icon here.
//        menu.findItem(R.id.action_hello).setIcon(getResources().getDrawable(R.drawable.ic_check));
        return true;
    }
}

main.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item
        android:id="@+id/action_hello"
        android:icon="@drawable/ic_x"
        android:title="Item1"
        app:showAsAction="ifRoom" />
</menu>

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:layoutDirection="ltr"
        android:padding="0px"
        android:theme="@style/ThemeOverlay.AppCompat.ActionBar"
        app:contentInsetEnd="0px"
        app:contentInsetEndWithActions="0px"
        app:contentInsetLeft="0px"
        app:contentInsetRight="0px"
        app:contentInsetStart="0px"
        app:contentInsetStartWithNavigation="0px"
        app:logo="@null"
        app:title="@null"
        app:titleMargin="0px"
        app:titleTextColor="#757575"
        tools:ignore="UnusedAttribute"
        tools:title="toolbar">
    </android.support.v7.widget.Toolbar>

</FrameLayout>
Cheticamp
  • 61,413
  • 10
  • 78
  • 131
0

In onCreateOptionsMenu(), when you call

findViewById(R.id.action_hello)

this searches in the View hierarchy starting with your "content root". Since the menu you inflated hasn't been attached to the content root yet, it is likely this will return null.

You should be able to post a Runnable to a Handler that will find the View you want. This should be called after you return from onCreateOptionsMenu() and Android has attached the menu views to the content root. You shouldn't need any delay. You just need to wait until the framework has completed the creation of the options menu.

David Wasser
  • 93,459
  • 16
  • 209
  • 274
0

Inflating your menu is not asynchronous, so you are able to find the item exactly where you are doing so - although onPrepareOptionsMenu is probably the more correct place to do so.

What you cannot do is use findItemById, which looks in the currently showing layout (not your collapsed menu), instead you must use menu.findItem() (or menu.getItem())

If you really need to work with the view of the Item (vs the MenuItem object) you can use menu.findItem().getActionView()

Nick Cardoso
  • 20,807
  • 14
  • 73
  • 124