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