Tags

, , , , , ,

One of the greatest myths in Android is that a new and custom menu cannot be created. Android doesn’t expose enough methods to create our own menu. Though starting Honeycomb, it added the functionality to create PopupMenu, the pre-Honeycomb phones are left with using the “5 + more” provided by the device manufacturer.

This is not true! In Firefox for Android, we implemented our own custom menu to match the look and feel of the app. The good thing about the custom menu (though enabled for only Honeycomb+ users) is that, it fits right inside the actual menu container provided by Android. So, on devices that doesn’t have a hardware button, it shows as a popup, and one those that have a hardware menu button, it shows on the side of the menu (as it’s inside original Android container).

And how did we break the myth? After going through the Android code, I found a loophole in one of the comments.

As per: https://github.com/android/platform_frameworks_base/blob/master/core/java/android/view/Window.java
/**
 * Instantiate the view to display in the panel for 'featureId'.
 * You can return null, in which case the default content (typically
 * a menu) will be created for you.
 */
public View onCreatePanelView(int featureId);

/**
* Prepare a panel to be displayed. This is called right before the
* panel window is shown, every time it is shown.
*
* @param featureId The panel that is being displayed.
* @param view The View that was returned by onCreatePanelView().
* @param menu If onCreatePanelView() returned null, this is the Menu
* being displayed in the panel.
*/
public boolean onPreparePanel(int featureId, View view, Menu menu);

So, returning a non-null for onCreatePanelView() will make Android use our view for the menu. Though we can (and should) hold a reference to the view we returned, Android gives us a reference to it back in the call to onPreparePanel(). Additionally, onPreparePanel() gives us the reference to the Menu to be inflated. That’s it! We have everything in hand to inflate a custom menu in the way we want!

The following code shows a skeleton for implementing a custom menu. CustomMenuInflater is needed to parse the menu entries for you, as the menu attributes aren’t publicly exposed by Android. Also, Android’s MenuInflater inflates the default menu, which we don’t want.

@Override
public MenuInflater getMenuInflater() {
    return new CustomMenuInflater();
}

MenuPanel is the custom View that holds the Custom Menu. This is the non-null value, that should be created and returned to the Android, so that it can stop creating the standard menu. Additionally, if the panel is already created, we can prepare the panel for subsequent calls.

@Override
public View onCreatePanelView(int featureId) {
    if (featureId == Window.FEATURE_OPTIONS_PANEL) {
        if (mMenuPanel == null) {
            mMenuPanel = new MenuPanel(_activity_context_, null);
        } else {
            // mMenu is a reference to the menu that Android gave us.
            onPreparePanel(featureId, mMenuPanel, mMenu);
        }
        return mMenuPanel;
    }

    return super.onCreatePanelView(featureId);
}

Android provides the option to create the menu of our dreams in onCreatePanelMenu(). This is the place where we can initialize our CustomMenu and inflate for the first time with a call to onCreateOptionsMenu(), which inturn doesn’t differ from how we usually do Menu intialization.

@Override
public boolean onCreatePanelMenu(int featureId, Menu menu) {
    if (featureId == Window.FEATURE_OPTIONS_PANEL) {
        if (mMenuPanel == null) {
            mMenuPanel = (MenuPanel) onCreatePanelView(featureId);
        }

        CustomMenu cMenu = new CustomMenu(_activity_context_, null);
        menu = cMenu;
        mMenuPanel.addView(cMenu);

        // Inflate the menu.
        return onCreateOptionsMenu(menu);
    }

    super.onCreatePanelMenu(featureId, menu);
}

And the last pieces of code to satisfy common preparation, and opening of menu in Honeycomb+ devices. The openOptionsMenu and closeOptionsMenu should open/close the custom menu (may be a visibility change or a cool animation).

@Override
public boolean onPreparePanel(int featureId, View view, Menu menu) {
    if (featureId == Window.FEATURE_OPTIONS_PANEL)
        return onPrepareOptionsMenu(menu);
    return super.onPreparePanel(featureId, view, menu);
}

@Override
public boolean onMenuOpened(int featureId, Menu menu) {
    if (featureId == Window.FEATURE_OPTIONS_PANEL) {
        if (mMenu == null) {
            onCreatePanelMenu(featureId, menu);
            onPreparePanel(featureId, mMenuPanel, mMenu);
        }

        return true;
     }

     return super.onMenuOpened(featureId, menu);
}

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
    // Custom Menu should be opened when hardware menu key is pressed.
    if (keyCode == KeyEvent.KEYCODE_MENU) {
        openOptionsMenu();
        return true;
    }

    return super.onKeyDown(keyCode, event);
}
Advertisements