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