ActionMenuPresenter.java revision bfcdfaf919cdb67897a6e24afc8f14b2c810596a
1/*
2 * Copyright (C) 2011 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 com.android.internal.view.menu;
18
19import com.android.internal.view.menu.ActionMenuView.ActionMenuChildView;
20
21import android.content.Context;
22import android.content.res.Configuration;
23import android.content.res.Resources;
24import android.os.Parcel;
25import android.os.Parcelable;
26import android.util.SparseBooleanArray;
27import android.view.MenuItem;
28import android.view.SoundEffectConstants;
29import android.view.View;
30import android.view.View.MeasureSpec;
31import android.view.ViewConfiguration;
32import android.view.ViewGroup;
33import android.widget.ImageButton;
34
35import java.util.ArrayList;
36
37/**
38 * MenuPresenter for building action menus as seen in the action bar and action modes.
39 */
40public class ActionMenuPresenter extends BaseMenuPresenter {
41    private static final String TAG = "ActionMenuPresenter";
42
43    private View mOverflowButton;
44    private boolean mReserveOverflow;
45    private boolean mReserveOverflowSet;
46    private int mWidthLimit;
47    private int mActionItemWidthLimit;
48    private int mMaxItems;
49    private boolean mMaxItemsSet;
50    private boolean mStrictWidthLimit;
51    private boolean mWidthLimitSet;
52    private boolean mExpandedActionViewsExclusive;
53
54    private int mMinCellSize;
55
56    // Group IDs that have been added as actions - used temporarily, allocated here for reuse.
57    private final SparseBooleanArray mActionButtonGroups = new SparseBooleanArray();
58
59    private View mScrapActionButtonView;
60
61    private OverflowPopup mOverflowPopup;
62    private ActionButtonSubmenu mActionButtonPopup;
63
64    private OpenOverflowRunnable mPostedOpenRunnable;
65
66    final PopupPresenterCallback mPopupPresenterCallback = new PopupPresenterCallback();
67    int mOpenSubMenuId;
68
69    public ActionMenuPresenter() {
70        super(com.android.internal.R.layout.action_menu_layout,
71                com.android.internal.R.layout.action_menu_item_layout);
72    }
73
74    @Override
75    public void initForMenu(Context context, MenuBuilder menu) {
76        super.initForMenu(context, menu);
77
78        final Resources res = context.getResources();
79
80        if (!mReserveOverflowSet) {
81            mReserveOverflow = !ViewConfiguration.get(context).hasPermanentMenuKey();
82        }
83
84        if (!mWidthLimitSet) {
85            mWidthLimit = res.getDisplayMetrics().widthPixels / 2;
86        }
87
88        // Measure for initial configuration
89        if (!mMaxItemsSet) {
90            mMaxItems = res.getInteger(com.android.internal.R.integer.max_action_buttons);
91        }
92
93        int width = mWidthLimit;
94        if (mReserveOverflow) {
95            if (mOverflowButton == null) {
96                mOverflowButton = new OverflowMenuButton(mContext);
97                final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
98                mOverflowButton.measure(spec, spec);
99            }
100            width -= mOverflowButton.getMeasuredWidth();
101        } else {
102            mOverflowButton = null;
103        }
104
105        mActionItemWidthLimit = width;
106
107        mMinCellSize = (int) (ActionMenuView.MIN_CELL_SIZE * res.getDisplayMetrics().density);
108
109        // Drop a scrap view as it may no longer reflect the proper context/config.
110        mScrapActionButtonView = null;
111    }
112
113    public void onConfigurationChanged(Configuration newConfig) {
114        if (!mMaxItemsSet) {
115            mMaxItems = mContext.getResources().getInteger(
116                    com.android.internal.R.integer.max_action_buttons);
117            if (mMenu != null) {
118                mMenu.onItemsChanged(true);
119            }
120        }
121    }
122
123    public void setWidthLimit(int width, boolean strict) {
124        mWidthLimit = width;
125        mStrictWidthLimit = strict;
126        mWidthLimitSet = true;
127    }
128
129    public void setReserveOverflow(boolean reserveOverflow) {
130        mReserveOverflow = reserveOverflow;
131        mReserveOverflowSet = true;
132    }
133
134    public void setItemLimit(int itemCount) {
135        mMaxItems = itemCount;
136        mMaxItemsSet = true;
137    }
138
139    public void setExpandedActionViewsExclusive(boolean isExclusive) {
140        mExpandedActionViewsExclusive = isExclusive;
141    }
142
143    @Override
144    public MenuView getMenuView(ViewGroup root) {
145        MenuView result = super.getMenuView(root);
146        ((ActionMenuView) result).setPresenter(this);
147        return result;
148    }
149
150    @Override
151    public View getItemView(MenuItemImpl item, View convertView, ViewGroup parent) {
152        View actionView = item.getActionView();
153        if (actionView == null || item.hasCollapsibleActionView()) {
154            if (!(convertView instanceof ActionMenuItemView)) {
155                convertView = null;
156            }
157            actionView = super.getItemView(item, convertView, parent);
158        }
159        actionView.setVisibility(item.isActionViewExpanded() ? View.GONE : View.VISIBLE);
160
161        final ActionMenuView menuParent = (ActionMenuView) parent;
162        final ViewGroup.LayoutParams lp = actionView.getLayoutParams();
163        if (!menuParent.checkLayoutParams(lp)) {
164            actionView.setLayoutParams(menuParent.generateLayoutParams(lp));
165        }
166        return actionView;
167    }
168
169    @Override
170    public void bindItemView(MenuItemImpl item, MenuView.ItemView itemView) {
171        itemView.initialize(item, 0);
172
173        final ActionMenuView menuView = (ActionMenuView) mMenuView;
174        ActionMenuItemView actionItemView = (ActionMenuItemView) itemView;
175        actionItemView.setItemInvoker(menuView);
176    }
177
178    @Override
179    public boolean shouldIncludeItem(int childIndex, MenuItemImpl item) {
180        return item.isActionButton();
181    }
182
183    @Override
184    public void updateMenuView(boolean cleared) {
185        super.updateMenuView(cleared);
186
187        final boolean hasOverflow = mReserveOverflow && mMenu != null &&
188                mMenu.getNonActionItems().size() > 0;
189        if (hasOverflow) {
190            if (mOverflowButton == null) {
191                mOverflowButton = new OverflowMenuButton(mContext);
192            }
193            ViewGroup parent = (ViewGroup) mOverflowButton.getParent();
194            if (parent != mMenuView) {
195                if (parent != null) {
196                    parent.removeView(mOverflowButton);
197                }
198                ActionMenuView menuView = (ActionMenuView) mMenuView;
199                menuView.addView(mOverflowButton, menuView.generateOverflowButtonLayoutParams());
200            }
201        } else if (mOverflowButton != null && mOverflowButton.getParent() == mMenuView) {
202            ((ViewGroup) mMenuView).removeView(mOverflowButton);
203        }
204
205        ((ActionMenuView) mMenuView).setOverflowReserved(mReserveOverflow);
206    }
207
208    @Override
209    public boolean filterLeftoverView(ViewGroup parent, int childIndex) {
210        if (parent.getChildAt(childIndex) == mOverflowButton) return false;
211        return super.filterLeftoverView(parent, childIndex);
212    }
213
214    public boolean onSubMenuSelected(SubMenuBuilder subMenu) {
215        if (!subMenu.hasVisibleItems()) return false;
216
217        SubMenuBuilder topSubMenu = subMenu;
218        while (topSubMenu.getParentMenu() != mMenu) {
219            topSubMenu = (SubMenuBuilder) topSubMenu.getParentMenu();
220        }
221        View anchor = findViewForItem(topSubMenu.getItem());
222        if (anchor == null) {
223            if (mOverflowButton == null) return false;
224            anchor = mOverflowButton;
225        }
226
227        mOpenSubMenuId = subMenu.getItem().getItemId();
228        mActionButtonPopup = new ActionButtonSubmenu(mContext, subMenu);
229        mActionButtonPopup.setAnchorView(anchor);
230        mActionButtonPopup.show();
231        super.onSubMenuSelected(subMenu);
232        return true;
233    }
234
235    private View findViewForItem(MenuItem item) {
236        final ViewGroup parent = (ViewGroup) mMenuView;
237        if (parent == null) return null;
238
239        final int count = parent.getChildCount();
240        for (int i = 0; i < count; i++) {
241            final View child = parent.getChildAt(i);
242            if (child instanceof MenuView.ItemView &&
243                    ((MenuView.ItemView) child).getItemData() == item) {
244                return child;
245            }
246        }
247        return null;
248    }
249
250    /**
251     * Display the overflow menu if one is present.
252     * @return true if the overflow menu was shown, false otherwise.
253     */
254    public boolean showOverflowMenu() {
255        if (mReserveOverflow && !isOverflowMenuShowing() && mMenuView != null &&
256                mPostedOpenRunnable == null) {
257            OverflowPopup popup = new OverflowPopup(mContext, mMenu, mOverflowButton, true);
258            mPostedOpenRunnable = new OpenOverflowRunnable(popup);
259            // Post this for later; we might still need a layout for the anchor to be right.
260            ((View) mMenuView).post(mPostedOpenRunnable);
261
262            // ActionMenuPresenter uses null as a callback argument here
263            // to indicate overflow is opening.
264            super.onSubMenuSelected(null);
265
266            return true;
267        }
268        return false;
269    }
270
271    /**
272     * Hide the overflow menu if it is currently showing.
273     *
274     * @return true if the overflow menu was hidden, false otherwise.
275     */
276    public boolean hideOverflowMenu() {
277        if (mPostedOpenRunnable != null && mMenuView != null) {
278            ((View) mMenuView).removeCallbacks(mPostedOpenRunnable);
279            return true;
280        }
281
282        MenuPopupHelper popup = mOverflowPopup;
283        if (popup != null) {
284            popup.dismiss();
285            return true;
286        }
287        return false;
288    }
289
290    /**
291     * Dismiss all popup menus - overflow and submenus.
292     * @return true if popups were dismissed, false otherwise. (This can be because none were open.)
293     */
294    public boolean dismissPopupMenus() {
295        boolean result = hideOverflowMenu();
296        result |= hideSubMenus();
297        return result;
298    }
299
300    /**
301     * Dismiss all submenu popups.
302     *
303     * @return true if popups were dismissed, false otherwise. (This can be because none were open.)
304     */
305    public boolean hideSubMenus() {
306        if (mActionButtonPopup != null) {
307            mActionButtonPopup.dismiss();
308            return true;
309        }
310        return false;
311    }
312
313    /**
314     * @return true if the overflow menu is currently showing
315     */
316    public boolean isOverflowMenuShowing() {
317        return mOverflowPopup != null && mOverflowPopup.isShowing();
318    }
319
320    /**
321     * @return true if space has been reserved in the action menu for an overflow item.
322     */
323    public boolean isOverflowReserved() {
324        return mReserveOverflow;
325    }
326
327    public boolean flagActionItems() {
328        final ArrayList<MenuItemImpl> visibleItems = mMenu.getVisibleItems();
329        final int itemsSize = visibleItems.size();
330        int maxActions = mMaxItems;
331        int widthLimit = mActionItemWidthLimit;
332        final int querySpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
333        final ViewGroup parent = (ViewGroup) mMenuView;
334
335        int requiredItems = 0;
336        int requestedItems = 0;
337        int firstActionWidth = 0;
338        boolean hasOverflow = false;
339        for (int i = 0; i < itemsSize; i++) {
340            MenuItemImpl item = visibleItems.get(i);
341            if (item.requiresActionButton()) {
342                requiredItems++;
343            } else if (item.requestsActionButton()) {
344                requestedItems++;
345            } else {
346                hasOverflow = true;
347            }
348            if (mExpandedActionViewsExclusive && item.isActionViewExpanded()) {
349                // Overflow everything if we have an expanded action view and we're
350                // space constrained.
351                maxActions = 0;
352            }
353        }
354
355        // Reserve a spot for the overflow item if needed.
356        if (mReserveOverflow &&
357                (hasOverflow || requiredItems + requestedItems > maxActions)) {
358            maxActions--;
359        }
360        maxActions -= requiredItems;
361
362        final SparseBooleanArray seenGroups = mActionButtonGroups;
363        seenGroups.clear();
364
365        int cellSize = 0;
366        int cellsRemaining = 0;
367        if (mStrictWidthLimit) {
368            cellsRemaining = widthLimit / mMinCellSize;
369            final int cellSizeRemaining = widthLimit % mMinCellSize;
370            cellSize = mMinCellSize + cellSizeRemaining / cellsRemaining;
371        }
372
373        // Flag as many more requested items as will fit.
374        for (int i = 0; i < itemsSize; i++) {
375            MenuItemImpl item = visibleItems.get(i);
376
377            if (item.requiresActionButton()) {
378                View v = getItemView(item, mScrapActionButtonView, parent);
379                if (mScrapActionButtonView == null) {
380                    mScrapActionButtonView = v;
381                }
382                if (mStrictWidthLimit) {
383                    cellsRemaining -= ActionMenuView.measureChildForCells(v,
384                            cellSize, cellsRemaining, querySpec, 0);
385                } else {
386                    v.measure(querySpec, querySpec);
387                }
388                final int measuredWidth = v.getMeasuredWidth();
389                widthLimit -= measuredWidth;
390                if (firstActionWidth == 0) {
391                    firstActionWidth = measuredWidth;
392                }
393                final int groupId = item.getGroupId();
394                if (groupId != 0) {
395                    seenGroups.put(groupId, true);
396                }
397                item.setIsActionButton(true);
398            } else if (item.requestsActionButton()) {
399                // Items in a group with other items that already have an action slot
400                // can break the max actions rule, but not the width limit.
401                final int groupId = item.getGroupId();
402                final boolean inGroup = seenGroups.get(groupId);
403                boolean isAction = (maxActions > 0 || inGroup) && widthLimit > 0 &&
404                        (!mStrictWidthLimit || cellsRemaining > 0);
405
406                if (isAction) {
407                    View v = getItemView(item, mScrapActionButtonView, parent);
408                    if (mScrapActionButtonView == null) {
409                        mScrapActionButtonView = v;
410                    }
411                    if (mStrictWidthLimit) {
412                        final int cells = ActionMenuView.measureChildForCells(v,
413                                cellSize, cellsRemaining, querySpec, 0);
414                        cellsRemaining -= cells;
415                        if (cells == 0) {
416                            isAction = false;
417                        }
418                    } else {
419                        v.measure(querySpec, querySpec);
420                    }
421                    final int measuredWidth = v.getMeasuredWidth();
422                    widthLimit -= measuredWidth;
423                    if (firstActionWidth == 0) {
424                        firstActionWidth = measuredWidth;
425                    }
426
427                    if (mStrictWidthLimit) {
428                        isAction &= widthLimit >= 0;
429                    } else {
430                        // Did this push the entire first item past the limit?
431                        isAction &= widthLimit + firstActionWidth > 0;
432                    }
433                }
434
435                if (isAction && groupId != 0) {
436                    seenGroups.put(groupId, true);
437                } else if (inGroup) {
438                    // We broke the width limit. Demote the whole group, they all overflow now.
439                    seenGroups.put(groupId, false);
440                    for (int j = 0; j < i; j++) {
441                        MenuItemImpl areYouMyGroupie = visibleItems.get(j);
442                        if (areYouMyGroupie.getGroupId() == groupId) {
443                            // Give back the action slot
444                            if (areYouMyGroupie.isActionButton()) maxActions++;
445                            areYouMyGroupie.setIsActionButton(false);
446                        }
447                    }
448                }
449
450                if (isAction) maxActions--;
451
452                item.setIsActionButton(isAction);
453            }
454        }
455        return true;
456    }
457
458    @Override
459    public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
460        dismissPopupMenus();
461        super.onCloseMenu(menu, allMenusAreClosing);
462    }
463
464    @Override
465    public Parcelable onSaveInstanceState() {
466        SavedState state = new SavedState();
467        state.openSubMenuId = mOpenSubMenuId;
468        return state;
469    }
470
471    @Override
472    public void onRestoreInstanceState(Parcelable state) {
473        SavedState saved = (SavedState) state;
474        if (saved.openSubMenuId > 0) {
475            MenuItem item = mMenu.findItem(saved.openSubMenuId);
476            if (item != null) {
477                SubMenuBuilder subMenu = (SubMenuBuilder) item.getSubMenu();
478                onSubMenuSelected(subMenu);
479            }
480        }
481    }
482
483    private static class SavedState implements Parcelable {
484        public int openSubMenuId;
485
486        SavedState() {
487        }
488
489        SavedState(Parcel in) {
490            openSubMenuId = in.readInt();
491        }
492
493        @Override
494        public int describeContents() {
495            return 0;
496        }
497
498        @Override
499        public void writeToParcel(Parcel dest, int flags) {
500            dest.writeInt(openSubMenuId);
501        }
502
503        public static final Parcelable.Creator<SavedState> CREATOR
504                = new Parcelable.Creator<SavedState>() {
505            public SavedState createFromParcel(Parcel in) {
506                return new SavedState(in);
507            }
508
509            public SavedState[] newArray(int size) {
510                return new SavedState[size];
511            }
512        };
513    }
514
515    private class OverflowMenuButton extends ImageButton implements ActionMenuChildView {
516        public OverflowMenuButton(Context context) {
517            super(context, null, com.android.internal.R.attr.actionOverflowButtonStyle);
518
519            setClickable(true);
520            setFocusable(true);
521            setVisibility(VISIBLE);
522            setEnabled(true);
523        }
524
525        @Override
526        public boolean performClick() {
527            if (super.performClick()) {
528                return true;
529            }
530
531            playSoundEffect(SoundEffectConstants.CLICK);
532            showOverflowMenu();
533            return true;
534        }
535
536        public boolean needsDividerBefore() {
537            return false;
538        }
539
540        public boolean needsDividerAfter() {
541            return false;
542        }
543    }
544
545    private class OverflowPopup extends MenuPopupHelper {
546        public OverflowPopup(Context context, MenuBuilder menu, View anchorView,
547                boolean overflowOnly) {
548            super(context, menu, anchorView, overflowOnly);
549            setCallback(mPopupPresenterCallback);
550        }
551
552        @Override
553        public void onDismiss() {
554            super.onDismiss();
555            mMenu.close();
556            mOverflowPopup = null;
557        }
558    }
559
560    private class ActionButtonSubmenu extends MenuPopupHelper {
561        private SubMenuBuilder mSubMenu;
562
563        public ActionButtonSubmenu(Context context, SubMenuBuilder subMenu) {
564            super(context, subMenu);
565            mSubMenu = subMenu;
566
567            MenuItemImpl item = (MenuItemImpl) subMenu.getItem();
568            if (!item.isActionButton()) {
569                // Give a reasonable anchor to nested submenus.
570                setAnchorView(mOverflowButton == null ? (View) mMenuView : mOverflowButton);
571            }
572
573            setCallback(mPopupPresenterCallback);
574
575            boolean preserveIconSpacing = false;
576            final int count = subMenu.size();
577            for (int i = 0; i < count; i++) {
578                MenuItem childItem = subMenu.getItem(i);
579                if (childItem.isVisible() && childItem.getIcon() != null) {
580                    preserveIconSpacing = true;
581                    break;
582                }
583            }
584            setForceShowIcon(preserveIconSpacing);
585        }
586
587        @Override
588        public void onDismiss() {
589            super.onDismiss();
590            mSubMenu.close();
591            mActionButtonPopup = null;
592            mOpenSubMenuId = 0;
593        }
594    }
595
596    private class PopupPresenterCallback implements MenuPresenter.Callback {
597
598        @Override
599        public boolean onOpenSubMenu(MenuBuilder subMenu) {
600            mOpenSubMenuId = ((SubMenuBuilder) subMenu).getItem().getItemId();
601            return false;
602        }
603
604        @Override
605        public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
606        }
607    }
608
609    private class OpenOverflowRunnable implements Runnable {
610        private OverflowPopup mPopup;
611
612        public OpenOverflowRunnable(OverflowPopup popup) {
613            mPopup = popup;
614        }
615
616        public void run() {
617            mMenu.changeMenuMode();
618            if (mPopup.tryShow()) {
619                mOverflowPopup = mPopup;
620                mPostedOpenRunnable = null;
621            }
622        }
623    }
624}
625