Android Tip: Detecting long click at options menu item

Android Tip: Detecting long click at options menu item

    In Android, the options menu is where you should include actions and other options that are relevant to the current activity context, such as "Search," "Compose email," and "Settings". The option menu always located in Action Bar/Toolbar (from API 11):
    By reading my previous post, you've learned about detecting overflow button (3) clicked (opening/closing hidden menu). With other options menu item which always displayed on the Action Bar/Toolbar, handling it's click event is very easy through overriding onOptionItemSelected(MenuItem item) method but detecting it's long click event is not simple, we must set a custom view for this menu item and handle the view long click event.
    Now, with this tip, I will present a solution to solve this problem.

Default long click event of the option menu item

    Suppose we have a simple menu file with only one item like this:
menu_main.xml
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item
        android:id="@+id/camera"
        android:title="Camera"
        android:icon="@android:drawable/ic_menu_camera"
        app:showAsAction="always" />

</menu>
    With menu item which has icon property, when running, this icon will be displayed instead of item title and if you long click at this item, the item title will be shown by a Toast (like ImageView's contentDescription):

Custom option menu item actionView

    So, if you want to custom the long click event of menu item, the first work is creating a new layout for it:
res\layout\layout_menu.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    style="@android:style/Widget.ActionButton"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:gravity="center"
    android:orientation="vertical">
    
    <Button
        android:id="@+id/button1"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:background="@android:drawable/ic_menu_camera" />
</RelativeLayout>
    And now, you must set this layout as the MenuItem action view. Get the Button by call findViewId() and handle it's long click event (by use setOnLongClickListener() method). These works are perform in onCreateOptionsMenu():
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_main, menu);
        MenuItem item1 = menu.findItem(R.id.camera);

        MenuItemCompat.setActionView(item1, R.layout.layout_menu);
        View menuLayout = MenuItemCompat.getActionView(item1);

        View cameraMenu = menuLayout.findViewById(R.id.button1);
        cameraMenu.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View view) {
                Toast.makeText(MainActivity.this, "Options menu item long clicked!", Toast.LENGTH_SHORT).show();
                return false;
            }
        });

        return super.onCreateOptionsMenu(menu);
    }
    And this is our new output when long click at the camera icon in the options menu:
    There is an important note here: with this custom, you can not handle this item "normal click" event by override onOptionsItemSelected(MenuItem item). So, if you want to perform this work, please call setOnClickListener() for the Button inside onCreateOptionsMenu(Menu menu):
cameraMenu.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Toast.makeText(MainActivity.this, "option menu item clicked!", Toast.LENGTH_SHORT).show();
            }
        });
    And you'll get this output when click at the camera icon:

Conclusions

    With this small tip, I hope you've learned one more trick with options menu in Android to apply to your own work. For more posts about Android menu topic, please visit this tag link. Finally, as usual, please get my full project by click the button below!

Android Tip: Detecting overflow button click event in Options Menu

Android Tip: Detecting overflow button click event in Options Menu

    In Android, the options menu is where you should include actions and other options that are relevant to the current activity context, such as "Search," "Compose email," and "Settings". The option menu always located in Action Bar/Toolbar (from API 11):
    In this post, I would like to talk about overflow button in the option menu (number (3) in the picture). By setting app:showAsAction="never" with an <item>, it will be hidden in the overflow button.
    Handling click event of each menu item is a basic work in Android app development and this is not hard to do, you only need to override onOptionsItemSelected(Menu menu) method and based on their id to perform different tasks. But, are we be able to handling the overflow button click event?
    The answer is "Yes", when click the overflow button, a menu will be shown and onMenuOpened() was called. This method description can be seen at the official document of Google developer:
boolean onMenuOpened (int featureId, Menu menu): Called when a panel's menu is opened by the user. This may also be called when the menu is changing from one type to another (for example, from the icon menu to the expanded menu).
    Absolutely similarly, onPanelClosed() will be called when the option menu closed. According to Google doc, we can notice this:
void onPanelClosed (int featureId, Menu menu): Default implementation of onPanelClosed(int, Menu) for activities. This calls through to onOptionsMenuClosed(Menu) method for the FEATURE_OPTIONS_PANEL panel, so that subclasses of Activity don't need to deal with feature codes. For context menus (FEATURE_CONTEXT_MENU), the onContextMenuClosed(Menu) will be called.
    For example, suppose we have a menu file like this:
res/menu_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/add"
        android:icon="@android:drawable/ic_menu_add"
        android:title="add"
        app:showAsAction="always" />

    <!-- Hidden in overflow button -->
    <item
        android:id="@+id/btn1"
        android:icon="@android:drawable/ic_menu_add"
        android:title="Item 1"
        app:showAsAction="never" />

    <item
        android:id="@+id/btn2"
        android:icon="@android:drawable/ic_menu_add"
        android:title="Item 2"
        app:showAsAction="never" />

    <item
        android:id="@+id/btn3"
        android:icon="@android:drawable/ic_menu_add"
        android:title="Item 3"
        app:showAsAction="never" />
</menu>
    And in the main activity, putting this code:
MainActivity.java
package info.devexchanges.overflowbuttonevent;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        return super.onOptionsItemSelected(item);
    }

    @Override
    public boolean onMenuOpened(int featureId, Menu menu) {

        Log.i("MainActivity", "open");
        return super.onMenuOpened(featureId, menu);
    }

    @Override
    public void onPanelClosed(int featureId, Menu menu) {
        super.onPanelClosed(featureId, menu);
        Log.i("MainActivity", "closed");
    }
}
    After running this activity and open/close the menu in overflow button, we have this output in LogCat:
1686-1686/info.devexchanges.overflowbuttonevent I/MainActivity: open
1686-1686/info.devexchanges.overflowbuttonevent I/MainActivity: open
1686-1686/info.devexchanges.overflowbuttonevent I/MainActivity: closed
1686-1686/info.devexchanges.overflowbuttonevent I/MainActivity: closed
1686-1686/info.devexchanges.overflowbuttonevent I/MainActivity: closed
1686-1686/info.devexchanges.overflowbuttonevent I/MainActivity: closed
    As you can see, onMenuOpened() and onPanelClosed() were called many times with only one open/close action. If you would like to do something when the menu opened/closed, please attention, just do it at the first time 2 methods called!
    For example, showing a Toast when clicking at this overflow button, just rewrite your activity code like this:
MainActivity.java
package info.devexchanges.overflowbuttonevent;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    private boolean isOpened = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

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

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if (item.getItemId() == R.id.add) {
            Toast.makeText(this, "Adding button in action bar clicked", Toast.LENGTH_SHORT).show();
        }
        return super.onOptionsItemSelected(item);
    }

    @Override
    public boolean onMenuOpened(int featureId, Menu menu) {
        if (!isOpened) Toast.makeText(this, "Menu opened", Toast.LENGTH_SHORT).show();
        isOpened = true;
        return super.onMenuOpened(featureId, menu);
    }

    @Override
    public void onPanelClosed(int featureId, Menu menu) {
        super.onPanelClosed(featureId, menu);
        if (isOpened) Toast.makeText(this, "menu closed", Toast.LENGTH_SHORT).show();
        isOpened = false;
    }
}
    Running this code, you'll notice that the Toast show only one time when menu opened/closed:
    References:

Android Basic Training Course: Menu and ContextMenu

    Menus are a common user interface component in many types of applications. To provide a familiar and consistent user experience, we will use the Menu APIs to present user actions and other options in our activities. In this tut, I present a demo project about creating menus and use it.
DEMO VIDEO:

Android menu types

    Android platform provides 3 types of Menu:
- Option Menu: locating at ActionBar or Toolbar, is the primary menu in Android. It's typically used for providing additional info about an app, as well as linking to a help and settings sections.
- Context Menu: display when long click at a view, for example: a ListView row, usually provide customizing methods for this view.
- Popup Menu: display as a Pop-up Dialog when clicking a View like Button, ImageView,...

Option Menus

    Along with the arrival of the ActionBar in API level 11, this menu types located on it. Firstly, we must declare it's layout by creating a new menu resource file (xml) in res/menu folder:
    As you can see, a menu contains 1 or more items in it. Each item has some properties:
- android:id: requirement property, express the uniqueness of the menu entity.
- android:title: name of entity, show on user interface.
- android:icon: set drawable resource use for entity, show instead of it's title if drawable not null.
- app:showAsAction: if set "never", item will be hide in overflow menu in ActionBar, to visible it, we set "ifRoom" or "always".
And output of this menu (two sorting items is hidden in overflow icon, adding item is visible in ActionBar):
    In programmatically code, we must inflate this option menu to Activity through overriding onCreateOptionsMenu() method:
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }
    Handling each item click event by overriding onOptionsItemSelected(), distinguish each entity event through it's id:
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if (item.getItemId() == R.id.a_z) {
            sortByName(countries);
            return true;

        } else if (item.getItemId() == R.id.z_a) {
            reverseByName(countries);
            return true;

        } else if (item.getItemId() == R.id.add) {
            showAddingDialog();
        }

        return super.onOptionsItemSelected(item);
    }    
    Some necessary methods called in onOptionsItemSelected():
    /**
     * sort countries by name A - Z
     */
    private void sortByName(ArrayList<String> countries) {
        Collections.sort(countries, new Comparator<String>() {
            public int compare(String v1, String v2) {
                return v1.compareTo(v2);
            }
        });
    }

    /**
     * sort countries by name Z - A
     */
    private void reverseByName(ArrayList<String> countries) {
        Collections.sort(countries, Collections.reverseOrder(new Comparator<String>() {

            @Override
            public int compare(String s1, String s2) {
                return s1.compareTo(s2);
            }
        }));
    }

    private void showAddingDialog() {
        AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
        LayoutInflater inflater = this.getLayoutInflater();
        final View dialogView = inflater.inflate(R.layout.layout_add_dialog, null, false);
        dialogBuilder.setView(dialogView);

        final EditText editText = (EditText) dialogView.findViewById(R.id.edit_text);

        dialogBuilder.setTitle("Adding new Country");
        dialogBuilder.setMessage("Input a Country name");

        dialogBuilder.setPositiveButton("Done", new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface dialog, int whichButton) {
                if (editText.getText().toString().trim().equals("")) {
                    Toast.makeText(MainActivity.this, "Please input some texts!", Toast.LENGTH_SHORT).show();
                } else {
                    //add new country name to List
                    countries.add(editText.getText().toString().trim());

                    //update ListView adapter
                    adapter.notifyDataSetChanged();
                    Toast.makeText(MainActivity.this, "Adding sucessful!", Toast.LENGTH_SHORT).show();
                }
            }
        });
        dialogBuilder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface dialog, int whichButton) {
                //do nothing, just close this dialog
            }
        });
        AlertDialog b = dialogBuilder.create();
        b.show();
    }
    When running app, we have this screen:
 
    Clicking the "Add item" (the red plus icon), a Dialog will appear to add new ListView item:
    Clicking the overflow button in ActionBar, 2 remaining options is sorting item by name will appear:

Context Menu Type

   The context menu appears when users long-press user interface items, pressing the item and holding it until the menu appears. Android users are typically accustomed to this type of interaction, as it is standard for system functions such as altering home screen icons.
    Like option menu, it's also declared by xml file in res/menu folder:
    In programmatically code, create ContextMenu by overrding onCreateContextMenu():
    @Override
    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
        super.onCreateContextMenu(menu, v, menuInfo);
        getMenuInflater().inflate(R.menu.menu_context_main, menu);
        AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
        menu.setHeaderTitle(countries.get(info.position));
    }
    And handling each ContextMenu item event in onContextItemSelected():
    @Override
    public boolean onContextItemSelected(MenuItem item) {
        AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
        //find out which menu item was pressed
        switch (item.getItemId()) {
            case R.id.delete:
                countries.remove(info.position);
                adapter.notifyDataSetChanged();
                return true;
            case R.id.go_wiki:
                String listItemName = countries.get(info.position);
                openBrowser("https://en.wikipedia.org/wiki/" + listItemName);

                return true;
            default:
                return false;
        }
    }
    Note: through AdapterView.AdapterContextMenuInfo, ListView item was linked with the ContextMenu item.
    By long-press at each ListView item, a pop-up will be shown with it's title is ListView item name:
    Use this method to open Browser when entry "go to Wikipedia" on ContextMenu clicked:
private void openBrowser(String url) {
        Intent i = new Intent(Intent.ACTION_VIEW);
        i.setData(Uri.parse(url));

        startActivity(i);
    }

Popup Menu

   A PopupMenu displays a Menu in a modal popup window anchored to a View. The popup will appear below the anchor view if there is room, or above it if there is not. If the IME is visible the popup will not overlap it until it is touched. Touching outside of the popup will dismiss it. In this example, we show a PopupMenu when click a TextView, inflating it and handle it's items click event is so simple:
 private View.OnClickListener onShowPopupMenu() {
        return new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                popupMenu = new PopupMenu(MainActivity.this, v);
                popupMenu.setOnMenuItemClickListener(onPopupMenuClickListener());
                popupMenu.inflate(R.menu.menu_pop_up);
                popupMenu.show();
            }
        };
    }

    private PopupMenu.OnMenuItemClickListener onPopupMenuClickListener() {
        return new PopupMenu.OnMenuItemClickListener() {
            @Override
            public boolean onMenuItemClick(MenuItem item) {
                switch (item.getItemId()) {
                    case R.id.hide:
                        if (listView.getVisibility() == View.VISIBLE) {
                            //hiding listview
                            listView.setVisibility(View.GONE);
                        } else {
                            //showing listview
                            listView.setVisibility(View.VISIBLE);
                        }
                        return true;
                    case R.id.change_color:
                        listViewHeader.setBackgroundColor(Color.GREEN);
                        return true;
                    case R.id.others:
                        Toast.makeText(MainActivity.this, "Other option, please define later!", Toast.LENGTH_SHORT).show();
                        return true;

                    default:
                        return false;
                }
            }
        };
    }
    This PopupMenu will display below the parent view:

Completing our project

    At this project, I use Toolbar instead of ActionBar as recommended in Material Design style, so set Toolbar as ActionBar in onCreate(). Beside it, register ContextMenu in a defined view by call registerForContextMenu() method:
private ListView listView;
    private ArrayList<String> countries;
    private ArrayAdapter<String> adapter;
    private TextView listViewHeader;
    private Toolbar toolbar;
    private PopupMenu popupMenu;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        listView = (ListView) findViewById(R.id.list);
        toolbar = (Toolbar) findViewById(R.id.toolbar);
        listViewHeader = (TextView) findViewById(R.id.header_list);
        listViewHeader.setOnClickListener(onShowPopupMenu());

        addingDataToList();
        adapter = new ArrayAdapter<>(this, android.R.layout.simple_dropdown_item_1line, countries);
        listView.setAdapter(adapter);

        setSupportActionBar(toolbar);

        //register contextMenu with TextView
        registerForContextMenu(listView);
    }

    private void addingDataToList() {
        countries = new ArrayList<>();
        countries.add("Vietnam");
        countries.add("Japan");
        countries.add("China");
        countries.add("Singapore");
        countries.add("USA");
        countries.add("Russia");
        countries.add("Korea");
        countries.add("Thailand");
        countries.add("India");
        countries.add("Malaysia");
        countries.add("Philippines");
        countries.add("Germany");
        countries.add("France");
        countries.add("Netherlands");
    }
    In Activity layout (xml) file, including Toolbar layout:
    And this is a sample Toolbar layout:

Conclusions

    Through this post, you are learned about using menu types in Android. As with any development project, your apps will be more usable if they exploit the type of interaction and functionality users expect as standard. Using the menu is a good way to achieve this when providing informative sections.


Next Chapter