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