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