I need to get a reference to a MenuItem's corresponding View without specifying a custom one. Using findViewById() with a menu item's ID returns null when called from Activity#onCreate() or onCreateOptionsMenu(). However, as a few answers to other questions point out, findViewById() does return the view when called from onOptionsItemSelected() or from a runnable posted in Activity#onCreate(), like so...
new Handler().post(new Runnable() {
#Override
public void run() {
findViewById(R.id.some_view);
}
});
So it seems that at some point, Android takes the Menu that you populate in onCreateOptionsMenu() and creates the corresponding Views that are shown on screen. A Runnable posted from onCreate() happens to get run after that happens. But it feels so hacky to assume that will always be the case instead of actually listening for some kind of menuViewsCreated() event.
So that brings me to my question – does Android provide an event that tells developers that the menu Views have been created?
It's late but it may help some one else
you must use new Handler() in the onCreateOptionsMenu(Menu menu)
#Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.section, menu);
new Handler().post(new Runnable() {
#Override
public void run() {
final View menuItemView = findViewById(R.id.menu_item);
//TODO
}
});
return true;
}
Related
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?
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.
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
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;
}
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;
}
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;
}
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}
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>
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.
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()
I'm working with ActionBarSherlock, I need to change some icons on the action bar when the user does some stuff. In order to that I have to save the action bar for later usage :
private Menu actionBarMenu;
...
#Override
public boolean onCreateOptionsMenu(Menu menu)
{
MenuInflater inflater = getSupportMenuInflater();
inflater.inflate(R.menu.map_activity_menu, menu);
actionBarMenu = menu;
return super.onCreateOptionsMenu(menu);
}
#Override
public boolean onOptionsItemSelected(MenuItem item)
{
actionBarMenu.findItem(R.id.follow_my_position).setIcon(R.drawable.toolbar_myposition_active);
}
Okay ! Here's where the problems begin. When I rotate the screen, I get a NullPointerException on actionBarMenu.
I know... I didn't save it before the screen was rotated, so normally I would go to onSaveInstanceState and save my Menu there, except the actionBarMenu is an interface... More specifically, the interface com.actionbarsherlock.view.Menu and I can't modify it so that it implements Parcelable or Serializable.
So how can I save an interface in the bundle ?
You should not keep reference to your action bar during configuration changes. After screen rotate, Android will recreate your activity and all UI references should be released, otherwise you will introduce reference leak.
Why dont you get your actionBarMenu reference again inside boolean onCreateOptionsMenu, after activity rotates?
You can't. However, you can still retain your member variable on an orientation change by adding the following code to your activity
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Object last = getLastNonConfigurationInstance();
if (last != null && last instanceof Menu) {
actionBarMenu = (Menu) last;
}
}
// for FragmentActivity it is onRetainCustomNonConfigurationInstance
public Object onRetainNonConfigurationInstance() {
return actionBarMenu;
};
You don't. You don't save the interface.
You save some String, or boolean, or integer, representing the action the user did.
Then you check this value and change the menu again.
Does this work before the rotate? Because a rotation just recreates the entire activity - i.e. it goes through the whole creation process again. So, I'm not sure that the recreation is your problem, because onCreateOptionsMenu(Menu menu); should be called again, anyway.
Are you sure that your actionBarMenu.findItem(R.id.follow_my_position) is returning correctly? Instead of handling it how your currently are - why not just...not store the menu and check the clicked menu item instead?
For example:
#Override
public boolean onOptionsItemSelected(MenuItem item)
{
switch(item.getItemId()){
case R.id.follow_my_position:
item.setIcon(R.drawable.toolbar_myposition_active);
}
}
or if that's not what you're looking for, check that your findItem() call finds the item:
MenuItem item = actionBarMenu.findItem(R.id.follow_my_position);
if(item != null){
item.setIcon(R.drawable.toolbar_myposition_active);
}
My application is basically an online store that has a cart. The button to start the cart is in the ActionBar. When someone presses on a product it starts an animation where the product quicly "slides" through the screen towards the ActionBar cart button. As soon as that finishes the cart "blinks". To blink the cart I use
ValueAnimator cartAnim = ObjectAnimator.ofFloat(mCartItem, "alpha", 1,
0.25f, 0);
Where mCartItem is the ActionBar Item View to be animated.
Now as it turns out getting the View of the actual ActionBar item is kinda hard. I can get the View in onOptionsItemSelected but that's basically it, however this won't work for me since the animation isn't triggered from the ActionBar, it's triggered from a ListView in the main UI. After some googling I did however find a hack around this, that works:
#Override
public boolean onCreateOptionsMenu(Menu menu) {
getSupportMenuInflater().inflate(R.menu.cartmenu, menu);
new Handler().post(new Runnable() {
#Override
public void run() {
mCartItem = findViewById(R.id.theitem);
}
});
return(super.onCreateOptionsMenu(menu));
}
Why is this way working? As opposed to:
#Override
public boolean onCreateOptionsMenu(Menu menu) {
getSupportMenuInflater().inflate(R.menu.cartmenu, menu);
mCartItem = findViewById(R.id.theitem); // Always ends up null.
return(super.onCreateOptionsMenu(menu));
}
How would you solve the problem I had?
findViewById()
searches for a child view with the given id in "this" view. When you call it in onCreateOptionsMenu() the menu is still not attached to the main view, so you it cannot find your item.
Using Handler().post(new Runnable()...) the findViewById(R.id.theitem) is executed after all your view is created and, of course, even the menu has been attached to your main view, so it can be found.
Better solution:
since you are inflating your cart menu in the menu object, you can use:
#Override
public boolean onCreateOptionsMenu(Menu menu) {
getSupportMenuInflater().inflate(R.menu.cartmenu, menu);
mCartItem = menu.findItem(R.id.theitem); //This will find your menu item! :-)
return(super.onCreateOptionsMenu(menu));
}
I have an ActionBarSherlock with one menu item in my bar. When I'm call
View item = findViewById(R.id.my_item);in activity's button onClick all works fine as expected.
But when I try to do this in onCreate or onResume or even in onPostResume it is always null. I also tryed do this in onCreateOptionsMenu(Menu menu) after inflating my menu from resource, but without any succes.
Therefore I can't understand when actionbar items created and how to catch this moment?
As it has been said here and here, getActionView returns the view that we sets in setActionView. Therefore, the only one way to customize action bar menu item described here
actually it is possible to get the view of the action item, even if it's not customized.
however, do note that sometimes action items get to be inside the overflow menu so you might get a null instead.
so, how can you do it?
here's a sample code:
public boolean onCreateOptionsMenu(final Menu menu) {
getSupportMenuInflater().inflate(R.menu.main, menu);
new Handler().post(new Runnable() {
#Override
public void run() {
final View syncItemView = findViewById(R.id.action_search);
...
this was tested when using actionBarSherlock library, on android 4.1.2 and android 2.3.5 .
another alternative is to use a more extensive way , used on the showcaseView library, here .
first of all get a menu reference as below:
#Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.custom_menu, menu);
customMenu = menu;
return super.onCreateOptionsMenu(menu);
}
after that you can get the item you need as below
customMenu.getItem(0);
Perhaps I'm missing the obvious, but my menu is only been created and populated when onCreateOptionsMenu(Menu menu) is called, as in this example, and this only seem to be called after I press the menu button.
I would like to populate the menu upon the creation of the activity, how can achieve that?
UPDATE:
Perhaps a better question would be:
How can I get the Menu instance of my Activity?
Thanks
Create a class which holds those states then set your enabled/checked etc from the properties of that class in onCreateOptionsMenu()
class MenuStates{
public static boolean userDidPressTheButton;
public static boolean serverDidRespond;
public static boolean colorWasChanged;
}
void someEventHandler(){
MenuStates.userDidPressTheButton = true;
}
void onCreateOptionsMenu(){
myCheckBox.setChecked(MenuStates.userDidPressTheButton);
}
[EDIT]
You don't say why you want to get the menu instance. One approach:
Menu optsMenu;
...
// this is called once only before the end of the Activity onCreate().
onCreateOptionsMenu(Menu menu){
opstMenu = menu;
super.onCreateOptionsMenu(menu);
}
Following that, modify your menu as you wish. Do any "as it pops" up work in onPrepareOptionsMenu().
The key understanding is the difference between onCreateOptionsMenu() and onPrepareOptionsMenu().
[MORE EDIT]
To completely control the thing yourself:
Menu optsMenu;
onCreate(){
openOptionsMenu() // the menu won't show in onCreate but onCreateOptionsMenu is shown
closeOptionsMenu()
}
onCreateOptionsMenu(Menu menu){
optsMenu = menu;
}
onPrepareOptionsMenu(Menu menu){
menu.clear();
for (int i=0;ioptsMenu.size();i++){
menu.add(optsMenu.get(i).getTitle());
}
return super.onPrepareOptionsMenu(menu);
}
From the docs
You can safely hold on to menu (and any items created from it), making modifications to it as desired, until the next time onCreateOptionsMenu() is called.
http://developer.android.com/reference/android/app/Activity.html#onCreateOptionsMenu%28android.view.Menu%29