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