MenuInflater.java revision 89e0645b4157961e8c465eb9c819f965fdb453d8
1/* 2 * Copyright (C) 2006 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.view; 18 19import java.io.IOException; 20import java.lang.reflect.Method; 21 22import org.xmlpull.v1.XmlPullParser; 23import org.xmlpull.v1.XmlPullParserException; 24 25import android.app.Activity; 26import android.content.Context; 27import android.content.res.TypedArray; 28import android.content.res.XmlResourceParser; 29import android.util.AttributeSet; 30import android.util.Xml; 31 32import com.android.internal.view.menu.MenuItemImpl; 33 34/** 35 * This class is used to instantiate menu XML files into Menu objects. 36 * <p> 37 * For performance reasons, menu inflation relies heavily on pre-processing of 38 * XML files that is done at build time. Therefore, it is not currently possible 39 * to use MenuInflater with an XmlPullParser over a plain XML file at runtime; 40 * it only works with an XmlPullParser returned from a compiled resource (R. 41 * <em>something</em> file.) 42 */ 43public class MenuInflater { 44 /** Menu tag name in XML. */ 45 private static final String XML_MENU = "menu"; 46 47 /** Group tag name in XML. */ 48 private static final String XML_GROUP = "group"; 49 50 /** Item tag name in XML. */ 51 private static final String XML_ITEM = "item"; 52 53 private static final int NO_ID = 0; 54 55 private Context mContext; 56 57 /** 58 * Constructs a menu inflater. 59 * 60 * @see Activity#getMenuInflater() 61 */ 62 public MenuInflater(Context context) { 63 mContext = context; 64 } 65 66 /** 67 * Inflate a menu hierarchy from the specified XML resource. Throws 68 * {@link InflateException} if there is an error. 69 * 70 * @param menuRes Resource ID for an XML layout resource to load (e.g., 71 * <code>R.menu.main_activity</code>) 72 * @param menu The Menu to inflate into. The items and submenus will be 73 * added to this Menu. 74 */ 75 public void inflate(int menuRes, Menu menu) { 76 XmlResourceParser parser = null; 77 try { 78 parser = mContext.getResources().getLayout(menuRes); 79 AttributeSet attrs = Xml.asAttributeSet(parser); 80 81 parseMenu(parser, attrs, menu); 82 } catch (XmlPullParserException e) { 83 throw new InflateException("Error inflating menu XML", e); 84 } catch (IOException e) { 85 throw new InflateException("Error inflating menu XML", e); 86 } finally { 87 if (parser != null) parser.close(); 88 } 89 } 90 91 /** 92 * Called internally to fill the given menu. If a sub menu is seen, it will 93 * call this recursively. 94 */ 95 private void parseMenu(XmlPullParser parser, AttributeSet attrs, Menu menu) 96 throws XmlPullParserException, IOException { 97 MenuState menuState = new MenuState(menu); 98 99 int eventType = parser.getEventType(); 100 String tagName; 101 boolean lookingForEndOfUnknownTag = false; 102 String unknownTagName = null; 103 104 // This loop will skip to the menu start tag 105 do { 106 if (eventType == XmlPullParser.START_TAG) { 107 tagName = parser.getName(); 108 if (tagName.equals(XML_MENU)) { 109 // Go to next tag 110 eventType = parser.next(); 111 break; 112 } 113 114 throw new RuntimeException("Expecting menu, got " + tagName); 115 } 116 eventType = parser.next(); 117 } while (eventType != XmlPullParser.END_DOCUMENT); 118 119 boolean reachedEndOfMenu = false; 120 while (!reachedEndOfMenu) { 121 switch (eventType) { 122 case XmlPullParser.START_TAG: 123 if (lookingForEndOfUnknownTag) { 124 break; 125 } 126 127 tagName = parser.getName(); 128 if (tagName.equals(XML_GROUP)) { 129 menuState.readGroup(attrs); 130 } else if (tagName.equals(XML_ITEM)) { 131 menuState.readItem(attrs); 132 } else if (tagName.equals(XML_MENU)) { 133 // A menu start tag denotes a submenu for an item 134 SubMenu subMenu = menuState.addSubMenuItem(); 135 136 // Parse the submenu into returned SubMenu 137 parseMenu(parser, attrs, subMenu); 138 } else { 139 lookingForEndOfUnknownTag = true; 140 unknownTagName = tagName; 141 } 142 break; 143 144 case XmlPullParser.END_TAG: 145 tagName = parser.getName(); 146 if (lookingForEndOfUnknownTag && tagName.equals(unknownTagName)) { 147 lookingForEndOfUnknownTag = false; 148 unknownTagName = null; 149 } else if (tagName.equals(XML_GROUP)) { 150 menuState.resetGroup(); 151 } else if (tagName.equals(XML_ITEM)) { 152 // Add the item if it hasn't been added (if the item was 153 // a submenu, it would have been added already) 154 if (!menuState.hasAddedItem()) { 155 menuState.addItem(); 156 } 157 } else if (tagName.equals(XML_MENU)) { 158 reachedEndOfMenu = true; 159 } 160 break; 161 162 case XmlPullParser.END_DOCUMENT: 163 throw new RuntimeException("Unexpected end of document"); 164 } 165 166 eventType = parser.next(); 167 } 168 } 169 170 private static class InflatedOnMenuItemClickListener 171 implements MenuItem.OnMenuItemClickListener { 172 private static final Class[] PARAM_TYPES = new Class[] { MenuItem.class }; 173 174 private Context mContext; 175 private Method mMethod; 176 177 public InflatedOnMenuItemClickListener(Context context, String methodName) { 178 mContext = context; 179 Class c = context.getClass(); 180 try { 181 mMethod = c.getMethod(methodName, PARAM_TYPES); 182 } catch (Exception e) { 183 InflateException ex = new InflateException( 184 "Couldn't resolve menu item onClick handler " + methodName + 185 " in class " + c.getName()); 186 ex.initCause(e); 187 throw ex; 188 } 189 } 190 191 public boolean onMenuItemClick(MenuItem item) { 192 try { 193 if (mMethod.getReturnType() == Boolean.TYPE) { 194 return (Boolean) mMethod.invoke(mContext, item); 195 } else { 196 mMethod.invoke(mContext, item); 197 return true; 198 } 199 } catch (Exception e) { 200 throw new RuntimeException(e); 201 } 202 } 203 } 204 205 /** 206 * State for the current menu. 207 * <p> 208 * Groups can not be nested unless there is another menu (which will have 209 * its state class). 210 */ 211 private class MenuState { 212 private Menu menu; 213 214 /* 215 * Group state is set on items as they are added, allowing an item to 216 * override its group state. (As opposed to set on items at the group end tag.) 217 */ 218 private int groupId; 219 private int groupCategory; 220 private int groupOrder; 221 private int groupCheckable; 222 private boolean groupVisible; 223 private boolean groupEnabled; 224 225 private boolean itemAdded; 226 private int itemId; 227 private int itemCategoryOrder; 228 private String itemTitle; 229 private String itemTitleCondensed; 230 private int itemIconResId; 231 private char itemAlphabeticShortcut; 232 private char itemNumericShortcut; 233 /** 234 * Sync to attrs.xml enum: 235 * - 0: none 236 * - 1: all 237 * - 2: exclusive 238 */ 239 private int itemCheckable; 240 private boolean itemChecked; 241 private boolean itemVisible; 242 private boolean itemEnabled; 243 244 /** 245 * Sync to attrs.xml enum, values in MenuItem: 246 * - 0: never 247 * - 1: ifRoom 248 * - 2: always 249 */ 250 private int itemShowAsAction = MenuItem.SHOW_AS_ACTION_NEVER; 251 252 private String itemListenerMethodName; 253 254 private static final int defaultGroupId = NO_ID; 255 private static final int defaultItemId = NO_ID; 256 private static final int defaultItemCategory = 0; 257 private static final int defaultItemOrder = 0; 258 private static final int defaultItemCheckable = 0; 259 private static final boolean defaultItemChecked = false; 260 private static final boolean defaultItemVisible = true; 261 private static final boolean defaultItemEnabled = true; 262 263 public MenuState(final Menu menu) { 264 this.menu = menu; 265 266 resetGroup(); 267 } 268 269 public void resetGroup() { 270 groupId = defaultGroupId; 271 groupCategory = defaultItemCategory; 272 groupOrder = defaultItemOrder; 273 groupCheckable = defaultItemCheckable; 274 groupVisible = defaultItemVisible; 275 groupEnabled = defaultItemEnabled; 276 } 277 278 /** 279 * Called when the parser is pointing to a group tag. 280 */ 281 public void readGroup(AttributeSet attrs) { 282 TypedArray a = mContext.obtainStyledAttributes(attrs, 283 com.android.internal.R.styleable.MenuGroup); 284 285 groupId = a.getResourceId(com.android.internal.R.styleable.MenuGroup_id, defaultGroupId); 286 groupCategory = a.getInt(com.android.internal.R.styleable.MenuGroup_menuCategory, defaultItemCategory); 287 groupOrder = a.getInt(com.android.internal.R.styleable.MenuGroup_orderInCategory, defaultItemOrder); 288 groupCheckable = a.getInt(com.android.internal.R.styleable.MenuGroup_checkableBehavior, defaultItemCheckable); 289 groupVisible = a.getBoolean(com.android.internal.R.styleable.MenuGroup_visible, defaultItemVisible); 290 groupEnabled = a.getBoolean(com.android.internal.R.styleable.MenuGroup_enabled, defaultItemEnabled); 291 292 a.recycle(); 293 } 294 295 /** 296 * Called when the parser is pointing to an item tag. 297 */ 298 public void readItem(AttributeSet attrs) { 299 TypedArray a = mContext.obtainStyledAttributes(attrs, 300 com.android.internal.R.styleable.MenuItem); 301 302 // Inherit attributes from the group as default value 303 itemId = a.getResourceId(com.android.internal.R.styleable.MenuItem_id, defaultItemId); 304 final int category = a.getInt(com.android.internal.R.styleable.MenuItem_menuCategory, groupCategory); 305 final int order = a.getInt(com.android.internal.R.styleable.MenuItem_orderInCategory, groupOrder); 306 itemCategoryOrder = (category & Menu.CATEGORY_MASK) | (order & Menu.USER_MASK); 307 itemTitle = a.getString(com.android.internal.R.styleable.MenuItem_title); 308 itemTitleCondensed = a.getString(com.android.internal.R.styleable.MenuItem_titleCondensed); 309 itemIconResId = a.getResourceId(com.android.internal.R.styleable.MenuItem_icon, 0); 310 itemAlphabeticShortcut = 311 getShortcut(a.getString(com.android.internal.R.styleable.MenuItem_alphabeticShortcut)); 312 itemNumericShortcut = 313 getShortcut(a.getString(com.android.internal.R.styleable.MenuItem_numericShortcut)); 314 if (a.hasValue(com.android.internal.R.styleable.MenuItem_checkable)) { 315 // Item has attribute checkable, use it 316 itemCheckable = a.getBoolean(com.android.internal.R.styleable.MenuItem_checkable, false) ? 1 : 0; 317 } else { 318 // Item does not have attribute, use the group's (group can have one more state 319 // for checkable that represents the exclusive checkable) 320 itemCheckable = groupCheckable; 321 } 322 itemChecked = a.getBoolean(com.android.internal.R.styleable.MenuItem_checked, defaultItemChecked); 323 itemVisible = a.getBoolean(com.android.internal.R.styleable.MenuItem_visible, groupVisible); 324 itemEnabled = a.getBoolean(com.android.internal.R.styleable.MenuItem_enabled, groupEnabled); 325 itemShowAsAction = a.getInt(com.android.internal.R.styleable.MenuItem_showAsAction, 0); 326 itemListenerMethodName = a.getString(com.android.internal.R.styleable.MenuItem_onClick); 327 328 a.recycle(); 329 330 itemAdded = false; 331 } 332 333 private char getShortcut(String shortcutString) { 334 if (shortcutString == null) { 335 return 0; 336 } else { 337 return shortcutString.charAt(0); 338 } 339 } 340 341 private void setItem(MenuItem item) { 342 item.setChecked(itemChecked) 343 .setVisible(itemVisible) 344 .setEnabled(itemEnabled) 345 .setCheckable(itemCheckable >= 1) 346 .setTitleCondensed(itemTitleCondensed) 347 .setIcon(itemIconResId) 348 .setAlphabeticShortcut(itemAlphabeticShortcut) 349 .setNumericShortcut(itemNumericShortcut) 350 .setShowAsAction(itemShowAsAction); 351 352 if (itemListenerMethodName != null) { 353 item.setOnMenuItemClickListener( 354 new InflatedOnMenuItemClickListener(mContext, itemListenerMethodName)); 355 } 356 357 if (item instanceof MenuItemImpl) { 358 MenuItemImpl impl = (MenuItemImpl) item; 359 if (itemCheckable >= 2) { 360 impl.setExclusiveCheckable(true); 361 } 362 } 363 } 364 365 public void addItem() { 366 itemAdded = true; 367 setItem(menu.add(groupId, itemId, itemCategoryOrder, itemTitle)); 368 } 369 370 public SubMenu addSubMenuItem() { 371 itemAdded = true; 372 SubMenu subMenu = menu.addSubMenu(groupId, itemId, itemCategoryOrder, itemTitle); 373 setItem(subMenu.getItem()); 374 return subMenu; 375 } 376 377 public boolean hasAddedItem() { 378 return itemAdded; 379 } 380 } 381 382} 383