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.support.v7.internal.view.menu; 18 19import android.content.Context; 20import android.content.DialogInterface; 21import android.content.res.Configuration; 22import android.content.res.Resources; 23import android.os.Parcel; 24import android.os.Parcelable; 25import android.support.v7.appcompat.R; 26import android.support.v7.internal.view.ActionBarPolicy; 27import android.support.v4.view.ActionProvider; 28import android.support.v7.internal.view.menu.ActionMenuView.ActionMenuChildView; 29import android.util.SparseBooleanArray; 30import android.view.MenuItem; 31import android.view.SoundEffectConstants; 32import android.view.View; 33import android.view.View.MeasureSpec; 34import android.view.ViewGroup; 35import android.widget.ImageButton; 36 37import java.util.ArrayList; 38 39/** 40 * MenuPresenter for building action menus as seen in the action bar and action modes. 41 * 42 * @hide 43 */ 44public class ActionMenuPresenter extends BaseMenuPresenter 45 implements ActionProvider.SubUiVisibilityListener { 46 47 private static final String TAG = "ActionMenuPresenter"; 48 49 private View mOverflowButton; 50 private boolean mReserveOverflow; 51 private boolean mReserveOverflowSet; 52 private int mWidthLimit; 53 private int mActionItemWidthLimit; 54 private int mMaxItems; 55 private boolean mMaxItemsSet; 56 private boolean mStrictWidthLimit; 57 private boolean mWidthLimitSet; 58 private boolean mExpandedActionViewsExclusive; 59 60 private int mMinCellSize; 61 62 // Group IDs that have been added as actions - used temporarily, allocated here for reuse. 63 private final SparseBooleanArray mActionButtonGroups = new SparseBooleanArray(); 64 65 private View mScrapActionButtonView; 66 67 private OverflowPopup mOverflowPopup; 68 private ActionButtonSubmenu mActionButtonPopup; 69 70 private OpenOverflowRunnable mPostedOpenRunnable; 71 72 final PopupPresenterCallback mPopupPresenterCallback = new PopupPresenterCallback(); 73 int mOpenSubMenuId; 74 75 public ActionMenuPresenter(Context context) { 76 super(context, R.layout.abc_action_menu_layout, R.layout.abc_action_menu_item_layout); 77 } 78 79 @Override 80 public void initForMenu(Context context, MenuBuilder menu) { 81 super.initForMenu(context, menu); 82 83 final Resources res = context.getResources(); 84 85 final ActionBarPolicy abp = ActionBarPolicy.get(context); 86 if (!mReserveOverflowSet) { 87 mReserveOverflow = abp.showsOverflowMenuButton(); 88 } 89 90 if (!mWidthLimitSet) { 91 mWidthLimit = abp.getEmbeddedMenuWidthLimit(); 92 } 93 94 // Measure for initial configuration 95 if (!mMaxItemsSet) { 96 mMaxItems = abp.getMaxActionButtons(); 97 } 98 99 int width = mWidthLimit; 100 if (mReserveOverflow) { 101 if (mOverflowButton == null) { 102 mOverflowButton = new OverflowMenuButton(mSystemContext); 103 final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 104 mOverflowButton.measure(spec, spec); 105 } 106 width -= mOverflowButton.getMeasuredWidth(); 107 } else { 108 mOverflowButton = null; 109 } 110 111 mActionItemWidthLimit = width; 112 113 mMinCellSize = (int) (ActionMenuView.MIN_CELL_SIZE * res.getDisplayMetrics().density); 114 115 // Drop a scrap view as it may no longer reflect the proper context/config. 116 mScrapActionButtonView = null; 117 } 118 119 public void onConfigurationChanged(Configuration newConfig) { 120 if (!mMaxItemsSet) { 121 mMaxItems = mContext.getResources().getInteger( 122 R.integer.abc_max_action_buttons); 123 } 124 if (mMenu != null) { 125 mMenu.onItemsChanged(true); 126 } 127 } 128 129 public void setWidthLimit(int width, boolean strict) { 130 mWidthLimit = width; 131 mStrictWidthLimit = strict; 132 mWidthLimitSet = true; 133 } 134 135 public void setReserveOverflow(boolean reserveOverflow) { 136 mReserveOverflow = reserveOverflow; 137 mReserveOverflowSet = true; 138 } 139 140 public void setItemLimit(int itemCount) { 141 mMaxItems = itemCount; 142 mMaxItemsSet = true; 143 } 144 145 public void setExpandedActionViewsExclusive(boolean isExclusive) { 146 mExpandedActionViewsExclusive = isExclusive; 147 } 148 149 @Override 150 public MenuView getMenuView(ViewGroup root) { 151 MenuView result = super.getMenuView(root); 152 ((ActionMenuView) result).setPresenter(this); 153 return result; 154 } 155 156 @Override 157 public View getItemView(MenuItemImpl item, View convertView, ViewGroup parent) { 158 View actionView = item.getActionView(); 159 if (actionView == null || item.hasCollapsibleActionView()) { 160 if (!(convertView instanceof ActionMenuItemView)) { 161 convertView = null; 162 } 163 actionView = super.getItemView(item, convertView, parent); 164 } 165 actionView.setVisibility(item.isActionViewExpanded() ? View.GONE : View.VISIBLE); 166 167 final ActionMenuView menuParent = (ActionMenuView) parent; 168 final ViewGroup.LayoutParams lp = actionView.getLayoutParams(); 169 if (!menuParent.checkLayoutParams(lp)) { 170 actionView.setLayoutParams(menuParent.generateLayoutParams(lp)); 171 } 172 return actionView; 173 } 174 175 @Override 176 public void bindItemView(MenuItemImpl item, MenuView.ItemView itemView) { 177 itemView.initialize(item, 0); 178 179 final ActionMenuView menuView = (ActionMenuView) mMenuView; 180 ActionMenuItemView actionItemView = (ActionMenuItemView) itemView; 181 actionItemView.setItemInvoker(menuView); 182 } 183 184 @Override 185 public boolean shouldIncludeItem(int childIndex, MenuItemImpl item) { 186 return item.isActionButton(); 187 } 188 189 @Override 190 public void updateMenuView(boolean cleared) { 191 super.updateMenuView(cleared); 192 193 if (mMenuView == null) { 194 return; 195 } 196 197 if (mMenu != null) { 198 final ArrayList<MenuItemImpl> actionItems = mMenu.getActionItems(); 199 final int count = actionItems.size(); 200 for (int i = 0; i < count; i++) { 201 final ActionProvider provider = actionItems.get(i).getSupportActionProvider(); 202 if (provider != null) { 203 provider.setSubUiVisibilityListener(this); 204 } 205 } 206 } 207 208 final ArrayList<MenuItemImpl> nonActionItems = mMenu != null ? 209 mMenu.getNonActionItems() : null; 210 211 boolean hasOverflow = false; 212 if (mReserveOverflow && nonActionItems != null) { 213 final int count = nonActionItems.size(); 214 if (count == 1) { 215 hasOverflow = !nonActionItems.get(0).isActionViewExpanded(); 216 } else { 217 hasOverflow = count > 0; 218 } 219 } 220 221 if (hasOverflow) { 222 if (mOverflowButton == null) { 223 mOverflowButton = new OverflowMenuButton(mSystemContext); 224 } 225 ViewGroup parent = (ViewGroup) mOverflowButton.getParent(); 226 if (parent != mMenuView) { 227 if (parent != null) { 228 parent.removeView(mOverflowButton); 229 } 230 ActionMenuView menuView = (ActionMenuView) mMenuView; 231 menuView.addView(mOverflowButton, menuView.generateOverflowButtonLayoutParams()); 232 } 233 } else if (mOverflowButton != null && mOverflowButton.getParent() == mMenuView) { 234 ((ViewGroup) mMenuView).removeView(mOverflowButton); 235 } 236 237 ((ActionMenuView) mMenuView).setOverflowReserved(mReserveOverflow); 238 } 239 240 @Override 241 public boolean filterLeftoverView(ViewGroup parent, int childIndex) { 242 if (parent.getChildAt(childIndex) == mOverflowButton) { 243 return false; 244 } 245 return super.filterLeftoverView(parent, childIndex); 246 } 247 248 public boolean onSubMenuSelected(SubMenuBuilder subMenu) { 249 if (!subMenu.hasVisibleItems()) { 250 return false; 251 } 252 253 SubMenuBuilder topSubMenu = subMenu; 254 while (topSubMenu.getParentMenu() != mMenu) { 255 topSubMenu = (SubMenuBuilder) topSubMenu.getParentMenu(); 256 } 257 View anchor = findViewForItem(topSubMenu.getItem()); 258 if (anchor == null) { 259 if (mOverflowButton == null) { 260 return false; 261 } 262 anchor = mOverflowButton; 263 } 264 265 mOpenSubMenuId = subMenu.getItem().getItemId(); 266 mActionButtonPopup = new ActionButtonSubmenu(subMenu); 267 mActionButtonPopup.show(null); 268 super.onSubMenuSelected(subMenu); 269 return true; 270 } 271 272 private View findViewForItem(MenuItem item) { 273 final ViewGroup parent = (ViewGroup) mMenuView; 274 if (parent == null) { 275 return null; 276 } 277 278 final int count = parent.getChildCount(); 279 for (int i = 0; i < count; i++) { 280 final View child = parent.getChildAt(i); 281 if (child instanceof MenuView.ItemView && 282 ((MenuView.ItemView) child).getItemData() == item) { 283 return child; 284 } 285 } 286 return null; 287 } 288 289 /** 290 * Display the overflow menu if one is present. 291 * 292 * @return true if the overflow menu was shown, false otherwise. 293 */ 294 public boolean showOverflowMenu() { 295 if (mReserveOverflow && !isOverflowMenuShowing() && mMenu != null && mMenuView != null && 296 mPostedOpenRunnable == null) { 297 OverflowPopup popup = new OverflowPopup(mContext, mMenu, mOverflowButton, true); 298 mPostedOpenRunnable = new OpenOverflowRunnable(popup); 299 // Post this for later; we might still need a layout for the anchor to be right. 300 ((View) mMenuView).post(mPostedOpenRunnable); 301 302 // ActionMenuPresenter uses null as a callback argument here 303 // to indicate overflow is opening. 304 super.onSubMenuSelected(null); 305 306 return true; 307 } 308 return false; 309 } 310 311 /** 312 * Hide the overflow menu if it is currently showing. 313 * 314 * @return true if the overflow menu was hidden, false otherwise. 315 */ 316 public boolean hideOverflowMenu() { 317 if (mPostedOpenRunnable != null && mMenuView != null) { 318 ((View) mMenuView).removeCallbacks(mPostedOpenRunnable); 319 mPostedOpenRunnable = null; 320 return true; 321 } 322 323 MenuPopupHelper popup = mOverflowPopup; 324 if (popup != null) { 325 popup.dismiss(); 326 return true; 327 } 328 return false; 329 } 330 331 /** 332 * Dismiss all popup menus - overflow and submenus. 333 * 334 * @return true if popups were dismissed, false otherwise. (This can be because none were open.) 335 */ 336 public boolean dismissPopupMenus() { 337 boolean result = hideOverflowMenu(); 338 result |= hideSubMenus(); 339 return result; 340 } 341 342 /** 343 * Dismiss all submenu popups. 344 * 345 * @return true if popups were dismissed, false otherwise. (This can be because none were open.) 346 */ 347 public boolean hideSubMenus() { 348 if (mActionButtonPopup != null) { 349 mActionButtonPopup.dismiss(); 350 return true; 351 } 352 return false; 353 } 354 355 /** 356 * @return true if the overflow menu is currently showing 357 */ 358 public boolean isOverflowMenuShowing() { 359 return mOverflowPopup != null && mOverflowPopup.isShowing(); 360 } 361 362 /** 363 * @return true if space has been reserved in the action menu for an overflow item. 364 */ 365 public boolean isOverflowReserved() { 366 return mReserveOverflow; 367 } 368 369 public boolean flagActionItems() { 370 final ArrayList<MenuItemImpl> visibleItems = mMenu.getVisibleItems(); 371 final int itemsSize = visibleItems.size(); 372 int maxActions = mMaxItems; 373 int widthLimit = mActionItemWidthLimit; 374 final int querySpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 375 final ViewGroup parent = (ViewGroup) mMenuView; 376 377 int requiredItems = 0; 378 int requestedItems = 0; 379 int firstActionWidth = 0; 380 boolean hasOverflow = false; 381 for (int i = 0; i < itemsSize; i++) { 382 MenuItemImpl item = visibleItems.get(i); 383 if (item.requiresActionButton()) { 384 requiredItems++; 385 } else if (item.requestsActionButton()) { 386 requestedItems++; 387 } else { 388 hasOverflow = true; 389 } 390 if (mExpandedActionViewsExclusive && item.isActionViewExpanded()) { 391 // Overflow everything if we have an expanded action view and we're 392 // space constrained. 393 maxActions = 0; 394 } 395 } 396 397 // Reserve a spot for the overflow item if needed. 398 if (mReserveOverflow && 399 (hasOverflow || requiredItems + requestedItems > maxActions)) { 400 maxActions--; 401 } 402 maxActions -= requiredItems; 403 404 final SparseBooleanArray seenGroups = mActionButtonGroups; 405 seenGroups.clear(); 406 407 int cellSize = 0; 408 int cellsRemaining = 0; 409 if (mStrictWidthLimit) { 410 cellsRemaining = widthLimit / mMinCellSize; 411 final int cellSizeRemaining = widthLimit % mMinCellSize; 412 cellSize = mMinCellSize + cellSizeRemaining / cellsRemaining; 413 } 414 415 // Flag as many more requested items as will fit. 416 for (int i = 0; i < itemsSize; i++) { 417 MenuItemImpl item = visibleItems.get(i); 418 419 if (item.requiresActionButton()) { 420 View v = getItemView(item, mScrapActionButtonView, parent); 421 if (mScrapActionButtonView == null) { 422 mScrapActionButtonView = v; 423 } 424 if (mStrictWidthLimit) { 425 cellsRemaining -= ActionMenuView.measureChildForCells(v, 426 cellSize, cellsRemaining, querySpec, 0); 427 } else { 428 v.measure(querySpec, querySpec); 429 } 430 final int measuredWidth = v.getMeasuredWidth(); 431 widthLimit -= measuredWidth; 432 if (firstActionWidth == 0) { 433 firstActionWidth = measuredWidth; 434 } 435 final int groupId = item.getGroupId(); 436 if (groupId != 0) { 437 seenGroups.put(groupId, true); 438 } 439 item.setIsActionButton(true); 440 } else if (item.requestsActionButton()) { 441 // Items in a group with other items that already have an action slot 442 // can break the max actions rule, but not the width limit. 443 final int groupId = item.getGroupId(); 444 final boolean inGroup = seenGroups.get(groupId); 445 boolean isAction = (maxActions > 0 || inGroup) && widthLimit > 0 && 446 (!mStrictWidthLimit || cellsRemaining > 0); 447 448 if (isAction) { 449 View v = getItemView(item, mScrapActionButtonView, parent); 450 if (mScrapActionButtonView == null) { 451 mScrapActionButtonView = v; 452 } 453 if (mStrictWidthLimit) { 454 final int cells = ActionMenuView.measureChildForCells(v, 455 cellSize, cellsRemaining, querySpec, 0); 456 cellsRemaining -= cells; 457 if (cells == 0) { 458 isAction = false; 459 } 460 } else { 461 v.measure(querySpec, querySpec); 462 } 463 final int measuredWidth = v.getMeasuredWidth(); 464 widthLimit -= measuredWidth; 465 if (firstActionWidth == 0) { 466 firstActionWidth = measuredWidth; 467 } 468 469 if (mStrictWidthLimit) { 470 isAction &= widthLimit >= 0; 471 } else { 472 // Did this push the entire first item past the limit? 473 isAction &= widthLimit + firstActionWidth > 0; 474 } 475 } 476 477 if (isAction && groupId != 0) { 478 seenGroups.put(groupId, true); 479 } else if (inGroup) { 480 // We broke the width limit. Demote the whole group, they all overflow now. 481 seenGroups.put(groupId, false); 482 for (int j = 0; j < i; j++) { 483 MenuItemImpl areYouMyGroupie = visibleItems.get(j); 484 if (areYouMyGroupie.getGroupId() == groupId) { 485 // Give back the action slot 486 if (areYouMyGroupie.isActionButton()) { 487 maxActions++; 488 } 489 areYouMyGroupie.setIsActionButton(false); 490 } 491 } 492 } 493 494 if (isAction) { 495 maxActions--; 496 } 497 498 item.setIsActionButton(isAction); 499 } 500 } 501 return true; 502 } 503 504 @Override 505 public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { 506 dismissPopupMenus(); 507 super.onCloseMenu(menu, allMenusAreClosing); 508 } 509 510 @Override 511 public Parcelable onSaveInstanceState() { 512 SavedState state = new SavedState(); 513 state.openSubMenuId = mOpenSubMenuId; 514 return state; 515 } 516 517 @Override 518 public void onRestoreInstanceState(Parcelable state) { 519 SavedState saved = (SavedState) state; 520 if (saved.openSubMenuId > 0) { 521 MenuItem item = mMenu.findItem(saved.openSubMenuId); 522 if (item != null) { 523 SubMenuBuilder subMenu = (SubMenuBuilder) item.getSubMenu(); 524 onSubMenuSelected(subMenu); 525 } 526 } 527 } 528 529 @Override 530 public void onSubUiVisibilityChanged(boolean isVisible) { 531 if (isVisible) { 532 // Not a submenu, but treat it like one. 533 super.onSubMenuSelected(null); 534 } else { 535 mMenu.close(false); 536 } 537 } 538 539 private static class SavedState implements Parcelable { 540 541 public int openSubMenuId; 542 543 SavedState() { 544 } 545 546 SavedState(Parcel in) { 547 openSubMenuId = in.readInt(); 548 } 549 550 @Override 551 public int describeContents() { 552 return 0; 553 } 554 555 @Override 556 public void writeToParcel(Parcel dest, int flags) { 557 dest.writeInt(openSubMenuId); 558 } 559 560 public static final Parcelable.Creator<SavedState> CREATOR 561 = new Parcelable.Creator<SavedState>() { 562 public SavedState createFromParcel(Parcel in) { 563 return new SavedState(in); 564 } 565 566 public SavedState[] newArray(int size) { 567 return new SavedState[size]; 568 } 569 }; 570 } 571 572 private class OverflowMenuButton extends ImageButton implements ActionMenuChildView { 573 574 public OverflowMenuButton(Context context) { 575 super(context, null, R.attr.actionOverflowButtonStyle); 576 577 setClickable(true); 578 setFocusable(true); 579 setVisibility(VISIBLE); 580 setEnabled(true); 581 } 582 583 @Override 584 public boolean performClick() { 585 if (super.performClick()) { 586 return true; 587 } 588 589 playSoundEffect(SoundEffectConstants.CLICK); 590 showOverflowMenu(); 591 return true; 592 } 593 594 public boolean needsDividerBefore() { 595 return false; 596 } 597 598 public boolean needsDividerAfter() { 599 return false; 600 } 601 } 602 603 private class OverflowPopup extends MenuPopupHelper { 604 605 public OverflowPopup(Context context, MenuBuilder menu, View anchorView, 606 boolean overflowOnly) { 607 super(context, menu, anchorView, overflowOnly); 608 setCallback(mPopupPresenterCallback); 609 } 610 611 @Override 612 public void onDismiss() { 613 super.onDismiss(); 614 mMenu.close(); 615 mOverflowPopup = null; 616 } 617 } 618 619 private class ActionButtonSubmenu extends MenuDialogHelper { 620 621 public ActionButtonSubmenu(SubMenuBuilder subMenu) { 622 super(subMenu); 623 setCallback(mPopupPresenterCallback); 624 } 625 626 @Override 627 public void onDismiss(DialogInterface dialog) { 628 super.onDismiss(dialog); 629 mActionButtonPopup = null; 630 mOpenSubMenuId = 0; 631 } 632 } 633 634 private class PopupPresenterCallback implements MenuPresenter.Callback { 635 636 @Override 637 public boolean onOpenSubMenu(MenuBuilder subMenu) { 638 if (subMenu == null) { 639 return false; 640 } 641 642 mOpenSubMenuId = ((SubMenuBuilder) subMenu).getItem().getItemId(); 643 return false; 644 } 645 646 @Override 647 public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { 648 if (menu instanceof SubMenuBuilder) { 649 ((SubMenuBuilder) menu).getRootMenu().close(false); 650 } 651 } 652 } 653 654 private class OpenOverflowRunnable implements Runnable { 655 656 private OverflowPopup mPopup; 657 658 public OpenOverflowRunnable(OverflowPopup popup) { 659 mPopup = popup; 660 } 661 662 public void run() { 663 mMenu.changeMenuMode(); 664 final View menuView = (View) mMenuView; 665 if (menuView != null && menuView.getWindowToken() != null && mPopup.tryShow()) { 666 mOverflowPopup = mPopup; 667 } 668 mPostedOpenRunnable = null; 669 } 670 } 671} 672