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