2 * Copyright (C) 2006 The Android Open Source Project
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
9 * http://www.apache.org/licenses/LICENSE-2.0
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
18 package com
.actionbarsherlock
.view
;
20 import java
.io
.IOException
;
21 import java
.lang
.reflect
.Constructor
;
22 import java
.lang
.reflect
.Method
;
23 import org
.xmlpull
.v1
.XmlPullParser
;
24 import org
.xmlpull
.v1
.XmlPullParserException
;
25 import android
.content
.Context
;
26 import android
.content
.res
.TypedArray
;
27 import android
.content
.res
.XmlResourceParser
;
28 import android
.util
.AttributeSet
;
29 import android
.util
.Log
;
30 import android
.util
.TypedValue
;
31 import android
.util
.Xml
;
32 import android
.view
.InflateException
;
33 import android
.view
.View
;
35 import com
.actionbarsherlock
.R
;
36 import com
.actionbarsherlock
.internal
.view
.menu
.MenuItemImpl
;
39 * This class is used to instantiate menu XML files into Menu objects.
41 * For performance reasons, menu inflation relies heavily on pre-processing of
42 * XML files that is done at build time. Therefore, it is not currently possible
43 * to use MenuInflater with an XmlPullParser over a plain XML file at runtime;
44 * it only works with an XmlPullParser returned from a compiled resource (R.
45 * <em>something</em> file.)
47 public class MenuInflater
{
48 private static final String LOG_TAG
= "MenuInflater";
50 /** Menu tag name in XML. */
51 private static final String XML_MENU
= "menu";
53 /** Group tag name in XML. */
54 private static final String XML_GROUP
= "group";
56 /** Item tag name in XML. */
57 private static final String XML_ITEM
= "item";
59 private static final int NO_ID
= 0;
61 private static final Class
<?
>[] ACTION_VIEW_CONSTRUCTOR_SIGNATURE
= new Class
[] {Context
.class};
63 private static final Class
<?
>[] ACTION_PROVIDER_CONSTRUCTOR_SIGNATURE
= ACTION_VIEW_CONSTRUCTOR_SIGNATURE
;
65 private final Object
[] mActionViewConstructorArguments
;
67 private final Object
[] mActionProviderConstructorArguments
;
69 private Context mContext
;
70 private Object mRealOwner
;
73 * Constructs a menu inflater.
75 * @see Activity#getMenuInflater()
77 public MenuInflater(Context context
) {
80 mActionViewConstructorArguments
= new Object
[] {context
};
81 mActionProviderConstructorArguments
= mActionViewConstructorArguments
;
85 * Constructs a menu inflater.
87 * @see Activity#getMenuInflater()
90 public MenuInflater(Context context
, Object realOwner
) {
92 mRealOwner
= realOwner
;
93 mActionViewConstructorArguments
= new Object
[] {context
};
94 mActionProviderConstructorArguments
= mActionViewConstructorArguments
;
98 * Inflate a menu hierarchy from the specified XML resource. Throws
99 * {@link InflateException} if there is an error.
101 * @param menuRes Resource ID for an XML layout resource to load (e.g.,
102 * <code>R.menu.main_activity</code>)
103 * @param menu The Menu to inflate into. The items and submenus will be
104 * added to this Menu.
106 public void inflate(int menuRes
, Menu menu
) {
107 XmlResourceParser parser
= null
;
109 parser
= mContext
.getResources().getLayout(menuRes
);
110 AttributeSet attrs
= Xml
.asAttributeSet(parser
);
112 parseMenu(parser
, attrs
, menu
);
113 } catch (XmlPullParserException e
) {
114 throw new InflateException("Error inflating menu XML", e
);
115 } catch (IOException e
) {
116 throw new InflateException("Error inflating menu XML", e
);
118 if (parser
!= null
) parser
.close();
123 * Called internally to fill the given menu. If a sub menu is seen, it will
124 * call this recursively.
126 private void parseMenu(XmlPullParser parser
, AttributeSet attrs
, Menu menu
)
127 throws XmlPullParserException
, IOException
{
128 MenuState menuState
= new MenuState(menu
);
130 int eventType
= parser
.getEventType();
132 boolean lookingForEndOfUnknownTag
= false
;
133 String unknownTagName
= null
;
135 // This loop will skip to the menu start tag
137 if (eventType
== XmlPullParser
.START_TAG
) {
138 tagName
= parser
.getName();
139 if (tagName
.equals(XML_MENU
)) {
141 eventType
= parser
.next();
145 throw new RuntimeException("Expecting menu, got " + tagName
);
147 eventType
= parser
.next();
148 } while (eventType
!= XmlPullParser
.END_DOCUMENT
);
150 boolean reachedEndOfMenu
= false
;
151 while (!reachedEndOfMenu
) {
153 case XmlPullParser
.START_TAG
:
154 if (lookingForEndOfUnknownTag
) {
158 tagName
= parser
.getName();
159 if (tagName
.equals(XML_GROUP
)) {
160 menuState
.readGroup(attrs
);
161 } else if (tagName
.equals(XML_ITEM
)) {
162 menuState
.readItem(attrs
);
163 } else if (tagName
.equals(XML_MENU
)) {
164 // A menu start tag denotes a submenu for an item
165 SubMenu subMenu
= menuState
.addSubMenuItem();
167 // Parse the submenu into returned SubMenu
168 parseMenu(parser
, attrs
, subMenu
);
170 lookingForEndOfUnknownTag
= true
;
171 unknownTagName
= tagName
;
175 case XmlPullParser
.END_TAG
:
176 tagName
= parser
.getName();
177 if (lookingForEndOfUnknownTag
&& tagName
.equals(unknownTagName
)) {
178 lookingForEndOfUnknownTag
= false
;
179 unknownTagName
= null
;
180 } else if (tagName
.equals(XML_GROUP
)) {
181 menuState
.resetGroup();
182 } else if (tagName
.equals(XML_ITEM
)) {
183 // Add the item if it hasn't been added (if the item was
184 // a submenu, it would have been added already)
185 if (!menuState
.hasAddedItem()) {
186 if (menuState
.itemActionProvider
!= null
&&
187 menuState
.itemActionProvider
.hasSubMenu()) {
188 menuState
.addSubMenuItem();
193 } else if (tagName
.equals(XML_MENU
)) {
194 reachedEndOfMenu
= true
;
198 case XmlPullParser
.END_DOCUMENT
:
199 throw new RuntimeException("Unexpected end of document");
202 eventType
= parser
.next();
206 private static class InflatedOnMenuItemClickListener
207 implements MenuItem
.OnMenuItemClickListener
{
208 private static final Class
<?
>[] PARAM_TYPES
= new Class
[] { MenuItem
.class };
210 private Object mRealOwner
;
211 private Method mMethod
;
213 public InflatedOnMenuItemClickListener(Object realOwner
, String methodName
) {
214 mRealOwner
= realOwner
;
215 Class
<?
> c
= realOwner
.getClass();
217 mMethod
= c
.getMethod(methodName
, PARAM_TYPES
);
218 } catch (Exception e
) {
219 InflateException ex
= new InflateException(
220 "Couldn't resolve menu item onClick handler " + methodName
+
221 " in class " + c
.getName());
227 public boolean onMenuItemClick(MenuItem item
) {
229 if (mMethod
.getReturnType() == Boolean
.TYPE
) {
230 return (Boolean
) mMethod
.invoke(mRealOwner
, item
);
232 mMethod
.invoke(mRealOwner
, item
);
235 } catch (Exception e
) {
236 throw new RuntimeException(e
);
242 * State for the current menu.
244 * Groups can not be nested unless there is another menu (which will have
247 private class MenuState
{
251 * Group state is set on items as they are added, allowing an item to
252 * override its group state. (As opposed to set on items at the group end tag.)
255 private int groupCategory
;
256 private int groupOrder
;
257 private int groupCheckable
;
258 private boolean groupVisible
;
259 private boolean groupEnabled
;
261 private boolean itemAdded
;
263 private int itemCategoryOrder
;
264 private CharSequence itemTitle
;
265 private CharSequence itemTitleCondensed
;
266 private int itemIconResId
;
267 private char itemAlphabeticShortcut
;
268 private char itemNumericShortcut
;
270 * Sync to attrs.xml enum:
275 private int itemCheckable
;
276 private boolean itemChecked
;
277 private boolean itemVisible
;
278 private boolean itemEnabled
;
281 * Sync to attrs.xml enum, values in MenuItem:
285 * - -1: Safe sentinel for "no value".
287 private int itemShowAsAction
;
289 private int itemActionViewLayout
;
290 private String itemActionViewClassName
;
291 private String itemActionProviderClassName
;
293 private String itemListenerMethodName
;
295 private ActionProvider itemActionProvider
;
297 private static final int defaultGroupId
= NO_ID
;
298 private static final int defaultItemId
= NO_ID
;
299 private static final int defaultItemCategory
= 0;
300 private static final int defaultItemOrder
= 0;
301 private static final int defaultItemCheckable
= 0;
302 private static final boolean defaultItemChecked
= false
;
303 private static final boolean defaultItemVisible
= true
;
304 private static final boolean defaultItemEnabled
= true
;
306 public MenuState(final Menu menu
) {
312 public void resetGroup() {
313 groupId
= defaultGroupId
;
314 groupCategory
= defaultItemCategory
;
315 groupOrder
= defaultItemOrder
;
316 groupCheckable
= defaultItemCheckable
;
317 groupVisible
= defaultItemVisible
;
318 groupEnabled
= defaultItemEnabled
;
322 * Called when the parser is pointing to a group tag.
324 public void readGroup(AttributeSet attrs
) {
325 TypedArray a
= mContext
.obtainStyledAttributes(attrs
,
326 R
.styleable
.SherlockMenuGroup
);
328 groupId
= a
.getResourceId(R
.styleable
.SherlockMenuGroup_android_id
, defaultGroupId
);
329 groupCategory
= a
.getInt(R
.styleable
.SherlockMenuGroup_android_menuCategory
, defaultItemCategory
);
330 groupOrder
= a
.getInt(R
.styleable
.SherlockMenuGroup_android_orderInCategory
, defaultItemOrder
);
331 groupCheckable
= a
.getInt(R
.styleable
.SherlockMenuGroup_android_checkableBehavior
, defaultItemCheckable
);
332 groupVisible
= a
.getBoolean(R
.styleable
.SherlockMenuGroup_android_visible
, defaultItemVisible
);
333 groupEnabled
= a
.getBoolean(R
.styleable
.SherlockMenuGroup_android_enabled
, defaultItemEnabled
);
339 * Called when the parser is pointing to an item tag.
341 public void readItem(AttributeSet attrs
) {
342 TypedArray a
= mContext
.obtainStyledAttributes(attrs
,
343 R
.styleable
.SherlockMenuItem
);
345 // Inherit attributes from the group as default value
346 itemId
= a
.getResourceId(R
.styleable
.SherlockMenuItem_android_id
, defaultItemId
);
347 final int category
= a
.getInt(R
.styleable
.SherlockMenuItem_android_menuCategory
, groupCategory
);
348 final int order
= a
.getInt(R
.styleable
.SherlockMenuItem_android_orderInCategory
, groupOrder
);
349 itemCategoryOrder
= (category
& Menu
.CATEGORY_MASK
) | (order
& Menu
.USER_MASK
);
350 itemTitle
= a
.getText(R
.styleable
.SherlockMenuItem_android_title
);
351 itemTitleCondensed
= a
.getText(R
.styleable
.SherlockMenuItem_android_titleCondensed
);
352 itemIconResId
= a
.getResourceId(R
.styleable
.SherlockMenuItem_android_icon
, 0);
353 itemAlphabeticShortcut
=
354 getShortcut(a
.getString(R
.styleable
.SherlockMenuItem_android_alphabeticShortcut
));
355 itemNumericShortcut
=
356 getShortcut(a
.getString(R
.styleable
.SherlockMenuItem_android_numericShortcut
));
357 if (a
.hasValue(R
.styleable
.SherlockMenuItem_android_checkable
)) {
358 // Item has attribute checkable, use it
359 itemCheckable
= a
.getBoolean(R
.styleable
.SherlockMenuItem_android_checkable
, false
) ?
1 : 0;
361 // Item does not have attribute, use the group's (group can have one more state
362 // for checkable that represents the exclusive checkable)
363 itemCheckable
= groupCheckable
;
366 itemChecked
= a
.getBoolean(R
.styleable
.SherlockMenuItem_android_checked
, defaultItemChecked
);
367 itemVisible
= a
.getBoolean(R
.styleable
.SherlockMenuItem_android_visible
, groupVisible
);
368 itemEnabled
= a
.getBoolean(R
.styleable
.SherlockMenuItem_android_enabled
, groupEnabled
);
370 TypedValue value
= new TypedValue();
371 a
.getValue(R
.styleable
.SherlockMenuItem_android_showAsAction
, value
);
372 itemShowAsAction
= value
.type
== TypedValue
.TYPE_INT_HEX ? value
.data
: -1;
374 itemListenerMethodName
= a
.getString(R
.styleable
.SherlockMenuItem_android_onClick
);
375 itemActionViewLayout
= a
.getResourceId(R
.styleable
.SherlockMenuItem_android_actionLayout
, 0);
377 // itemActionViewClassName = a.getString(R.styleable.SherlockMenuItem_android_actionViewClass);
378 value
= new TypedValue();
379 a
.getValue(R
.styleable
.SherlockMenuItem_android_actionViewClass
, value
);
380 itemActionViewClassName
= value
.type
== TypedValue
.TYPE_STRING ? value
.string
.toString() : null
;
382 // itemActionProviderClassName = a.getString(R.styleable.SherlockMenuItem_android_actionProviderClass);
383 value
= new TypedValue();
384 a
.getValue(R
.styleable
.SherlockMenuItem_android_actionProviderClass
, value
);
385 itemActionProviderClassName
= value
.type
== TypedValue
.TYPE_STRING ? value
.string
.toString() : null
;
387 final boolean hasActionProvider
= itemActionProviderClassName
!= null
;
388 if (hasActionProvider
&& itemActionViewLayout
== 0 && itemActionViewClassName
== null
) {
389 itemActionProvider
= newInstance(itemActionProviderClassName
,
390 ACTION_PROVIDER_CONSTRUCTOR_SIGNATURE
,
391 mActionProviderConstructorArguments
);
393 if (hasActionProvider
) {
394 Log
.w(LOG_TAG
, "Ignoring attribute 'actionProviderClass'."
395 + " Action view already specified.");
397 itemActionProvider
= null
;
405 private char getShortcut(String shortcutString
) {
406 if (shortcutString
== null
) {
409 return shortcutString
.charAt(0);
413 private void setItem(MenuItem item
) {
414 item
.setChecked(itemChecked
)
415 .setVisible(itemVisible
)
416 .setEnabled(itemEnabled
)
417 .setCheckable(itemCheckable
>= 1)
418 .setTitleCondensed(itemTitleCondensed
)
419 .setIcon(itemIconResId
)
420 .setAlphabeticShortcut(itemAlphabeticShortcut
)
421 .setNumericShortcut(itemNumericShortcut
);
423 if (itemShowAsAction
>= 0) {
424 item
.setShowAsAction(itemShowAsAction
);
427 if (itemListenerMethodName
!= null
) {
428 if (mContext
.isRestricted()) {
429 throw new IllegalStateException("The android:onClick attribute cannot "
430 + "be used within a restricted context");
432 item
.setOnMenuItemClickListener(
433 new InflatedOnMenuItemClickListener(mRealOwner
, itemListenerMethodName
));
436 if (itemCheckable
>= 2) {
437 if (item
instanceof MenuItemImpl
) {
438 MenuItemImpl impl
= (MenuItemImpl
) item
;
439 impl
.setExclusiveCheckable(true
);
441 menu
.setGroupCheckable(groupId
, true
, true
);
445 boolean actionViewSpecified
= false
;
446 if (itemActionViewClassName
!= null
) {
447 View actionView
= (View
) newInstance(itemActionViewClassName
,
448 ACTION_VIEW_CONSTRUCTOR_SIGNATURE
, mActionViewConstructorArguments
);
449 item
.setActionView(actionView
);
450 actionViewSpecified
= true
;
452 if (itemActionViewLayout
> 0) {
453 if (!actionViewSpecified
) {
454 item
.setActionView(itemActionViewLayout
);
455 actionViewSpecified
= true
;
457 Log
.w(LOG_TAG
, "Ignoring attribute 'itemActionViewLayout'."
458 + " Action view already specified.");
461 if (itemActionProvider
!= null
) {
462 item
.setActionProvider(itemActionProvider
);
466 public void addItem() {
468 setItem(menu
.add(groupId
, itemId
, itemCategoryOrder
, itemTitle
));
471 public SubMenu
addSubMenuItem() {
473 SubMenu subMenu
= menu
.addSubMenu(groupId
, itemId
, itemCategoryOrder
, itemTitle
);
474 setItem(subMenu
.getItem());
478 public boolean hasAddedItem() {
482 @SuppressWarnings("unchecked")
483 private <T
> T
newInstance(String className
, Class
<?
>[] constructorSignature
,
484 Object
[] arguments
) {
486 Class
<?
> clazz
= mContext
.getClassLoader().loadClass(className
);
487 Constructor
<?
> constructor
= clazz
.getConstructor(constructorSignature
);
488 return (T
) constructor
.newInstance(arguments
);
489 } catch (Exception e
) {
490 Log
.w(LOG_TAG
, "Cannot instantiate class: " + className
, e
);