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