/* * Copyright (C) 2006 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.view; import com.android.internal.view.menu.MenuItemImpl; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import android.app.Activity; import android.content.Context; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; import android.util.AttributeSet; import android.util.Xml; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.Method; /** * This class is used to instantiate menu XML files into Menu objects. *
* For performance reasons, menu inflation relies heavily on pre-processing of
* XML files that is done at build time. Therefore, it is not currently possible
* to use MenuInflater with an XmlPullParser over a plain XML file at runtime;
* it only works with an XmlPullParser returned from a compiled resource (R.
* something file.)
*/
public class MenuInflater {
/** Menu tag name in XML. */
private static final String XML_MENU = "menu";
/** Group tag name in XML. */
private static final String XML_GROUP = "group";
/** Item tag name in XML. */
private static final String XML_ITEM = "item";
private static final int NO_ID = 0;
private static final Class>[] ACTION_VIEW_CONSTRUCTOR_SIGNATURE = new Class[]{Context.class};
private Context mContext;
/**
* Constructs a menu inflater.
*
* @see Activity#getMenuInflater()
*/
public MenuInflater(Context context) {
mContext = context;
}
/**
* Inflate a menu hierarchy from the specified XML resource. Throws
* {@link InflateException} if there is an error.
*
* @param menuRes Resource ID for an XML layout resource to load (e.g.,
* R.menu.main_activity
)
* @param menu The Menu to inflate into. The items and submenus will be
* added to this Menu.
*/
public void inflate(int menuRes, Menu menu) {
XmlResourceParser parser = null;
try {
parser = mContext.getResources().getLayout(menuRes);
AttributeSet attrs = Xml.asAttributeSet(parser);
parseMenu(parser, attrs, menu);
} catch (XmlPullParserException e) {
throw new InflateException("Error inflating menu XML", e);
} catch (IOException e) {
throw new InflateException("Error inflating menu XML", e);
} finally {
if (parser != null) parser.close();
}
}
/**
* Called internally to fill the given menu. If a sub menu is seen, it will
* call this recursively.
*/
private void parseMenu(XmlPullParser parser, AttributeSet attrs, Menu menu)
throws XmlPullParserException, IOException {
MenuState menuState = new MenuState(menu);
int eventType = parser.getEventType();
String tagName;
boolean lookingForEndOfUnknownTag = false;
String unknownTagName = null;
// This loop will skip to the menu start tag
do {
if (eventType == XmlPullParser.START_TAG) {
tagName = parser.getName();
if (tagName.equals(XML_MENU)) {
// Go to next tag
eventType = parser.next();
break;
}
throw new RuntimeException("Expecting menu, got " + tagName);
}
eventType = parser.next();
} while (eventType != XmlPullParser.END_DOCUMENT);
boolean reachedEndOfMenu = false;
while (!reachedEndOfMenu) {
switch (eventType) {
case XmlPullParser.START_TAG:
if (lookingForEndOfUnknownTag) {
break;
}
tagName = parser.getName();
if (tagName.equals(XML_GROUP)) {
menuState.readGroup(attrs);
} else if (tagName.equals(XML_ITEM)) {
menuState.readItem(attrs);
} else if (tagName.equals(XML_MENU)) {
// A menu start tag denotes a submenu for an item
SubMenu subMenu = menuState.addSubMenuItem();
// Parse the submenu into returned SubMenu
parseMenu(parser, attrs, subMenu);
} else {
lookingForEndOfUnknownTag = true;
unknownTagName = tagName;
}
break;
case XmlPullParser.END_TAG:
tagName = parser.getName();
if (lookingForEndOfUnknownTag && tagName.equals(unknownTagName)) {
lookingForEndOfUnknownTag = false;
unknownTagName = null;
} else if (tagName.equals(XML_GROUP)) {
menuState.resetGroup();
} else if (tagName.equals(XML_ITEM)) {
// Add the item if it hasn't been added (if the item was
// a submenu, it would have been added already)
if (!menuState.hasAddedItem()) {
menuState.addItem();
}
} else if (tagName.equals(XML_MENU)) {
reachedEndOfMenu = true;
}
break;
case XmlPullParser.END_DOCUMENT:
throw new RuntimeException("Unexpected end of document");
}
eventType = parser.next();
}
}
private static class InflatedOnMenuItemClickListener
implements MenuItem.OnMenuItemClickListener {
private static final Class[] PARAM_TYPES = new Class[] { MenuItem.class };
private Context mContext;
private Method mMethod;
public InflatedOnMenuItemClickListener(Context context, String methodName) {
mContext = context;
Class c = context.getClass();
try {
mMethod = c.getMethod(methodName, PARAM_TYPES);
} catch (Exception e) {
InflateException ex = new InflateException(
"Couldn't resolve menu item onClick handler " + methodName +
" in class " + c.getName());
ex.initCause(e);
throw ex;
}
}
public boolean onMenuItemClick(MenuItem item) {
try {
if (mMethod.getReturnType() == Boolean.TYPE) {
return (Boolean) mMethod.invoke(mContext, item);
} else {
mMethod.invoke(mContext, item);
return true;
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
/**
* State for the current menu.
*
* Groups can not be nested unless there is another menu (which will have * its state class). */ private class MenuState { private Menu menu; /* * Group state is set on items as they are added, allowing an item to * override its group state. (As opposed to set on items at the group end tag.) */ private int groupId; private int groupCategory; private int groupOrder; private int groupCheckable; private boolean groupVisible; private boolean groupEnabled; private boolean itemAdded; private int itemId; private int itemCategoryOrder; private CharSequence itemTitle; private CharSequence itemTitleCondensed; private int itemIconResId; private char itemAlphabeticShortcut; private char itemNumericShortcut; /** * Sync to attrs.xml enum: * - 0: none * - 1: all * - 2: exclusive */ private int itemCheckable; private boolean itemChecked; private boolean itemVisible; private boolean itemEnabled; /** * Sync to attrs.xml enum, values in MenuItem: * - 0: never * - 1: ifRoom * - 2: always * - -1: Safe sentinel for "no value". */ private int itemShowAsAction; private int itemActionViewLayout; private String itemActionViewClassName; private String itemListenerMethodName; private static final int defaultGroupId = NO_ID; private static final int defaultItemId = NO_ID; private static final int defaultItemCategory = 0; private static final int defaultItemOrder = 0; private static final int defaultItemCheckable = 0; private static final boolean defaultItemChecked = false; private static final boolean defaultItemVisible = true; private static final boolean defaultItemEnabled = true; public MenuState(final Menu menu) { this.menu = menu; resetGroup(); } public void resetGroup() { groupId = defaultGroupId; groupCategory = defaultItemCategory; groupOrder = defaultItemOrder; groupCheckable = defaultItemCheckable; groupVisible = defaultItemVisible; groupEnabled = defaultItemEnabled; } /** * Called when the parser is pointing to a group tag. */ public void readGroup(AttributeSet attrs) { TypedArray a = mContext.obtainStyledAttributes(attrs, com.android.internal.R.styleable.MenuGroup); groupId = a.getResourceId(com.android.internal.R.styleable.MenuGroup_id, defaultGroupId); groupCategory = a.getInt(com.android.internal.R.styleable.MenuGroup_menuCategory, defaultItemCategory); groupOrder = a.getInt(com.android.internal.R.styleable.MenuGroup_orderInCategory, defaultItemOrder); groupCheckable = a.getInt(com.android.internal.R.styleable.MenuGroup_checkableBehavior, defaultItemCheckable); groupVisible = a.getBoolean(com.android.internal.R.styleable.MenuGroup_visible, defaultItemVisible); groupEnabled = a.getBoolean(com.android.internal.R.styleable.MenuGroup_enabled, defaultItemEnabled); a.recycle(); } /** * Called when the parser is pointing to an item tag. */ public void readItem(AttributeSet attrs) { TypedArray a = mContext.obtainStyledAttributes(attrs, com.android.internal.R.styleable.MenuItem); // Inherit attributes from the group as default value itemId = a.getResourceId(com.android.internal.R.styleable.MenuItem_id, defaultItemId); final int category = a.getInt(com.android.internal.R.styleable.MenuItem_menuCategory, groupCategory); final int order = a.getInt(com.android.internal.R.styleable.MenuItem_orderInCategory, groupOrder); itemCategoryOrder = (category & Menu.CATEGORY_MASK) | (order & Menu.USER_MASK); itemTitle = a.getText(com.android.internal.R.styleable.MenuItem_title); itemTitleCondensed = a.getText(com.android.internal.R.styleable.MenuItem_titleCondensed); itemIconResId = a.getResourceId(com.android.internal.R.styleable.MenuItem_icon, 0); itemAlphabeticShortcut = getShortcut(a.getString(com.android.internal.R.styleable.MenuItem_alphabeticShortcut)); itemNumericShortcut = getShortcut(a.getString(com.android.internal.R.styleable.MenuItem_numericShortcut)); if (a.hasValue(com.android.internal.R.styleable.MenuItem_checkable)) { // Item has attribute checkable, use it itemCheckable = a.getBoolean(com.android.internal.R.styleable.MenuItem_checkable, false) ? 1 : 0; } else { // Item does not have attribute, use the group's (group can have one more state // for checkable that represents the exclusive checkable) itemCheckable = groupCheckable; } itemChecked = a.getBoolean(com.android.internal.R.styleable.MenuItem_checked, defaultItemChecked); itemVisible = a.getBoolean(com.android.internal.R.styleable.MenuItem_visible, groupVisible); itemEnabled = a.getBoolean(com.android.internal.R.styleable.MenuItem_enabled, groupEnabled); itemShowAsAction = a.getInt(com.android.internal.R.styleable.MenuItem_showAsAction, -1); itemListenerMethodName = a.getString(com.android.internal.R.styleable.MenuItem_onClick); itemActionViewLayout = a.getResourceId(com.android.internal.R.styleable.MenuItem_actionLayout, 0); itemActionViewClassName = a.getString(com.android.internal.R.styleable.MenuItem_actionViewClass); a.recycle(); itemAdded = false; } private char getShortcut(String shortcutString) { if (shortcutString == null) { return 0; } else { return shortcutString.charAt(0); } } private void setItem(MenuItem item) { item.setChecked(itemChecked) .setVisible(itemVisible) .setEnabled(itemEnabled) .setCheckable(itemCheckable >= 1) .setTitleCondensed(itemTitleCondensed) .setIcon(itemIconResId) .setAlphabeticShortcut(itemAlphabeticShortcut) .setNumericShortcut(itemNumericShortcut); if (itemShowAsAction >= 0) { item.setShowAsAction(itemShowAsAction); } if (itemListenerMethodName != null) { if (mContext.isRestricted()) { throw new IllegalStateException("The android:onClick attribute cannot " + "be used within a restricted context"); } item.setOnMenuItemClickListener( new InflatedOnMenuItemClickListener(mContext, itemListenerMethodName)); } if (item instanceof MenuItemImpl) { MenuItemImpl impl = (MenuItemImpl) item; if (itemCheckable >= 2) { impl.setExclusiveCheckable(true); } } if (itemActionViewClassName != null) { try { final Class> clazz = Class.forName(itemActionViewClassName, true, mContext.getClassLoader()); Constructor> c = clazz.getConstructor(ACTION_VIEW_CONSTRUCTOR_SIGNATURE); item.setActionView((View) c.newInstance(mContext)); } catch (Exception e) { throw new InflateException(e); } } else if (itemActionViewLayout > 0) { item.setActionView(itemActionViewLayout); } } public void addItem() { itemAdded = true; setItem(menu.add(groupId, itemId, itemCategoryOrder, itemTitle)); } public SubMenu addSubMenuItem() { itemAdded = true; SubMenu subMenu = menu.addSubMenu(groupId, itemId, itemCategoryOrder, itemTitle); setItem(subMenu.getItem()); return subMenu; } public boolean hasAddedItem() { return itemAdded; } } }