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