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