Home | History | Annotate | Download | only in widget
      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 android.widget;
     18 
     19 import android.animation.Animator;
     20 import android.animation.AnimatorListenerAdapter;
     21 import android.animation.ObjectAnimator;
     22 import android.animation.PropertyValuesHolder;
     23 import android.annotation.NonNull;
     24 import android.annotation.Nullable;
     25 import android.content.Context;
     26 import android.content.res.Configuration;
     27 import android.content.res.Resources;
     28 import android.graphics.drawable.Drawable;
     29 import android.os.Parcel;
     30 import android.os.Parcelable;
     31 import android.util.SparseArray;
     32 import android.util.SparseBooleanArray;
     33 import android.view.ActionProvider;
     34 import android.view.Gravity;
     35 import android.view.MenuItem;
     36 import android.view.SoundEffectConstants;
     37 import android.view.View;
     38 import android.view.View.MeasureSpec;
     39 import android.view.ViewGroup;
     40 import android.view.ViewTreeObserver;
     41 import android.view.accessibility.AccessibilityNodeInfo;
     42 
     43 import com.android.internal.view.ActionBarPolicy;
     44 import com.android.internal.view.menu.ActionMenuItemView;
     45 import com.android.internal.view.menu.BaseMenuPresenter;
     46 import com.android.internal.view.menu.MenuBuilder;
     47 import com.android.internal.view.menu.MenuItemImpl;
     48 import com.android.internal.view.menu.MenuPopupHelper;
     49 import com.android.internal.view.menu.MenuView;
     50 import com.android.internal.view.menu.ShowableListMenu;
     51 import com.android.internal.view.menu.SubMenuBuilder;
     52 
     53 import java.util.ArrayList;
     54 import java.util.List;
     55 
     56 /**
     57  * MenuPresenter for building action menus as seen in the action bar and action modes.
     58  *
     59  * @hide
     60  */
     61 public class ActionMenuPresenter extends BaseMenuPresenter
     62         implements ActionProvider.SubUiVisibilityListener {
     63     private static final int ITEM_ANIMATION_DURATION = 150;
     64     private static final boolean ACTIONBAR_ANIMATIONS_ENABLED = false;
     65 
     66     private OverflowMenuButton mOverflowButton;
     67     private Drawable mPendingOverflowIcon;
     68     private boolean mPendingOverflowIconSet;
     69     private boolean mReserveOverflow;
     70     private boolean mReserveOverflowSet;
     71     private int mWidthLimit;
     72     private int mActionItemWidthLimit;
     73     private int mMaxItems;
     74     private boolean mMaxItemsSet;
     75     private boolean mStrictWidthLimit;
     76     private boolean mWidthLimitSet;
     77     private boolean mExpandedActionViewsExclusive;
     78 
     79     private int mMinCellSize;
     80 
     81     // Group IDs that have been added as actions - used temporarily, allocated here for reuse.
     82     private final SparseBooleanArray mActionButtonGroups = new SparseBooleanArray();
     83 
     84     private OverflowPopup mOverflowPopup;
     85     private ActionButtonSubmenu mActionButtonPopup;
     86 
     87     private OpenOverflowRunnable mPostedOpenRunnable;
     88     private ActionMenuPopupCallback mPopupCallback;
     89 
     90     final PopupPresenterCallback mPopupPresenterCallback = new PopupPresenterCallback();
     91     int mOpenSubMenuId;
     92 
     93     // These collections are used to store pre- and post-layout information for menu items,
     94     // which is used to determine appropriate animations to run for changed items.
     95     private SparseArray<MenuItemLayoutInfo> mPreLayoutItems = new SparseArray<>();
     96     private SparseArray<MenuItemLayoutInfo> mPostLayoutItems = new SparseArray<>();
     97 
     98     // The list of currently running animations on menu items.
     99     private List<ItemAnimationInfo> mRunningItemAnimations = new ArrayList<>();
    100     private ViewTreeObserver.OnPreDrawListener mItemAnimationPreDrawListener =
    101             new ViewTreeObserver.OnPreDrawListener() {
    102         @Override
    103         public boolean onPreDraw() {
    104             computeMenuItemAnimationInfo(false);
    105             ((View) mMenuView).getViewTreeObserver().removeOnPreDrawListener(this);
    106             runItemAnimations();
    107             return true;
    108         }
    109     };
    110     private View.OnAttachStateChangeListener mAttachStateChangeListener =
    111             new View.OnAttachStateChangeListener() {
    112         @Override
    113         public void onViewAttachedToWindow(View v) {
    114         }
    115 
    116         @Override
    117         public void onViewDetachedFromWindow(View v) {
    118             ((View) mMenuView).getViewTreeObserver().removeOnPreDrawListener(
    119                     mItemAnimationPreDrawListener);
    120             mPreLayoutItems.clear();
    121             mPostLayoutItems.clear();
    122         }
    123     };
    124 
    125 
    126     public ActionMenuPresenter(Context context) {
    127         super(context, com.android.internal.R.layout.action_menu_layout,
    128                 com.android.internal.R.layout.action_menu_item_layout);
    129     }
    130 
    131     @Override
    132     public void initForMenu(@NonNull Context context, @Nullable MenuBuilder menu) {
    133         super.initForMenu(context, menu);
    134 
    135         final Resources res = context.getResources();
    136 
    137         final ActionBarPolicy abp = ActionBarPolicy.get(context);
    138         if (!mReserveOverflowSet) {
    139             mReserveOverflow = abp.showsOverflowMenuButton();
    140         }
    141 
    142         if (!mWidthLimitSet) {
    143             mWidthLimit = abp.getEmbeddedMenuWidthLimit();
    144         }
    145 
    146         // Measure for initial configuration
    147         if (!mMaxItemsSet) {
    148             mMaxItems = abp.getMaxActionButtons();
    149         }
    150 
    151         int width = mWidthLimit;
    152         if (mReserveOverflow) {
    153             if (mOverflowButton == null) {
    154                 mOverflowButton = new OverflowMenuButton(mSystemContext);
    155                 if (mPendingOverflowIconSet) {
    156                     mOverflowButton.setImageDrawable(mPendingOverflowIcon);
    157                     mPendingOverflowIcon = null;
    158                     mPendingOverflowIconSet = false;
    159                 }
    160                 final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
    161                 mOverflowButton.measure(spec, spec);
    162             }
    163             width -= mOverflowButton.getMeasuredWidth();
    164         } else {
    165             mOverflowButton = null;
    166         }
    167 
    168         mActionItemWidthLimit = width;
    169 
    170         mMinCellSize = (int) (ActionMenuView.MIN_CELL_SIZE * res.getDisplayMetrics().density);
    171     }
    172 
    173     public void onConfigurationChanged(Configuration newConfig) {
    174         if (!mMaxItemsSet) {
    175             mMaxItems = ActionBarPolicy.get(mContext).getMaxActionButtons();
    176         }
    177         if (mMenu != null) {
    178             mMenu.onItemsChanged(true);
    179         }
    180     }
    181 
    182     public void setWidthLimit(int width, boolean strict) {
    183         mWidthLimit = width;
    184         mStrictWidthLimit = strict;
    185         mWidthLimitSet = true;
    186     }
    187 
    188     public void setReserveOverflow(boolean reserveOverflow) {
    189         mReserveOverflow = reserveOverflow;
    190         mReserveOverflowSet = true;
    191     }
    192 
    193     public void setItemLimit(int itemCount) {
    194         mMaxItems = itemCount;
    195         mMaxItemsSet = true;
    196     }
    197 
    198     public void setExpandedActionViewsExclusive(boolean isExclusive) {
    199         mExpandedActionViewsExclusive = isExclusive;
    200     }
    201 
    202     public void setOverflowIcon(Drawable icon) {
    203         if (mOverflowButton != null) {
    204             mOverflowButton.setImageDrawable(icon);
    205         } else {
    206             mPendingOverflowIconSet = true;
    207             mPendingOverflowIcon = icon;
    208         }
    209     }
    210 
    211     public Drawable getOverflowIcon() {
    212         if (mOverflowButton != null) {
    213             return mOverflowButton.getDrawable();
    214         } else if (mPendingOverflowIconSet) {
    215             return mPendingOverflowIcon;
    216         }
    217         return null;
    218     }
    219 
    220     @Override
    221     public MenuView getMenuView(ViewGroup root) {
    222         MenuView oldMenuView = mMenuView;
    223         MenuView result = super.getMenuView(root);
    224         if (oldMenuView != result) {
    225             ((ActionMenuView) result).setPresenter(this);
    226             if (oldMenuView != null) {
    227                 ((View) oldMenuView).removeOnAttachStateChangeListener(mAttachStateChangeListener);
    228             }
    229             ((View) result).addOnAttachStateChangeListener(mAttachStateChangeListener);
    230         }
    231         return result;
    232     }
    233 
    234     @Override
    235     public View getItemView(final MenuItemImpl item, View convertView, ViewGroup parent) {
    236         View actionView = item.getActionView();
    237         if (actionView == null || item.hasCollapsibleActionView()) {
    238             actionView = super.getItemView(item, convertView, parent);
    239         }
    240         actionView.setVisibility(item.isActionViewExpanded() ? View.GONE : View.VISIBLE);
    241 
    242         final ActionMenuView menuParent = (ActionMenuView) parent;
    243         final ViewGroup.LayoutParams lp = actionView.getLayoutParams();
    244         if (!menuParent.checkLayoutParams(lp)) {
    245             actionView.setLayoutParams(menuParent.generateLayoutParams(lp));
    246         }
    247         return actionView;
    248     }
    249 
    250     @Override
    251     public void bindItemView(MenuItemImpl item, MenuView.ItemView itemView) {
    252         itemView.initialize(item, 0);
    253 
    254         final ActionMenuView menuView = (ActionMenuView) mMenuView;
    255         final ActionMenuItemView actionItemView = (ActionMenuItemView) itemView;
    256         actionItemView.setItemInvoker(menuView);
    257 
    258         if (mPopupCallback == null) {
    259             mPopupCallback = new ActionMenuPopupCallback();
    260         }
    261         actionItemView.setPopupCallback(mPopupCallback);
    262     }
    263 
    264     @Override
    265     public boolean shouldIncludeItem(int childIndex, MenuItemImpl item) {
    266         return item.isActionButton();
    267     }
    268 
    269     /**
    270      * Store layout information about current items in the menu. This is stored for
    271      * both pre- and post-layout phases and compared in runItemAnimations() to determine
    272      * the animations that need to be run on any item changes.
    273      *
    274      * @param preLayout Whether this is being called in the pre-layout phase. This is passed
    275      * into the MenuItemLayoutInfo structure to store the appropriate position values.
    276      */
    277     private void computeMenuItemAnimationInfo(boolean preLayout) {
    278         final ViewGroup menuView = (ViewGroup) mMenuView;
    279         final int count = menuView.getChildCount();
    280         SparseArray items = preLayout ? mPreLayoutItems : mPostLayoutItems;
    281         for (int i = 0; i < count; ++i) {
    282             View child = menuView.getChildAt(i);
    283             final int id = child.getId();
    284             if (id > 0 && child.getWidth() != 0 && child.getHeight() != 0) {
    285                 MenuItemLayoutInfo info = new MenuItemLayoutInfo(child, preLayout);
    286                 items.put(id, info);
    287             }
    288         }
    289     }
    290 
    291     /**
    292      * This method is called once both the pre-layout and post-layout steps have
    293      * happened. It figures out which views are new (didn't exist prior to layout),
    294      * gone (existed pre-layout, but are now gone), or changed (exist in both,
    295      * but in a different location) and runs appropriate animations on those views.
    296      * Items are tracked by ids, since the underlying views that represent items
    297      * pre- and post-layout may be different.
    298      */
    299     private void runItemAnimations() {
    300         for (int i = 0; i < mPreLayoutItems.size(); ++i) {
    301             int id = mPreLayoutItems.keyAt(i);
    302             final MenuItemLayoutInfo menuItemLayoutInfoPre = mPreLayoutItems.get(id);
    303             final int postLayoutIndex = mPostLayoutItems.indexOfKey(id);
    304             if (postLayoutIndex >= 0) {
    305                 // item exists pre and post: see if it's changed
    306                 final MenuItemLayoutInfo menuItemLayoutInfoPost =
    307                         mPostLayoutItems.valueAt(postLayoutIndex);
    308                 PropertyValuesHolder pvhX = null;
    309                 PropertyValuesHolder pvhY = null;
    310                 if (menuItemLayoutInfoPre.left != menuItemLayoutInfoPost.left) {
    311                     pvhX = PropertyValuesHolder.ofFloat(View.TRANSLATION_X,
    312                             (menuItemLayoutInfoPre.left - menuItemLayoutInfoPost.left), 0);
    313                 }
    314                 if (menuItemLayoutInfoPre.top != menuItemLayoutInfoPost.top) {
    315                     pvhY = PropertyValuesHolder.ofFloat(View.TRANSLATION_Y,
    316                             menuItemLayoutInfoPre.top - menuItemLayoutInfoPost.top, 0);
    317                 }
    318                 if (pvhX != null || pvhY != null) {
    319                     for (int j = 0; j < mRunningItemAnimations.size(); ++j) {
    320                         ItemAnimationInfo oldInfo = mRunningItemAnimations.get(j);
    321                         if (oldInfo.id == id && oldInfo.animType == ItemAnimationInfo.MOVE) {
    322                             oldInfo.animator.cancel();
    323                         }
    324                     }
    325                     ObjectAnimator anim;
    326                     if (pvhX != null) {
    327                         if (pvhY != null) {
    328                             anim = ObjectAnimator.ofPropertyValuesHolder(menuItemLayoutInfoPost.view,
    329                                     pvhX, pvhY);
    330                         } else {
    331                             anim = ObjectAnimator.ofPropertyValuesHolder(menuItemLayoutInfoPost.view, pvhX);
    332                         }
    333                     } else {
    334                         anim = ObjectAnimator.ofPropertyValuesHolder(menuItemLayoutInfoPost.view, pvhY);
    335                     }
    336                     anim.setDuration(ITEM_ANIMATION_DURATION);
    337                     anim.start();
    338                     ItemAnimationInfo info = new ItemAnimationInfo(id, menuItemLayoutInfoPost, anim,
    339                             ItemAnimationInfo.MOVE);
    340                     mRunningItemAnimations.add(info);
    341                     anim.addListener(new AnimatorListenerAdapter() {
    342                         @Override
    343                         public void onAnimationEnd(Animator animation) {
    344                             for (int j = 0; j < mRunningItemAnimations.size(); ++j) {
    345                                 if (mRunningItemAnimations.get(j).animator == animation) {
    346                                     mRunningItemAnimations.remove(j);
    347                                     break;
    348                                 }
    349                             }
    350                         }
    351                     });
    352                 }
    353                 mPostLayoutItems.remove(id);
    354             } else {
    355                 // item used to be there, is now gone
    356                 float oldAlpha = 1;
    357                 for (int j = 0; j < mRunningItemAnimations.size(); ++j) {
    358                     ItemAnimationInfo oldInfo = mRunningItemAnimations.get(j);
    359                     if (oldInfo.id == id && oldInfo.animType == ItemAnimationInfo.FADE_IN) {
    360                         oldAlpha = oldInfo.menuItemLayoutInfo.view.getAlpha();
    361                         oldInfo.animator.cancel();
    362                     }
    363                 }
    364                 ObjectAnimator anim = ObjectAnimator.ofFloat(menuItemLayoutInfoPre.view, View.ALPHA,
    365                         oldAlpha, 0);
    366                 // Re-using the view from pre-layout assumes no view recycling
    367                 ((ViewGroup) mMenuView).getOverlay().add(menuItemLayoutInfoPre.view);
    368                 anim.setDuration(ITEM_ANIMATION_DURATION);
    369                 anim.start();
    370                 ItemAnimationInfo info = new ItemAnimationInfo(id, menuItemLayoutInfoPre, anim, ItemAnimationInfo.FADE_OUT);
    371                 mRunningItemAnimations.add(info);
    372                 anim.addListener(new AnimatorListenerAdapter() {
    373                     @Override
    374                     public void onAnimationEnd(Animator animation) {
    375                         for (int j = 0; j < mRunningItemAnimations.size(); ++j) {
    376                             if (mRunningItemAnimations.get(j).animator == animation) {
    377                                 mRunningItemAnimations.remove(j);
    378                                 break;
    379                             }
    380                         }
    381                         ((ViewGroup) mMenuView).getOverlay().remove(menuItemLayoutInfoPre.view);
    382                     }
    383                 });
    384             }
    385         }
    386         for (int i = 0; i < mPostLayoutItems.size(); ++i) {
    387             int id = mPostLayoutItems.keyAt(i);
    388             final int postLayoutIndex = mPostLayoutItems.indexOfKey(id);
    389             if (postLayoutIndex >= 0) {
    390                 // item is new
    391                 final MenuItemLayoutInfo menuItemLayoutInfo =
    392                         mPostLayoutItems.valueAt(postLayoutIndex);
    393                 float oldAlpha = 0;
    394                 for (int j = 0; j < mRunningItemAnimations.size(); ++j) {
    395                     ItemAnimationInfo oldInfo = mRunningItemAnimations.get(j);
    396                     if (oldInfo.id == id && oldInfo.animType == ItemAnimationInfo.FADE_OUT) {
    397                         oldAlpha = oldInfo.menuItemLayoutInfo.view.getAlpha();
    398                         oldInfo.animator.cancel();
    399                     }
    400                 }
    401                 ObjectAnimator anim = ObjectAnimator.ofFloat(menuItemLayoutInfo.view, View.ALPHA,
    402                         oldAlpha, 1);
    403                 anim.start();
    404                 anim.setDuration(ITEM_ANIMATION_DURATION);
    405                 ItemAnimationInfo info = new ItemAnimationInfo(id, menuItemLayoutInfo, anim, ItemAnimationInfo.FADE_IN);
    406                 mRunningItemAnimations.add(info);
    407                 anim.addListener(new AnimatorListenerAdapter() {
    408                     @Override
    409                     public void onAnimationEnd(Animator animation) {
    410                         for (int j = 0; j < mRunningItemAnimations.size(); ++j) {
    411                             if (mRunningItemAnimations.get(j).animator == animation) {
    412                                 mRunningItemAnimations.remove(j);
    413                                 break;
    414                             }
    415                         }
    416                     }
    417                 });
    418             }
    419         }
    420         mPreLayoutItems.clear();
    421         mPostLayoutItems.clear();
    422     }
    423 
    424     /**
    425      * Gets position/existence information on menu items before and after layout,
    426      * which is then fed into runItemAnimations()
    427      */
    428     private void setupItemAnimations() {
    429         computeMenuItemAnimationInfo(true);
    430         ((View) mMenuView).getViewTreeObserver().
    431                 addOnPreDrawListener(mItemAnimationPreDrawListener);
    432     }
    433 
    434     @Override
    435     public void updateMenuView(boolean cleared) {
    436         final ViewGroup menuViewParent = (ViewGroup) ((View) mMenuView).getParent();
    437         if (menuViewParent != null && ACTIONBAR_ANIMATIONS_ENABLED) {
    438             setupItemAnimations();
    439         }
    440         super.updateMenuView(cleared);
    441 
    442         ((View) mMenuView).requestLayout();
    443 
    444         if (mMenu != null) {
    445             final ArrayList<MenuItemImpl> actionItems = mMenu.getActionItems();
    446             final int count = actionItems.size();
    447             for (int i = 0; i < count; i++) {
    448                 final ActionProvider provider = actionItems.get(i).getActionProvider();
    449                 if (provider != null) {
    450                     provider.setSubUiVisibilityListener(this);
    451                 }
    452             }
    453         }
    454 
    455         final ArrayList<MenuItemImpl> nonActionItems = mMenu != null ?
    456                 mMenu.getNonActionItems() : null;
    457 
    458         boolean hasOverflow = false;
    459         if (mReserveOverflow && nonActionItems != null) {
    460             final int count = nonActionItems.size();
    461             if (count == 1) {
    462                 hasOverflow = !nonActionItems.get(0).isActionViewExpanded();
    463             } else {
    464                 hasOverflow = count > 0;
    465             }
    466         }
    467 
    468         if (hasOverflow) {
    469             if (mOverflowButton == null) {
    470                 mOverflowButton = new OverflowMenuButton(mSystemContext);
    471             }
    472             ViewGroup parent = (ViewGroup) mOverflowButton.getParent();
    473             if (parent != mMenuView) {
    474                 if (parent != null) {
    475                     parent.removeView(mOverflowButton);
    476                 }
    477                 ActionMenuView menuView = (ActionMenuView) mMenuView;
    478                 menuView.addView(mOverflowButton, menuView.generateOverflowButtonLayoutParams());
    479             }
    480         } else if (mOverflowButton != null && mOverflowButton.getParent() == mMenuView) {
    481             ((ViewGroup) mMenuView).removeView(mOverflowButton);
    482         }
    483 
    484         ((ActionMenuView) mMenuView).setOverflowReserved(mReserveOverflow);
    485     }
    486 
    487     @Override
    488     public boolean filterLeftoverView(ViewGroup parent, int childIndex) {
    489         if (parent.getChildAt(childIndex) == mOverflowButton) return false;
    490         return super.filterLeftoverView(parent, childIndex);
    491     }
    492 
    493     public boolean onSubMenuSelected(SubMenuBuilder subMenu) {
    494         if (!subMenu.hasVisibleItems()) return false;
    495 
    496         SubMenuBuilder topSubMenu = subMenu;
    497         while (topSubMenu.getParentMenu() != mMenu) {
    498             topSubMenu = (SubMenuBuilder) topSubMenu.getParentMenu();
    499         }
    500         View anchor = findViewForItem(topSubMenu.getItem());
    501         if (anchor == null) {
    502             // This means the submenu was opened from an overflow menu item, indicating the
    503             // MenuPopupHelper will handle opening the submenu via its MenuPopup. Return false to
    504             // ensure that the MenuPopup acts as presenter for the submenu, and acts on its
    505             // responsibility to display the new submenu.
    506             return false;
    507         }
    508 
    509         mOpenSubMenuId = subMenu.getItem().getItemId();
    510 
    511         boolean preserveIconSpacing = false;
    512         final int count = subMenu.size();
    513         for (int i = 0; i < count; i++) {
    514             MenuItem childItem = subMenu.getItem(i);
    515             if (childItem.isVisible() && childItem.getIcon() != null) {
    516                 preserveIconSpacing = true;
    517                 break;
    518             }
    519         }
    520 
    521         mActionButtonPopup = new ActionButtonSubmenu(mContext, subMenu, anchor);
    522         mActionButtonPopup.setForceShowIcon(preserveIconSpacing);
    523         mActionButtonPopup.show();
    524 
    525         super.onSubMenuSelected(subMenu);
    526         return true;
    527     }
    528 
    529     private View findViewForItem(MenuItem item) {
    530         final ViewGroup parent = (ViewGroup) mMenuView;
    531         if (parent == null) return null;
    532 
    533         final int count = parent.getChildCount();
    534         for (int i = 0; i < count; i++) {
    535             final View child = parent.getChildAt(i);
    536             if (child instanceof MenuView.ItemView &&
    537                     ((MenuView.ItemView) child).getItemData() == item) {
    538                 return child;
    539             }
    540         }
    541         return null;
    542     }
    543 
    544     /**
    545      * Display the overflow menu if one is present.
    546      * @return true if the overflow menu was shown, false otherwise.
    547      */
    548     public boolean showOverflowMenu() {
    549         if (mReserveOverflow && !isOverflowMenuShowing() && mMenu != null && mMenuView != null &&
    550                 mPostedOpenRunnable == null && !mMenu.getNonActionItems().isEmpty()) {
    551             OverflowPopup popup = new OverflowPopup(mContext, mMenu, mOverflowButton, true);
    552             mPostedOpenRunnable = new OpenOverflowRunnable(popup);
    553             // Post this for later; we might still need a layout for the anchor to be right.
    554             ((View) mMenuView).post(mPostedOpenRunnable);
    555 
    556             // ActionMenuPresenter uses null as a callback argument here
    557             // to indicate overflow is opening.
    558             super.onSubMenuSelected(null);
    559 
    560             return true;
    561         }
    562         return false;
    563     }
    564 
    565     /**
    566      * Hide the overflow menu if it is currently showing.
    567      *
    568      * @return true if the overflow menu was hidden, false otherwise.
    569      */
    570     public boolean hideOverflowMenu() {
    571         if (mPostedOpenRunnable != null && mMenuView != null) {
    572             ((View) mMenuView).removeCallbacks(mPostedOpenRunnable);
    573             mPostedOpenRunnable = null;
    574             return true;
    575         }
    576 
    577         MenuPopupHelper popup = mOverflowPopup;
    578         if (popup != null) {
    579             popup.dismiss();
    580             return true;
    581         }
    582         return false;
    583     }
    584 
    585     /**
    586      * Dismiss all popup menus - overflow and submenus.
    587      * @return true if popups were dismissed, false otherwise. (This can be because none were open.)
    588      */
    589     public boolean dismissPopupMenus() {
    590         boolean result = hideOverflowMenu();
    591         result |= hideSubMenus();
    592         return result;
    593     }
    594 
    595     /**
    596      * Dismiss all submenu popups.
    597      *
    598      * @return true if popups were dismissed, false otherwise. (This can be because none were open.)
    599      */
    600     public boolean hideSubMenus() {
    601         if (mActionButtonPopup != null) {
    602             mActionButtonPopup.dismiss();
    603             return true;
    604         }
    605         return false;
    606     }
    607 
    608     /**
    609      * @return true if the overflow menu is currently showing
    610      */
    611     public boolean isOverflowMenuShowing() {
    612         return mOverflowPopup != null && mOverflowPopup.isShowing();
    613     }
    614 
    615     public boolean isOverflowMenuShowPending() {
    616         return mPostedOpenRunnable != null || isOverflowMenuShowing();
    617     }
    618 
    619     /**
    620      * @return true if space has been reserved in the action menu for an overflow item.
    621      */
    622     public boolean isOverflowReserved() {
    623         return mReserveOverflow;
    624     }
    625 
    626     public boolean flagActionItems() {
    627         final ArrayList<MenuItemImpl> visibleItems;
    628         final int itemsSize;
    629         if (mMenu != null) {
    630             visibleItems = mMenu.getVisibleItems();
    631             itemsSize = visibleItems.size();
    632         } else {
    633             visibleItems = null;
    634             itemsSize = 0;
    635         }
    636 
    637         int maxActions = mMaxItems;
    638         int widthLimit = mActionItemWidthLimit;
    639         final int querySpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
    640         final ViewGroup parent = (ViewGroup) mMenuView;
    641 
    642         int requiredItems = 0;
    643         int requestedItems = 0;
    644         int firstActionWidth = 0;
    645         boolean hasOverflow = false;
    646         for (int i = 0; i < itemsSize; i++) {
    647             MenuItemImpl item = visibleItems.get(i);
    648             if (item.requiresActionButton()) {
    649                 requiredItems++;
    650             } else if (item.requestsActionButton()) {
    651                 requestedItems++;
    652             } else {
    653                 hasOverflow = true;
    654             }
    655             if (mExpandedActionViewsExclusive && item.isActionViewExpanded()) {
    656                 // Overflow everything if we have an expanded action view and we're
    657                 // space constrained.
    658                 maxActions = 0;
    659             }
    660         }
    661 
    662         // Reserve a spot for the overflow item if needed.
    663         if (mReserveOverflow &&
    664                 (hasOverflow || requiredItems + requestedItems > maxActions)) {
    665             maxActions--;
    666         }
    667         maxActions -= requiredItems;
    668 
    669         final SparseBooleanArray seenGroups = mActionButtonGroups;
    670         seenGroups.clear();
    671 
    672         int cellSize = 0;
    673         int cellsRemaining = 0;
    674         if (mStrictWidthLimit) {
    675             cellsRemaining = widthLimit / mMinCellSize;
    676             final int cellSizeRemaining = widthLimit % mMinCellSize;
    677             cellSize = mMinCellSize + cellSizeRemaining / cellsRemaining;
    678         }
    679 
    680         // Flag as many more requested items as will fit.
    681         for (int i = 0; i < itemsSize; i++) {
    682             MenuItemImpl item = visibleItems.get(i);
    683 
    684             if (item.requiresActionButton()) {
    685                 View v = getItemView(item, null, parent);
    686                 if (mStrictWidthLimit) {
    687                     cellsRemaining -= ActionMenuView.measureChildForCells(v,
    688                             cellSize, cellsRemaining, querySpec, 0);
    689                 } else {
    690                     v.measure(querySpec, querySpec);
    691                 }
    692                 final int measuredWidth = v.getMeasuredWidth();
    693                 widthLimit -= measuredWidth;
    694                 if (firstActionWidth == 0) {
    695                     firstActionWidth = measuredWidth;
    696                 }
    697                 final int groupId = item.getGroupId();
    698                 if (groupId != 0) {
    699                     seenGroups.put(groupId, true);
    700                 }
    701                 item.setIsActionButton(true);
    702             } else if (item.requestsActionButton()) {
    703                 // Items in a group with other items that already have an action slot
    704                 // can break the max actions rule, but not the width limit.
    705                 final int groupId = item.getGroupId();
    706                 final boolean inGroup = seenGroups.get(groupId);
    707                 boolean isAction = (maxActions > 0 || inGroup) && widthLimit > 0 &&
    708                         (!mStrictWidthLimit || cellsRemaining > 0);
    709 
    710                 if (isAction) {
    711                     View v = getItemView(item, null, parent);
    712                     if (mStrictWidthLimit) {
    713                         final int cells = ActionMenuView.measureChildForCells(v,
    714                                 cellSize, cellsRemaining, querySpec, 0);
    715                         cellsRemaining -= cells;
    716                         if (cells == 0) {
    717                             isAction = false;
    718                         }
    719                     } else {
    720                         v.measure(querySpec, querySpec);
    721                     }
    722                     final int measuredWidth = v.getMeasuredWidth();
    723                     widthLimit -= measuredWidth;
    724                     if (firstActionWidth == 0) {
    725                         firstActionWidth = measuredWidth;
    726                     }
    727 
    728                     if (mStrictWidthLimit) {
    729                         isAction &= widthLimit >= 0;
    730                     } else {
    731                         // Did this push the entire first item past the limit?
    732                         isAction &= widthLimit + firstActionWidth > 0;
    733                     }
    734                 }
    735 
    736                 if (isAction && groupId != 0) {
    737                     seenGroups.put(groupId, true);
    738                 } else if (inGroup) {
    739                     // We broke the width limit. Demote the whole group, they all overflow now.
    740                     seenGroups.put(groupId, false);
    741                     for (int j = 0; j < i; j++) {
    742                         MenuItemImpl areYouMyGroupie = visibleItems.get(j);
    743                         if (areYouMyGroupie.getGroupId() == groupId) {
    744                             // Give back the action slot
    745                             if (areYouMyGroupie.isActionButton()) maxActions++;
    746                             areYouMyGroupie.setIsActionButton(false);
    747                         }
    748                     }
    749                 }
    750 
    751                 if (isAction) maxActions--;
    752 
    753                 item.setIsActionButton(isAction);
    754             } else {
    755                 // Neither requires nor requests an action button.
    756                 item.setIsActionButton(false);
    757             }
    758         }
    759         return true;
    760     }
    761 
    762     @Override
    763     public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
    764         dismissPopupMenus();
    765         super.onCloseMenu(menu, allMenusAreClosing);
    766     }
    767 
    768     @Override
    769     public Parcelable onSaveInstanceState() {
    770         SavedState state = new SavedState();
    771         state.openSubMenuId = mOpenSubMenuId;
    772         return state;
    773     }
    774 
    775     @Override
    776     public void onRestoreInstanceState(Parcelable state) {
    777         SavedState saved = (SavedState) state;
    778         if (saved.openSubMenuId > 0) {
    779             MenuItem item = mMenu.findItem(saved.openSubMenuId);
    780             if (item != null) {
    781                 SubMenuBuilder subMenu = (SubMenuBuilder) item.getSubMenu();
    782                 onSubMenuSelected(subMenu);
    783             }
    784         }
    785     }
    786 
    787     @Override
    788     public void onSubUiVisibilityChanged(boolean isVisible) {
    789         if (isVisible) {
    790             // Not a submenu, but treat it like one.
    791             super.onSubMenuSelected(null);
    792         } else if (mMenu != null) {
    793             mMenu.close(false /* closeAllMenus */);
    794         }
    795     }
    796 
    797     public void setMenuView(ActionMenuView menuView) {
    798         if (menuView != mMenuView) {
    799             if (mMenuView != null) {
    800                 ((View) mMenuView).removeOnAttachStateChangeListener(mAttachStateChangeListener);
    801             }
    802             mMenuView = menuView;
    803             menuView.initialize(mMenu);
    804             menuView.addOnAttachStateChangeListener(mAttachStateChangeListener);
    805         }
    806     }
    807 
    808     private static class SavedState implements Parcelable {
    809         public int openSubMenuId;
    810 
    811         SavedState() {
    812         }
    813 
    814         SavedState(Parcel in) {
    815             openSubMenuId = in.readInt();
    816         }
    817 
    818         @Override
    819         public int describeContents() {
    820             return 0;
    821         }
    822 
    823         @Override
    824         public void writeToParcel(Parcel dest, int flags) {
    825             dest.writeInt(openSubMenuId);
    826         }
    827 
    828         public static final Parcelable.Creator<SavedState> CREATOR
    829                 = new Parcelable.Creator<SavedState>() {
    830             public SavedState createFromParcel(Parcel in) {
    831                 return new SavedState(in);
    832             }
    833 
    834             public SavedState[] newArray(int size) {
    835                 return new SavedState[size];
    836             }
    837         };
    838     }
    839 
    840     private class OverflowMenuButton extends ImageButton implements ActionMenuView.ActionMenuChildView {
    841         public OverflowMenuButton(Context context) {
    842             super(context, null, com.android.internal.R.attr.actionOverflowButtonStyle);
    843 
    844             setClickable(true);
    845             setFocusable(true);
    846             setVisibility(VISIBLE);
    847             setEnabled(true);
    848 
    849             setOnTouchListener(new ForwardingListener(this) {
    850                 @Override
    851                 public ShowableListMenu getPopup() {
    852                     if (mOverflowPopup == null) {
    853                         return null;
    854                     }
    855 
    856                     return mOverflowPopup.getPopup();
    857                 }
    858 
    859                 @Override
    860                 public boolean onForwardingStarted() {
    861                     showOverflowMenu();
    862                     return true;
    863                 }
    864 
    865                 @Override
    866                 public boolean onForwardingStopped() {
    867                     // Displaying the popup occurs asynchronously, so wait for
    868                     // the runnable to finish before deciding whether to stop
    869                     // forwarding.
    870                     if (mPostedOpenRunnable != null) {
    871                         return false;
    872                     }
    873 
    874                     hideOverflowMenu();
    875                     return true;
    876                 }
    877             });
    878         }
    879 
    880         @Override
    881         public boolean performClick() {
    882             if (super.performClick()) {
    883                 return true;
    884             }
    885 
    886             playSoundEffect(SoundEffectConstants.CLICK);
    887             showOverflowMenu();
    888             return true;
    889         }
    890 
    891         @Override
    892         public boolean needsDividerBefore() {
    893             return false;
    894         }
    895 
    896         @Override
    897         public boolean needsDividerAfter() {
    898             return false;
    899         }
    900 
    901     /** @hide */
    902         @Override
    903         public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
    904             super.onInitializeAccessibilityNodeInfoInternal(info);
    905             info.setCanOpenPopup(true);
    906         }
    907 
    908         @Override
    909         protected boolean setFrame(int l, int t, int r, int b) {
    910             final boolean changed = super.setFrame(l, t, r, b);
    911 
    912             // Set up the hotspot bounds to square and centered on the image.
    913             final Drawable d = getDrawable();
    914             final Drawable bg = getBackground();
    915             if (d != null && bg != null) {
    916                 final int width = getWidth();
    917                 final int height = getHeight();
    918                 final int halfEdge = Math.max(width, height) / 2;
    919                 final int offsetX = getPaddingLeft() - getPaddingRight();
    920                 final int offsetY = getPaddingTop() - getPaddingBottom();
    921                 final int centerX = (width + offsetX) / 2;
    922                 final int centerY = (height + offsetY) / 2;
    923                 bg.setHotspotBounds(centerX - halfEdge, centerY - halfEdge,
    924                         centerX + halfEdge, centerY + halfEdge);
    925             }
    926 
    927             return changed;
    928         }
    929     }
    930 
    931     private class OverflowPopup extends MenuPopupHelper {
    932         public OverflowPopup(Context context, MenuBuilder menu, View anchorView,
    933                 boolean overflowOnly) {
    934             super(context, menu, anchorView, overflowOnly,
    935                     com.android.internal.R.attr.actionOverflowMenuStyle);
    936             setGravity(Gravity.END);
    937             setPresenterCallback(mPopupPresenterCallback);
    938         }
    939 
    940         @Override
    941         protected void onDismiss() {
    942             if (mMenu != null) {
    943                 mMenu.close();
    944             }
    945             mOverflowPopup = null;
    946 
    947             super.onDismiss();
    948         }
    949     }
    950 
    951     private class ActionButtonSubmenu extends MenuPopupHelper {
    952         public ActionButtonSubmenu(Context context, SubMenuBuilder subMenu, View anchorView) {
    953             super(context, subMenu, anchorView, false,
    954                     com.android.internal.R.attr.actionOverflowMenuStyle);
    955 
    956             MenuItemImpl item = (MenuItemImpl) subMenu.getItem();
    957             if (!item.isActionButton()) {
    958                 // Give a reasonable anchor to nested submenus.
    959                 setAnchorView(mOverflowButton == null ? (View) mMenuView : mOverflowButton);
    960             }
    961 
    962             setPresenterCallback(mPopupPresenterCallback);
    963         }
    964 
    965         @Override
    966         protected void onDismiss() {
    967             mActionButtonPopup = null;
    968             mOpenSubMenuId = 0;
    969 
    970             super.onDismiss();
    971         }
    972     }
    973 
    974     private class PopupPresenterCallback implements Callback {
    975 
    976         @Override
    977         public boolean onOpenSubMenu(MenuBuilder subMenu) {
    978             if (subMenu == null) return false;
    979 
    980             mOpenSubMenuId = ((SubMenuBuilder) subMenu).getItem().getItemId();
    981             final Callback cb = getCallback();
    982             return cb != null ? cb.onOpenSubMenu(subMenu) : false;
    983         }
    984 
    985         @Override
    986         public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
    987             if (menu instanceof SubMenuBuilder) {
    988                 menu.getRootMenu().close(false /* closeAllMenus */);
    989             }
    990             final Callback cb = getCallback();
    991             if (cb != null) {
    992                 cb.onCloseMenu(menu, allMenusAreClosing);
    993             }
    994         }
    995     }
    996 
    997     private class OpenOverflowRunnable implements Runnable {
    998         private OverflowPopup mPopup;
    999 
   1000         public OpenOverflowRunnable(OverflowPopup popup) {
   1001             mPopup = popup;
   1002         }
   1003 
   1004         public void run() {
   1005             if (mMenu != null) {
   1006                 mMenu.changeMenuMode();
   1007             }
   1008             final View menuView = (View) mMenuView;
   1009             if (menuView != null && menuView.getWindowToken() != null && mPopup.tryShow()) {
   1010                 mOverflowPopup = mPopup;
   1011             }
   1012             mPostedOpenRunnable = null;
   1013         }
   1014     }
   1015 
   1016     private class ActionMenuPopupCallback extends ActionMenuItemView.PopupCallback {
   1017         @Override
   1018         public ShowableListMenu getPopup() {
   1019             return mActionButtonPopup != null ? mActionButtonPopup.getPopup() : null;
   1020         }
   1021     }
   1022 
   1023     /**
   1024      * This class holds layout information for a menu item. This is used to determine
   1025      * pre- and post-layout information about menu items, which will then be used to
   1026      * determine appropriate item animations.
   1027      */
   1028     private static class MenuItemLayoutInfo {
   1029         View view;
   1030         int left;
   1031         int top;
   1032 
   1033         MenuItemLayoutInfo(View view, boolean preLayout) {
   1034             left = view.getLeft();
   1035             top = view.getTop();
   1036             if (preLayout) {
   1037                 // We track translation for pre-layout because a view might be mid-animation
   1038                 // and we need this information to know where to animate from
   1039                 left += view.getTranslationX();
   1040                 top += view.getTranslationY();
   1041             }
   1042             this.view = view;
   1043         }
   1044     }
   1045 
   1046     /**
   1047      * This class is used to store information about currently-running item animations.
   1048      * This is used when new animations are scheduled to determine whether any existing
   1049      * animations need to be canceled, based on whether the running animations overlap
   1050      * with any new animations. For example, if an item is currently animating from
   1051      * location A to B and another change dictates that it be animated to C, then the current
   1052      * A-B animation will be canceled and a new animation to C will be started.
   1053      */
   1054     private static class ItemAnimationInfo {
   1055         int id;
   1056         MenuItemLayoutInfo menuItemLayoutInfo;
   1057         Animator animator;
   1058         int animType;
   1059         static final int MOVE = 0;
   1060         static final int FADE_IN = 1;
   1061         static final int FADE_OUT = 2;
   1062 
   1063         ItemAnimationInfo(int id, MenuItemLayoutInfo info, Animator anim, int animType) {
   1064             this.id = id;
   1065             menuItemLayoutInfo = info;
   1066             animator = anim;
   1067             this.animType = animType;
   1068         }
   1069     }
   1070 }
   1071