• 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 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 
ActionMenuPresenter(Context context)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
initForMenu(@onNull Context context, @Nullable MenuBuilder menu)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 
onConfigurationChanged(Configuration newConfig)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 
setWidthLimit(int width, boolean strict)182     public void setWidthLimit(int width, boolean strict) {
183         mWidthLimit = width;
184         mStrictWidthLimit = strict;
185         mWidthLimitSet = true;
186     }
187 
setReserveOverflow(boolean reserveOverflow)188     public void setReserveOverflow(boolean reserveOverflow) {
189         mReserveOverflow = reserveOverflow;
190         mReserveOverflowSet = true;
191     }
192 
setItemLimit(int itemCount)193     public void setItemLimit(int itemCount) {
194         mMaxItems = itemCount;
195         mMaxItemsSet = true;
196     }
197 
setExpandedActionViewsExclusive(boolean isExclusive)198     public void setExpandedActionViewsExclusive(boolean isExclusive) {
199         mExpandedActionViewsExclusive = isExclusive;
200     }
201 
setOverflowIcon(Drawable icon)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 
getOverflowIcon()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
getMenuView(ViewGroup root)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
getItemView(final MenuItemImpl item, View convertView, ViewGroup parent)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
bindItemView(MenuItemImpl item, MenuView.ItemView itemView)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
shouldIncludeItem(int childIndex, MenuItemImpl item)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      */
computeMenuItemAnimationInfo(boolean preLayout)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      */
runItemAnimations()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      */
setupItemAnimations()428     private void setupItemAnimations() {
429         computeMenuItemAnimationInfo(true);
430         ((View) mMenuView).getViewTreeObserver().
431                 addOnPreDrawListener(mItemAnimationPreDrawListener);
432     }
433 
434     @Override
updateMenuView(boolean cleared)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
filterLeftoverView(ViewGroup parent, int childIndex)488     public boolean filterLeftoverView(ViewGroup parent, int childIndex) {
489         if (parent.getChildAt(childIndex) == mOverflowButton) return false;
490         return super.filterLeftoverView(parent, childIndex);
491     }
492 
onSubMenuSelected(SubMenuBuilder subMenu)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 
findViewForItem(MenuItem item)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      */
showOverflowMenu()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      */
hideOverflowMenu()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      */
dismissPopupMenus()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      */
hideSubMenus()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      */
isOverflowMenuShowing()611     public boolean isOverflowMenuShowing() {
612         return mOverflowPopup != null && mOverflowPopup.isShowing();
613     }
614 
isOverflowMenuShowPending()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      */
isOverflowReserved()622     public boolean isOverflowReserved() {
623         return mReserveOverflow;
624     }
625 
flagActionItems()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
onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing)763     public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
764         dismissPopupMenus();
765         super.onCloseMenu(menu, allMenusAreClosing);
766     }
767 
768     @Override
onSaveInstanceState()769     public Parcelable onSaveInstanceState() {
770         SavedState state = new SavedState();
771         state.openSubMenuId = mOpenSubMenuId;
772         return state;
773     }
774 
775     @Override
onRestoreInstanceState(Parcelable state)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
onSubUiVisibilityChanged(boolean isVisible)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 
setMenuView(ActionMenuView menuView)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 
SavedState()811         SavedState() {
812         }
813 
SavedState(Parcel in)814         SavedState(Parcel in) {
815             openSubMenuId = in.readInt();
816         }
817 
818         @Override
describeContents()819         public int describeContents() {
820             return 0;
821         }
822 
823         @Override
writeToParcel(Parcel dest, int flags)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 {
OverflowMenuButton(Context context)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
performClick()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
needsDividerBefore()892         public boolean needsDividerBefore() {
893             return false;
894         }
895 
896         @Override
needsDividerAfter()897         public boolean needsDividerAfter() {
898             return false;
899         }
900 
901     /** @hide */
902         @Override
onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info)903         public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
904             super.onInitializeAccessibilityNodeInfoInternal(info);
905             info.setCanOpenPopup(true);
906         }
907 
908         @Override
setFrame(int l, int t, int r, int b)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 {
OverflowPopup(Context context, MenuBuilder menu, View anchorView, boolean overflowOnly)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
onDismiss()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 {
ActionButtonSubmenu(Context context, SubMenuBuilder subMenu, View anchorView)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
onDismiss()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
onOpenSubMenu(MenuBuilder subMenu)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
onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing)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 
OpenOverflowRunnable(OverflowPopup popup)1000         public OpenOverflowRunnable(OverflowPopup popup) {
1001             mPopup = popup;
1002         }
1003 
run()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
getPopup()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 
MenuItemLayoutInfo(View view, boolean preLayout)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 
ItemAnimationInfo(int id, MenuItemLayoutInfo info, Animator anim, int animType)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