2 * Copyright (C) 2006 The Android Open Source Project
\r
5 * Licensed under the Apache License, Version 2.0 (the "License");
\r
6 * you may not use this file except in compliance with the License.
\r
7 * You may obtain a copy of the License at
\r
9 * http://www.apache.org/licenses/LICENSE-2.0
\r
11 * Unless required by applicable law or agreed to in writing, software
\r
12 * distributed under the License is distributed on an "AS IS" BASIS,
\r
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
\r
14 * See the License for the specific language governing permissions and
\r
15 * limitations under the License.
\r
18 package com.actionbarsherlock.view;
\r
20 import java.io.IOException;
\r
21 import java.lang.reflect.Constructor;
\r
22 import java.lang.reflect.Method;
\r
23 import org.xmlpull.v1.XmlPullParser;
\r
24 import org.xmlpull.v1.XmlPullParserException;
\r
25 import android.content.Context;
\r
26 import android.content.res.TypedArray;
\r
27 import android.content.res.XmlResourceParser;
\r
28 import android.util.AttributeSet;
\r
29 import android.util.Log;
\r
30 import android.util.TypedValue;
\r
31 import android.util.Xml;
\r
32 import android.view.InflateException;
\r
33 import android.view.View;
\r
35 import com.actionbarsherlock.R;
\r
36 import com.actionbarsherlock.internal.view.menu.MenuItemImpl;
\r
39 * This class is used to instantiate menu XML files into Menu objects.
\r
41 * For performance reasons, menu inflation relies heavily on pre-processing of
\r
42 * XML files that is done at build time. Therefore, it is not currently possible
\r
43 * to use MenuInflater with an XmlPullParser over a plain XML file at runtime;
\r
44 * it only works with an XmlPullParser returned from a compiled resource (R.
\r
45 * <em>something</em> file.)
\r
47 public class MenuInflater {
\r
48 private static final String LOG_TAG = "MenuInflater";
\r
50 /** Menu tag name in XML. */
\r
51 private static final String XML_MENU = "menu";
\r
53 /** Group tag name in XML. */
\r
54 private static final String XML_GROUP = "group";
\r
56 /** Item tag name in XML. */
\r
57 private static final String XML_ITEM = "item";
\r
59 private static final int NO_ID = 0;
\r
61 private static final Class<?>[] ACTION_VIEW_CONSTRUCTOR_SIGNATURE = new Class[] {Context.class};
\r
63 private static final Class<?>[] ACTION_PROVIDER_CONSTRUCTOR_SIGNATURE = ACTION_VIEW_CONSTRUCTOR_SIGNATURE;
\r
65 private final Object[] mActionViewConstructorArguments;
\r
67 private final Object[] mActionProviderConstructorArguments;
\r
69 private Context mContext;
\r
72 * Constructs a menu inflater.
\r
74 * @see Activity#getMenuInflater()
\r
76 public MenuInflater(Context context) {
\r
78 mActionViewConstructorArguments = new Object[] {context};
\r
79 mActionProviderConstructorArguments = mActionViewConstructorArguments;
\r
83 * Inflate a menu hierarchy from the specified XML resource. Throws
\r
84 * {@link InflateException} if there is an error.
\r
86 * @param menuRes Resource ID for an XML layout resource to load (e.g.,
\r
87 * <code>R.menu.main_activity</code>)
\r
88 * @param menu The Menu to inflate into. The items and submenus will be
\r
89 * added to this Menu.
\r
91 public void inflate(int menuRes, Menu menu) {
\r
92 XmlResourceParser parser = null;
\r
94 parser = mContext.getResources().getLayout(menuRes);
\r
95 AttributeSet attrs = Xml.asAttributeSet(parser);
\r
97 parseMenu(parser, attrs, menu);
\r
98 } catch (XmlPullParserException e) {
\r
99 throw new InflateException("Error inflating menu XML", e);
\r
100 } catch (IOException e) {
\r
101 throw new InflateException("Error inflating menu XML", e);
\r
103 if (parser != null) parser.close();
\r
108 * Called internally to fill the given menu. If a sub menu is seen, it will
\r
109 * call this recursively.
\r
111 private void parseMenu(XmlPullParser parser, AttributeSet attrs, Menu menu)
\r
112 throws XmlPullParserException, IOException {
\r
113 MenuState menuState = new MenuState(menu);
\r
115 int eventType = parser.getEventType();
\r
117 boolean lookingForEndOfUnknownTag = false;
\r
118 String unknownTagName = null;
\r
120 // This loop will skip to the menu start tag
\r
122 if (eventType == XmlPullParser.START_TAG) {
\r
123 tagName = parser.getName();
\r
124 if (tagName.equals(XML_MENU)) {
\r
126 eventType = parser.next();
\r
130 throw new RuntimeException("Expecting menu, got " + tagName);
\r
132 eventType = parser.next();
\r
133 } while (eventType != XmlPullParser.END_DOCUMENT);
\r
135 boolean reachedEndOfMenu = false;
\r
136 while (!reachedEndOfMenu) {
\r
137 switch (eventType) {
\r
138 case XmlPullParser.START_TAG:
\r
139 if (lookingForEndOfUnknownTag) {
\r
143 tagName = parser.getName();
\r
144 if (tagName.equals(XML_GROUP)) {
\r
145 menuState.readGroup(attrs);
\r
146 } else if (tagName.equals(XML_ITEM)) {
\r
147 menuState.readItem(attrs);
\r
148 } else if (tagName.equals(XML_MENU)) {
\r
149 // A menu start tag denotes a submenu for an item
\r
150 SubMenu subMenu = menuState.addSubMenuItem();
\r
152 // Parse the submenu into returned SubMenu
\r
153 parseMenu(parser, attrs, subMenu);
\r
155 lookingForEndOfUnknownTag = true;
\r
156 unknownTagName = tagName;
\r
160 case XmlPullParser.END_TAG:
\r
161 tagName = parser.getName();
\r
162 if (lookingForEndOfUnknownTag && tagName.equals(unknownTagName)) {
\r
163 lookingForEndOfUnknownTag = false;
\r
164 unknownTagName = null;
\r
165 } else if (tagName.equals(XML_GROUP)) {
\r
166 menuState.resetGroup();
\r
167 } else if (tagName.equals(XML_ITEM)) {
\r
168 // Add the item if it hasn't been added (if the item was
\r
169 // a submenu, it would have been added already)
\r
170 if (!menuState.hasAddedItem()) {
\r
171 if (menuState.itemActionProvider != null &&
\r
172 menuState.itemActionProvider.hasSubMenu()) {
\r
173 menuState.addSubMenuItem();
\r
175 menuState.addItem();
\r
178 } else if (tagName.equals(XML_MENU)) {
\r
179 reachedEndOfMenu = true;
\r
183 case XmlPullParser.END_DOCUMENT:
\r
184 throw new RuntimeException("Unexpected end of document");
\r
187 eventType = parser.next();
\r
191 private static class InflatedOnMenuItemClickListener
\r
192 implements MenuItem.OnMenuItemClickListener {
\r
193 private static final Class<?>[] PARAM_TYPES = new Class[] { MenuItem.class };
\r
195 private Context mContext;
\r
196 private Method mMethod;
\r
198 public InflatedOnMenuItemClickListener(Context context, String methodName) {
\r
199 mContext = context;
\r
200 Class<?> c = context.getClass();
\r
202 mMethod = c.getMethod(methodName, PARAM_TYPES);
\r
203 } catch (Exception e) {
\r
204 InflateException ex = new InflateException(
\r
205 "Couldn't resolve menu item onClick handler " + methodName +
\r
206 " in class " + c.getName());
\r
212 public boolean onMenuItemClick(MenuItem item) {
\r
214 if (mMethod.getReturnType() == Boolean.TYPE) {
\r
215 return (Boolean) mMethod.invoke(mContext, item);
\r
217 mMethod.invoke(mContext, item);
\r
220 } catch (Exception e) {
\r
221 throw new RuntimeException(e);
\r
227 * State for the current menu.
\r
229 * Groups can not be nested unless there is another menu (which will have
\r
230 * its state class).
\r
232 private class MenuState {
\r
236 * Group state is set on items as they are added, allowing an item to
\r
237 * override its group state. (As opposed to set on items at the group end tag.)
\r
239 private int groupId;
\r
240 private int groupCategory;
\r
241 private int groupOrder;
\r
242 private int groupCheckable;
\r
243 private boolean groupVisible;
\r
244 private boolean groupEnabled;
\r
246 private boolean itemAdded;
\r
247 private int itemId;
\r
248 private int itemCategoryOrder;
\r
249 private CharSequence itemTitle;
\r
250 private CharSequence itemTitleCondensed;
\r
251 private int itemIconResId;
\r
252 private char itemAlphabeticShortcut;
\r
253 private char itemNumericShortcut;
\r
255 * Sync to attrs.xml enum:
\r
260 private int itemCheckable;
\r
261 private boolean itemChecked;
\r
262 private boolean itemVisible;
\r
263 private boolean itemEnabled;
\r
266 * Sync to attrs.xml enum, values in MenuItem:
\r
270 * - -1: Safe sentinel for "no value".
\r
272 private int itemShowAsAction;
\r
274 private int itemActionViewLayout;
\r
275 private String itemActionViewClassName;
\r
276 private String itemActionProviderClassName;
\r
278 private String itemListenerMethodName;
\r
280 private ActionProvider itemActionProvider;
\r
282 private static final int defaultGroupId = NO_ID;
\r
283 private static final int defaultItemId = NO_ID;
\r
284 private static final int defaultItemCategory = 0;
\r
285 private static final int defaultItemOrder = 0;
\r
286 private static final int defaultItemCheckable = 0;
\r
287 private static final boolean defaultItemChecked = false;
\r
288 private static final boolean defaultItemVisible = true;
\r
289 private static final boolean defaultItemEnabled = true;
\r
291 public MenuState(final Menu menu) {
\r
297 public void resetGroup() {
\r
298 groupId = defaultGroupId;
\r
299 groupCategory = defaultItemCategory;
\r
300 groupOrder = defaultItemOrder;
\r
301 groupCheckable = defaultItemCheckable;
\r
302 groupVisible = defaultItemVisible;
\r
303 groupEnabled = defaultItemEnabled;
\r
307 * Called when the parser is pointing to a group tag.
\r
309 public void readGroup(AttributeSet attrs) {
\r
310 TypedArray a = mContext.obtainStyledAttributes(attrs,
\r
311 R.styleable.SherlockMenuGroup);
\r
313 groupId = a.getResourceId(R.styleable.SherlockMenuGroup_android_id, defaultGroupId);
\r
314 groupCategory = a.getInt(R.styleable.SherlockMenuGroup_android_menuCategory, defaultItemCategory);
\r
315 groupOrder = a.getInt(R.styleable.SherlockMenuGroup_android_orderInCategory, defaultItemOrder);
\r
316 groupCheckable = a.getInt(R.styleable.SherlockMenuGroup_android_checkableBehavior, defaultItemCheckable);
\r
317 groupVisible = a.getBoolean(R.styleable.SherlockMenuGroup_android_visible, defaultItemVisible);
\r
318 groupEnabled = a.getBoolean(R.styleable.SherlockMenuGroup_android_enabled, defaultItemEnabled);
\r
324 * Called when the parser is pointing to an item tag.
\r
326 public void readItem(AttributeSet attrs) {
\r
327 TypedArray a = mContext.obtainStyledAttributes(attrs,
\r
328 R.styleable.SherlockMenuItem);
\r
330 // Inherit attributes from the group as default value
\r
331 itemId = a.getResourceId(R.styleable.SherlockMenuItem_android_id, defaultItemId);
\r
332 final int category = a.getInt(R.styleable.SherlockMenuItem_android_menuCategory, groupCategory);
\r
333 final int order = a.getInt(R.styleable.SherlockMenuItem_android_orderInCategory, groupOrder);
\r
334 itemCategoryOrder = (category & Menu.CATEGORY_MASK) | (order & Menu.USER_MASK);
\r
335 itemTitle = a.getText(R.styleable.SherlockMenuItem_android_title);
\r
336 itemTitleCondensed = a.getText(R.styleable.SherlockMenuItem_android_titleCondensed);
\r
337 itemIconResId = a.getResourceId(R.styleable.SherlockMenuItem_android_icon, 0);
\r
338 itemAlphabeticShortcut =
\r
339 getShortcut(a.getString(R.styleable.SherlockMenuItem_android_alphabeticShortcut));
\r
340 itemNumericShortcut =
\r
341 getShortcut(a.getString(R.styleable.SherlockMenuItem_android_numericShortcut));
\r
342 if (a.hasValue(R.styleable.SherlockMenuItem_android_checkable)) {
\r
343 // Item has attribute checkable, use it
\r
344 itemCheckable = a.getBoolean(R.styleable.SherlockMenuItem_android_checkable, false) ? 1 : 0;
\r
346 // Item does not have attribute, use the group's (group can have one more state
\r
347 // for checkable that represents the exclusive checkable)
\r
348 itemCheckable = groupCheckable;
\r
351 itemChecked = a.getBoolean(R.styleable.SherlockMenuItem_android_checked, defaultItemChecked);
\r
352 itemVisible = a.getBoolean(R.styleable.SherlockMenuItem_android_visible, groupVisible);
\r
353 itemEnabled = a.getBoolean(R.styleable.SherlockMenuItem_android_enabled, groupEnabled);
\r
355 TypedValue value = new TypedValue();
\r
356 a.getValue(R.styleable.SherlockMenuItem_android_showAsAction, value);
\r
357 itemShowAsAction = value.type == TypedValue.TYPE_INT_HEX ? value.data : -1;
\r
359 itemListenerMethodName = a.getString(R.styleable.SherlockMenuItem_android_onClick);
\r
360 itemActionViewLayout = a.getResourceId(R.styleable.SherlockMenuItem_android_actionLayout, 0);
\r
361 itemActionViewClassName = a.getString(R.styleable.SherlockMenuItem_android_actionViewClass);
\r
362 itemActionProviderClassName = a.getString(R.styleable.SherlockMenuItem_android_actionProviderClass);
\r
364 final boolean hasActionProvider = itemActionProviderClassName != null;
\r
365 if (hasActionProvider && itemActionViewLayout == 0 && itemActionViewClassName == null) {
\r
366 itemActionProvider = newInstance(itemActionProviderClassName,
\r
367 ACTION_PROVIDER_CONSTRUCTOR_SIGNATURE,
\r
368 mActionProviderConstructorArguments);
\r
370 if (hasActionProvider) {
\r
371 Log.w(LOG_TAG, "Ignoring attribute 'actionProviderClass'."
\r
372 + " Action view already specified.");
\r
374 itemActionProvider = null;
\r
382 private char getShortcut(String shortcutString) {
\r
383 if (shortcutString == null) {
\r
386 return shortcutString.charAt(0);
\r
390 private void setItem(MenuItem item) {
\r
391 item.setChecked(itemChecked)
\r
392 .setVisible(itemVisible)
\r
393 .setEnabled(itemEnabled)
\r
394 .setCheckable(itemCheckable >= 1)
\r
395 .setTitleCondensed(itemTitleCondensed)
\r
396 .setIcon(itemIconResId)
\r
397 .setAlphabeticShortcut(itemAlphabeticShortcut)
\r
398 .setNumericShortcut(itemNumericShortcut);
\r
400 if (itemShowAsAction >= 0) {
\r
401 item.setShowAsAction(itemShowAsAction);
\r
404 if (itemListenerMethodName != null) {
\r
405 if (mContext.isRestricted()) {
\r
406 throw new IllegalStateException("The android:onClick attribute cannot "
\r
407 + "be used within a restricted context");
\r
409 item.setOnMenuItemClickListener(
\r
410 new InflatedOnMenuItemClickListener(mContext, itemListenerMethodName));
\r
413 if (itemCheckable >= 2) {
\r
414 if (item instanceof MenuItemImpl) {
\r
415 MenuItemImpl impl = (MenuItemImpl) item;
\r
416 impl.setExclusiveCheckable(true);
\r
418 menu.setGroupCheckable(groupId, true, true);
\r
422 boolean actionViewSpecified = false;
\r
423 if (itemActionViewClassName != null) {
\r
424 View actionView = (View) newInstance(itemActionViewClassName,
\r
425 ACTION_VIEW_CONSTRUCTOR_SIGNATURE, mActionViewConstructorArguments);
\r
426 item.setActionView(actionView);
\r
427 actionViewSpecified = true;
\r
429 if (itemActionViewLayout > 0) {
\r
430 if (!actionViewSpecified) {
\r
431 item.setActionView(itemActionViewLayout);
\r
432 actionViewSpecified = true;
\r
434 Log.w(LOG_TAG, "Ignoring attribute 'itemActionViewLayout'."
\r
435 + " Action view already specified.");
\r
438 if (itemActionProvider != null) {
\r
439 item.setActionProvider(itemActionProvider);
\r
443 public void addItem() {
\r
445 setItem(menu.add(groupId, itemId, itemCategoryOrder, itemTitle));
\r
448 public SubMenu addSubMenuItem() {
\r
450 SubMenu subMenu = menu.addSubMenu(groupId, itemId, itemCategoryOrder, itemTitle);
\r
451 setItem(subMenu.getItem());
\r
455 public boolean hasAddedItem() {
\r
459 @SuppressWarnings("unchecked")
\r
460 private <T> T newInstance(String className, Class<?>[] constructorSignature,
\r
461 Object[] arguments) {
\r
463 Class<?> clazz = mContext.getClassLoader().loadClass(className);
\r
464 Constructor<?> constructor = clazz.getConstructor(constructorSignature);
\r
465 return (T) constructor.newInstance(arguments);
\r
466 } catch (Exception e) {
\r
467 Log.w(LOG_TAG, "Cannot instantiate class: " + className, e);
\r