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