Android Core

Android: Menu Class Investigation

Android provides decent functionality for instantiating your own run of the mill Menu within the standard framework. They even have a half decent guide on using them within your applications. Most users will be well acquainted with the stock menu seeing how Google Maps, GMail, the Contacts list and even the default background window use them.

As comforting as they might be to your users (all things familiar are) to you and your design aesthetics they’re bland, boring and stale. But do they have to be? That is the question and it’s something I’d like to solve. Let’s start by investigatingthe where, why, and how of Android Menus. Specifically let’s find out where in the code the logic for the Menus live and then let’s find out how we can change it.

The Standard Theme

What’s a component of Android without a Theme to back it up? Of course, for different types of Menus there are different types of Themes. Here are the standard two (there are actually three) used for Menus taken from the XML repo on the git repository:

<style name="Theme.IconMenu">
 <!-- Menu/item attributes -->
 <item name="android:itemTextAppearance">@android:style/TextAppearance.Widget.IconMenu.Item</item>
    <item name="android:itemBackground">@android:drawable/menu_selector</item>
    <item name="android:itemIconDisabledAlpha">?android:attr/disabledAlpha</item>
    <item name="android:horizontalDivider">@android:drawable/divider_horizontal_bright</item>
    <item name="android:verticalDivider">@android:drawable/divider_vertical_bright</item>
    <item name="android:windowAnimationStyle">@android:style/Animation.OptionsPanel</item>
    <item name="android:moreIcon">@android:drawable/ic_menu_more</item>
    <item name="android:background">@null</item>
</style>

<style name="Theme.ExpandedMenu">
 <!-- Menu/item attributes -->
 <item name="android:itemTextAppearance">?android:attr/textAppearanceLargeInverse</item>
    <item name="android:listViewStyle">@android:style/Widget.ListView.Menu</item>
    <item name="android:windowAnimationStyle">@android:style/Animation.OptionsPanel</item>
    <item name="android:background">@null</item> 
</style>

The first theme is the one you see when you hit the menu button on your phone while the second is when you press the “more” options button on the first.
And if you check your manifest XML file:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.owein"
      android:versionCode="1"
      android:versionName="1.0">
 <application android:icon="@drawable/icon" android:label="@string/app_name"> 
 <activity android:name=".SampleApp" android:label="@string/app_name"> 
 <intent-filter> 
 <action android:name="android.intent.action.MAIN" /> 
 <category android:name="android.intent.category.LAUNCHER" /> 
     </intent-filter> 
     </activity> 
    </application> 
    <uses-sdk android:minSdkVersion="8" />
</manifest>

You see that these themes appear right in the part of the manifest…Um, right in the part of… Um… Wait a minute, I don’t see them anywhere! Where do they get ingested into the App? How am I supposed to overcome this limitation?

Ok, time for a little sleuthing. Put on your Dick Tracy hats. There’s a place where the Themes are ingested and once we find out where they are, we can figure out what needs to be done in order to substitute our own Themes.

Introducing IconMenuView and ExpandedMenuView

It’s safe to assume that the Menu that you see is actually a product of several classes; namely a listener for the actions and some child of the View class. Since the Android Dev guide gives no mention of how to personalize orstylizeour Menus we have to assume there is a reason for this. That reason must lurk deep within the bowls of the “internal” Android code.

One clue to finding where a Theme is ingested is where the Menu Views are instantiated, either in the View code itself or in the object that inflates the view. Normally, the Theme (and Style) would be retrieved from the Context class and placed intoa TypedArrayduring View construction. As luck would have it, that is exactly what happens within the classes:IconMenuView and ExpandedMenuView.

Here is the constructor from IconMenuView’s constructor:

public IconMenuView(Context context, AttributeSet attrs) {
    super(context, attrs);

    TypedArray a =
        context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.IconMenuView, 0, 0);
    
   mRowHeight = 
   a.getDimensionPixelSize(com.android.internal.R.styleable
      .IconMenuView_rowHeight, 64);

    mMaxRows = a.getInt(com.android.internal.R.styleable.IconMenuView_maxRows, 2);
    mMaxItems = a.getInt(com.android.internal.R.styleable.IconMenuView_maxItems, 6);
    
    mMaxItemsPerRow =  
      a.getInt(com.android.internal.R.styleable.IconMenuView_maxItemsPerRow, 3);

    mMoreIcon = 
      a.getDrawable(com.android.internal.R.styleable.IconMenuView_moreIcon);
    a.recycle();

    a = context.obtainStyledAttributes(attrs, 
       com.android.internal.R.styleable.MenuView, 0, 0);
    
    mItemBackground = 
       a.getDrawable(com.android.internal.R.styleable.MenuView_itemBackground);
   
    mHorizontalDivider = 
       a.getDrawable(com.android.internal.R.styleable.MenuView_horizontalDivider);
    
    mHorizontalDividerRects = new ArrayList<Rect>();
    mVerticalDivider =  
       a.getDrawable(com.android.internal.R.styleable.MenuView_verticalDivider);
    
    mVerticalDividerRects = new ArrayList<Rect>();
    
    mAnimations = 
       a.getResourceId(com.android.internal.R.styleable.
          MenuView_windowAnimationStyle, 0);
    a.recycle();

    if (mHorizontalDivider != null) {
        mHorizontalDividerHeight = mHorizontalDivider.getIntrinsicHeight();
    }

    // Make sure to have some height for the divider
    if (mHorizontalDividerHeight == -1){
        mHorizontalDividerHeight = 1;
    }

    if (mVerticalDivider != null) {
        mVerticalDividerWidth = mVerticalDivider.getIntrinsicWidth();

        // Make sure to have some width for the divider
        if (mVerticalDividerWidth == -1){
            mVerticalDividerWidth = 1;
        }
    }

    mLayout = new int[mMaxRows];

    // This view will be drawing the dividers
    setWillNotDraw(false);

    // This is so we'll receive the MENU key in touch mode
    setFocusableInTouchMode(true);
    // This is so our children can still be arrow-key focused
    setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
}

As is plainly visible, all those values are hard-coded in there. There’s really no way to substitute your own. You can’t even come up with a IconMenuView which has 4, 5 or 6 rows. You’d have to create your own class which derived from MenuViewin order to do that.

But the view is just one component of a Menu. What are the other components and how do they work together?

Building a Menu

Within the internal menu directory of the Android git repository there are a number of classes related to, you guessed it, menus. The ones we’re most interested in finding implement eitherMenu,ContextMenuor SubMenu. Why? If you look back at the Menu guide, under an Activity’s onCreateOptionsMenu they recommend the following code segment:

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.game_menu, menu);
    return true;
}

Which tells us that an Activity is hard-coded to expect a subclass of Menu. If we want to even begin to think about building our own custom Menu we’re going to have to work with this interface.
This section of the blog post is entitled “Building a Menu” for a reason. The classes which implement Menu are MenuBuilder, ContextMenuBuilder, and SubMenuBuilder. The MenuBuilder instantiates the view of the Menu with a call to the method getMenuView:

public View getMenuView(int menuType, ViewGroup parent) {
    if (menuType == TYPE_EXPANDED 
            && (mMenuTypes[TYPE_ICON] == null || !mMenuTypes[TYPE_ICON].hasMenuView())) {
        getMenuType(TYPE_ICON).getMenuView(parent);
    }
    return (View) getMenuType(menuType).getMenuView(parent);
}

which delegates to an inner class’ method (MenuType::getMenuView):

MenuView getMenuView(ViewGroup parent) {
    if (LAYOUT_RES_FOR_TYPE[mMenuType] == 0) {
        return null;
    }

    synchronized (this) {
        MenuView menuView = mMenuView != null ? mMenuView.get() : null;
        if (menuView == null) {
            menuView = (MenuView) getInflater().inflate(
                    LAYOUT_RES_FOR_TYPE[mMenuType], parent, false);
            menuView.initialize(MenuBuilder.this, mMenuType);
            mMenuView = new WeakReference<MenuView>(menuView);
            if (mFrozenViewStates != null) {
                View view = (View) menuView;
                view.restoreHierarchyState(mFrozenViewStates);
                mFrozenViewStates.remove(view.getId());
            }
        }
        return menuView;
    }
}

which in turn delegates the building of the View to aLayoutInflater. It is here, at this stage, that the hard-coded layout resource file is passed to the MenuView.

The LayoutInflator is retrieved from the getInflater() function call:

LayoutInflater getInflater() {
    // Create an inflater that uses the given theme for the Views it inflates
    if (mInflater == null) {
        Context wrappedContext = 
     new ContextThemeWrapper(mContext, THEME_RES_FOR_TYPE[mMenuType]);
        mInflater = (LayoutInflater) wrappedContext
                         .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    }

    return mInflater;
}

A ContextThemeWrapper makes sure that the stock Android Menu Theme replaces the Activity’s Theme. It’s hard-coded again.

Surprisingly, ContextMenuBuider and SubMenuBuilder do not implement their own versions. They rely on the same functionality as MenuBuilder. Each one of them is passed a hard-coded Theme. There’s no way to pass in your own Theme for a Menu. In order to do that, we’d need to create a custom builder which derived from Menu. This is starting to sound like a lot of work.

So if the LayoutInflator is responsible for creating the View that we see on our phones then what’s all this about a MenuInflaterand how do we get those buttons into the Menu itself?

Menu Inflation

So you’ve read the bit about Menu resourcesand creating Menus from the Android developer pages, right? The MenuInflater is what populates and places all the MenuItemsor SubMenus that occupy the MenuView. It’s job is to parse the menu.xml file and load the contents of that file into the view which is presented to your end user. So it doesn’t actually create nor assign which type of Menu you’re dealing with. That comes directly from the Acitivity itself.

So Where Are Menus Born?

What we want to know is how to separate the stock Activity from the stock Menus? To do that we need to know where the actual reference to a menu is held. Until we see where “new” is called upon a reference to a ContextMenu or an OptionsMenu we could write as many custom Menus as we wanted, it wouldn’t matter

Looking over the Activity class’ documentation on the Android developer there’s a number of methods relating to Menus like closeContextMenu, onContextMenuClosed, onContextItemSelected, onMenuOpened, onCreateContextMenu, ect. The last one is a sore spot as we’re dealing not with a method which operates on a Menu (allowing for fully customized behavior) but rather with a method that deals strictly with a ContextMenu (which stifles our creativity.) They really didn’t want us to have control over that menu button, pfft.

Since the Android developer’s guides on Activities in general or application fundamentals sheds no additional light on the situation we head yet again to the git repository only to find one particularly frustrating find:

public void openContextMenu(View view) {
    view.showContextMenu();
}

They’ve attached ContextMenu to every single View in existence! But I’m getting distracted. If I’m having so much trouble searching for a ContextMenu, perhaps OptionsMenu provides a more direct route:

public void openOptionsMenu() {
    mWindow.openPanel(Window.FEATURE_OPTIONS_PANEL, null);
}

where the mWindow variable is set within the attach method like so:

mWindow = PolicyManager.makeNewWindow(this);

and is of type Window, an interface. From the description on Window:

The only existing implementation of this abstract class is android.policy.PhoneWindow, which you should instantiate when needing a Window. Eventually that class will be refactored and a factory method added for creating Window instances without knowing about a particular implementation.

I think the PolicyManager is that factory method and we’ve already met PhoneWindow before in another blog post. So where are the Menus born? For a guess take a look at the following code snippet from PhoneWindow:

@Override
public boolean showContextMenuForChild(View originalView) {
    // Reuse the context menu builder
    if (mContextMenu == null) {
        mContextMenu = new ContextMenuBuilder(getContext());
        mContextMenu.setCallback(mContextMenuCallback);
    } else {
        mContextMenu.clearAll();
    }

    mContextMenuHelper = mContextMenu.show(originalView, 
       originalView.getWindowToken());
    return mContextMenuHelper != null;
}

Conclusion

I entitled this blog post “How can you Implement a Custom Menu Class?” without first knowing the answer. The whole reason I wanted to investigate this was to create an open source library for something I had yet to see done, custom Menus. I found very little in the tutorial/blogosphere regarding this.

To answer the question, you can implement them however you want, you’ll just never be able to use them. My question is to Google engineers. Why? Why is this so? Why can’t I pass in a custom Theme to my Menus? Why can’t I choose how to Style them? Yes, I can understand you not wanting some devious developer taking control of a phone by disabling the actions of all the phone’s buttons but the Menu button? Oh well. Onward and upward to the next pet project.

Reference: Android: How can you Implement a Custom Menu Class? from our JCG partner at Statically Typed.

Related Articles :
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button