1 package com.android.internal.view.menu; 2 3 import android.annotation.AttrRes; 4 import android.annotation.IntDef; 5 import android.annotation.NonNull; 6 import android.annotation.Nullable; 7 import android.annotation.StyleRes; 8 import android.app.AppGlobals; 9 import android.content.Context; 10 import android.content.res.Resources; 11 import android.graphics.Rect; 12 import android.os.Handler; 13 import android.os.Parcelable; 14 import android.os.SystemClock; 15 import android.text.TextFlags; 16 import android.view.Gravity; 17 import android.view.KeyEvent; 18 import android.view.LayoutInflater; 19 import android.view.MenuItem; 20 import android.view.View; 21 import android.view.View.OnAttachStateChangeListener; 22 import android.view.View.OnKeyListener; 23 import android.view.ViewTreeObserver; 24 import android.view.ViewTreeObserver.OnGlobalLayoutListener; 25 import android.widget.AbsListView; 26 import android.widget.FrameLayout; 27 import android.widget.HeaderViewListAdapter; 28 import android.widget.ListAdapter; 29 import android.widget.ListView; 30 import android.widget.MenuItemHoverListener; 31 import android.widget.MenuPopupWindow; 32 import android.widget.PopupWindow; 33 import android.widget.PopupWindow.OnDismissListener; 34 import android.widget.TextView; 35 36 import com.android.internal.R; 37 import com.android.internal.util.Preconditions; 38 39 import java.lang.annotation.Retention; 40 import java.lang.annotation.RetentionPolicy; 41 import java.util.ArrayList; 42 import java.util.List; 43 44 /** 45 * A popup for a menu which will allow multiple submenus to appear in a cascading fashion, side by 46 * side. 47 * @hide 48 */ 49 final class CascadingMenuPopup extends MenuPopup implements MenuPresenter, OnKeyListener, 50 PopupWindow.OnDismissListener { 51 @Retention(RetentionPolicy.SOURCE) 52 @IntDef({HORIZ_POSITION_LEFT, HORIZ_POSITION_RIGHT}) 53 public @interface HorizPosition {} 54 55 private static final int HORIZ_POSITION_LEFT = 0; 56 private static final int HORIZ_POSITION_RIGHT = 1; 57 58 /** 59 * Delay between hovering over a menu item with a mouse and receiving 60 * side-effects (ex. opening a sub-menu or closing unrelated menus). 61 */ 62 private static final int SUBMENU_TIMEOUT_MS = 200; 63 64 private final Context mContext; 65 private final int mMenuMaxWidth; 66 private final int mPopupStyleAttr; 67 private final int mPopupStyleRes; 68 private final boolean mOverflowOnly; 69 private final Handler mSubMenuHoverHandler; 70 71 /** List of menus that were added before this popup was shown. */ 72 private final List<MenuBuilder> mPendingMenus = new ArrayList<>(); 73 74 /** 75 * List of open menus. The first item is the root menu and each 76 * subsequent item is a direct submenu of the previous item. 77 */ 78 private final List<CascadingMenuInfo> mShowingMenus = new ArrayList<>(); 79 80 private final OnGlobalLayoutListener mGlobalLayoutListener = new OnGlobalLayoutListener() { 81 @Override 82 public void onGlobalLayout() { 83 // Only move the popup if it's showing and non-modal. We don't want 84 // to be moving around the only interactive window, since there's a 85 // good chance the user is interacting with it. 86 if (isShowing() && mShowingMenus.size() > 0 87 && !mShowingMenus.get(0).window.isModal()) { 88 final View anchor = mShownAnchorView; 89 if (anchor == null || !anchor.isShown()) { 90 dismiss(); 91 } else { 92 // Recompute window sizes and positions. 93 for (CascadingMenuInfo info : mShowingMenus) { 94 info.window.show(); 95 } 96 } 97 } 98 } 99 }; 100 101 private final OnAttachStateChangeListener mAttachStateChangeListener = 102 new OnAttachStateChangeListener() { 103 @Override 104 public void onViewAttachedToWindow(View v) { 105 } 106 107 @Override 108 public void onViewDetachedFromWindow(View v) { 109 if (mTreeObserver != null) { 110 if (!mTreeObserver.isAlive()) { 111 mTreeObserver = v.getViewTreeObserver(); 112 } 113 mTreeObserver.removeGlobalOnLayoutListener(mGlobalLayoutListener); 114 } 115 v.removeOnAttachStateChangeListener(this); 116 } 117 }; 118 119 private final MenuItemHoverListener mMenuItemHoverListener = new MenuItemHoverListener() { 120 @Override 121 public void onItemHoverExit(@NonNull MenuBuilder menu, @NonNull MenuItem item) { 122 // If the mouse moves between two windows, hover enter/exit pairs 123 // may be received out of order. So, instead of canceling all 124 // pending runnables, only cancel runnables for the host menu. 125 mSubMenuHoverHandler.removeCallbacksAndMessages(menu); 126 } 127 128 @Override 129 public void onItemHoverEnter( 130 @NonNull final MenuBuilder menu, @NonNull final MenuItem item) { 131 // Something new was hovered, cancel all scheduled runnables. 132 mSubMenuHoverHandler.removeCallbacksAndMessages(null); 133 134 // Find the position of the hovered menu within the added menus. 135 int menuIndex = -1; 136 for (int i = 0, count = mShowingMenus.size(); i < count; i++) { 137 if (menu == mShowingMenus.get(i).menu) { 138 menuIndex = i; 139 break; 140 } 141 } 142 143 if (menuIndex == -1) { 144 return; 145 } 146 147 final CascadingMenuInfo nextInfo; 148 final int nextIndex = menuIndex + 1; 149 if (nextIndex < mShowingMenus.size()) { 150 nextInfo = mShowingMenus.get(nextIndex); 151 } else { 152 nextInfo = null; 153 } 154 155 final Runnable runnable = new Runnable() { 156 @Override 157 public void run() { 158 // Close any other submenus that might be open at the 159 // current or a deeper level. 160 if (nextInfo != null) { 161 // Disable exit animations to prevent overlapping 162 // fading out submenus. 163 mShouldCloseImmediately = true; 164 nextInfo.menu.close(false /* closeAllMenus */); 165 mShouldCloseImmediately = false; 166 } 167 168 // Then open the selected submenu, if there is one. 169 if (item.isEnabled() && item.hasSubMenu()) { 170 menu.performItemAction(item, 0); 171 } 172 } 173 }; 174 final long uptimeMillis = SystemClock.uptimeMillis() + SUBMENU_TIMEOUT_MS; 175 mSubMenuHoverHandler.postAtTime(runnable, menu, uptimeMillis); 176 } 177 }; 178 179 private int mRawDropDownGravity = Gravity.NO_GRAVITY; 180 private int mDropDownGravity = Gravity.NO_GRAVITY; 181 private View mAnchorView; 182 private View mShownAnchorView; 183 private int mLastPosition; 184 private boolean mHasXOffset; 185 private boolean mHasYOffset; 186 private int mXOffset; 187 private int mYOffset; 188 private boolean mForceShowIcon; 189 private boolean mShowTitle; 190 private Callback mPresenterCallback; 191 private ViewTreeObserver mTreeObserver; 192 private PopupWindow.OnDismissListener mOnDismissListener; 193 private final int mItemLayout; 194 195 /** Whether popup menus should disable exit animations when closing. */ 196 private boolean mShouldCloseImmediately; 197 198 /** 199 * Initializes a new cascading-capable menu popup. 200 * 201 * @param anchor A parent view to get the {@link android.view.View#getWindowToken()} token from. 202 */ CascadingMenuPopup(@onNull Context context, @NonNull View anchor, @AttrRes int popupStyleAttr, @StyleRes int popupStyleRes, boolean overflowOnly)203 public CascadingMenuPopup(@NonNull Context context, @NonNull View anchor, 204 @AttrRes int popupStyleAttr, @StyleRes int popupStyleRes, boolean overflowOnly) { 205 mContext = Preconditions.checkNotNull(context); 206 mAnchorView = Preconditions.checkNotNull(anchor); 207 mPopupStyleAttr = popupStyleAttr; 208 mPopupStyleRes = popupStyleRes; 209 mOverflowOnly = overflowOnly; 210 211 mForceShowIcon = false; 212 mLastPosition = getInitialMenuPosition(); 213 214 final Resources res = context.getResources(); 215 mMenuMaxWidth = Math.max(res.getDisplayMetrics().widthPixels / 2, 216 res.getDimensionPixelSize(com.android.internal.R.dimen.config_prefDialogWidth)); 217 218 mSubMenuHoverHandler = new Handler(); 219 220 mItemLayout = AppGlobals.getIntCoreSetting( 221 TextFlags.KEY_ENABLE_NEW_CONTEXT_MENU, 222 TextFlags.ENABLE_NEW_CONTEXT_MENU_DEFAULT ? 1 : 0) != 0 223 ? com.android.internal.R.layout.cascading_menu_item_layout_material 224 : com.android.internal.R.layout.cascading_menu_item_layout; 225 } 226 227 @Override setForceShowIcon(boolean forceShow)228 public void setForceShowIcon(boolean forceShow) { 229 mForceShowIcon = forceShow; 230 } 231 createPopupWindow()232 private MenuPopupWindow createPopupWindow() { 233 MenuPopupWindow popupWindow = new MenuPopupWindow( 234 mContext, null, mPopupStyleAttr, mPopupStyleRes); 235 popupWindow.setHoverListener(mMenuItemHoverListener); 236 popupWindow.setOnItemClickListener(this); 237 popupWindow.setOnDismissListener(this); 238 popupWindow.setAnchorView(mAnchorView); 239 popupWindow.setDropDownGravity(mDropDownGravity); 240 popupWindow.setModal(true); 241 popupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); 242 return popupWindow; 243 } 244 245 @Override show()246 public void show() { 247 if (isShowing()) { 248 return; 249 } 250 251 // Display all pending menus. 252 for (MenuBuilder menu : mPendingMenus) { 253 showMenu(menu); 254 } 255 mPendingMenus.clear(); 256 257 mShownAnchorView = mAnchorView; 258 259 if (mShownAnchorView != null) { 260 final boolean addGlobalListener = mTreeObserver == null; 261 mTreeObserver = mShownAnchorView.getViewTreeObserver(); // Refresh to latest 262 if (addGlobalListener) { 263 mTreeObserver.addOnGlobalLayoutListener(mGlobalLayoutListener); 264 } 265 mShownAnchorView.addOnAttachStateChangeListener(mAttachStateChangeListener); 266 } 267 } 268 269 @Override dismiss()270 public void dismiss() { 271 // Need to make another list to avoid a concurrent modification 272 // exception, as #onDismiss may clear mPopupWindows while we are 273 // iterating. Remove from the last added menu so that the callbacks 274 // are received in order from foreground to background. 275 final int length = mShowingMenus.size(); 276 if (length > 0) { 277 final CascadingMenuInfo[] addedMenus = 278 mShowingMenus.toArray(new CascadingMenuInfo[length]); 279 for (int i = length - 1; i >= 0; i--) { 280 final CascadingMenuInfo info = addedMenus[i]; 281 if (info.window.isShowing()) { 282 info.window.dismiss(); 283 } 284 } 285 } 286 } 287 288 @Override onKey(View v, int keyCode, KeyEvent event)289 public boolean onKey(View v, int keyCode, KeyEvent event) { 290 if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_MENU) { 291 dismiss(); 292 return true; 293 } 294 return false; 295 } 296 297 /** 298 * Determines the proper initial menu position for the current LTR/RTL configuration. 299 * @return The initial position. 300 */ 301 @HorizPosition getInitialMenuPosition()302 private int getInitialMenuPosition() { 303 final int layoutDirection = mAnchorView.getLayoutDirection(); 304 return layoutDirection == View.LAYOUT_DIRECTION_RTL ? HORIZ_POSITION_LEFT : 305 HORIZ_POSITION_RIGHT; 306 } 307 308 /** 309 * Determines whether the next submenu (of the given width) should display on the right or on 310 * the left of the most recent menu. 311 * 312 * @param nextMenuWidth Width of the next submenu to display. 313 * @return The position to display it. 314 */ 315 @HorizPosition getNextMenuPosition(int nextMenuWidth)316 private int getNextMenuPosition(int nextMenuWidth) { 317 ListView lastListView = mShowingMenus.get(mShowingMenus.size() - 1).getListView(); 318 319 final int[] screenLocation = new int[2]; 320 lastListView.getLocationOnScreen(screenLocation); 321 322 final Rect displayFrame = new Rect(); 323 mShownAnchorView.getWindowVisibleDisplayFrame(displayFrame); 324 325 if (mLastPosition == HORIZ_POSITION_RIGHT) { 326 final int right = screenLocation[0] + lastListView.getWidth() + nextMenuWidth; 327 if (right > displayFrame.right) { 328 return HORIZ_POSITION_LEFT; 329 } 330 return HORIZ_POSITION_RIGHT; 331 } else { // LEFT 332 final int left = screenLocation[0] - nextMenuWidth; 333 if (left < 0) { 334 return HORIZ_POSITION_RIGHT; 335 } 336 return HORIZ_POSITION_LEFT; 337 } 338 } 339 340 @Override addMenu(MenuBuilder menu)341 public void addMenu(MenuBuilder menu) { 342 menu.addMenuPresenter(this, mContext); 343 344 if (isShowing()) { 345 showMenu(menu); 346 } else { 347 mPendingMenus.add(menu); 348 } 349 } 350 351 /** 352 * Prepares and shows the specified menu immediately. 353 * 354 * @param menu the menu to show 355 */ showMenu(@onNull MenuBuilder menu)356 private void showMenu(@NonNull MenuBuilder menu) { 357 final LayoutInflater inflater = LayoutInflater.from(mContext); 358 final MenuAdapter adapter = new MenuAdapter(menu, inflater, mOverflowOnly, mItemLayout); 359 360 // Apply "force show icon" setting. There are 3 cases: 361 // (1) This is the top level menu and icon spacing is forced. Add spacing. 362 // (2) This is a submenu. Add spacing if any of the visible menu items has an icon. 363 // (3) This is the top level menu and icon spacing isn't forced. Do not add spacing. 364 if (!isShowing() && mForceShowIcon) { 365 // Case 1 366 adapter.setForceShowIcon(true); 367 } else if (isShowing()) { 368 // Case 2 369 adapter.setForceShowIcon(MenuPopup.shouldPreserveIconSpacing(menu)); 370 } 371 // Case 3: Else, don't allow spacing for icons (default behavior; do nothing). 372 373 final int menuWidth = measureIndividualMenuWidth(adapter, null, mContext, mMenuMaxWidth); 374 final MenuPopupWindow popupWindow = createPopupWindow(); 375 popupWindow.setAdapter(adapter); 376 popupWindow.setContentWidth(menuWidth); 377 popupWindow.setDropDownGravity(mDropDownGravity); 378 379 final CascadingMenuInfo parentInfo; 380 final View parentView; 381 if (mShowingMenus.size() > 0) { 382 parentInfo = mShowingMenus.get(mShowingMenus.size() - 1); 383 parentView = findParentViewForSubmenu(parentInfo, menu); 384 } else { 385 parentInfo = null; 386 parentView = null; 387 } 388 389 if (parentView != null) { 390 // This menu is a cascading submenu anchored to a parent view. 391 popupWindow.setAnchorView(parentView); 392 popupWindow.setTouchModal(false); 393 popupWindow.setEnterTransition(null); 394 395 final @HorizPosition int nextMenuPosition = getNextMenuPosition(menuWidth); 396 final boolean showOnRight = nextMenuPosition == HORIZ_POSITION_RIGHT; 397 mLastPosition = nextMenuPosition; 398 399 // Compute the horizontal offset to display the submenu to the right or to the left 400 // of the parent item. 401 // By now, mDropDownGravity is the resolved absolute gravity, so 402 // this should work in both LTR and RTL. 403 final int x; 404 if ((mDropDownGravity & Gravity.RIGHT) == Gravity.RIGHT) { 405 if (showOnRight) { 406 x = menuWidth; 407 } else { 408 x = -parentView.getWidth(); 409 } 410 } else { 411 if (showOnRight) { 412 x = parentView.getWidth(); 413 } else { 414 x = -menuWidth; 415 } 416 } 417 popupWindow.setHorizontalOffset(x); 418 419 // Align with the top edge of the parent view (or the bottom edge when the submenu is 420 // flipped vertically). 421 popupWindow.setOverlapAnchor(true); 422 popupWindow.setVerticalOffset(0); 423 } else { 424 if (mHasXOffset) { 425 popupWindow.setHorizontalOffset(mXOffset); 426 } 427 if (mHasYOffset) { 428 popupWindow.setVerticalOffset(mYOffset); 429 } 430 final Rect epicenterBounds = getEpicenterBounds(); 431 popupWindow.setEpicenterBounds(epicenterBounds); 432 } 433 434 435 final CascadingMenuInfo menuInfo = new CascadingMenuInfo(popupWindow, menu, mLastPosition); 436 mShowingMenus.add(menuInfo); 437 438 popupWindow.show(); 439 440 final ListView listView = popupWindow.getListView(); 441 listView.setOnKeyListener(this); 442 443 // If this is the root menu, show the title if requested. 444 if (parentInfo == null && mShowTitle && menu.getHeaderTitle() != null) { 445 final FrameLayout titleItemView = (FrameLayout) inflater.inflate( 446 R.layout.popup_menu_header_item_layout, listView, false); 447 final TextView titleView = (TextView) titleItemView.findViewById(R.id.title); 448 titleItemView.setEnabled(false); 449 titleView.setText(menu.getHeaderTitle()); 450 listView.addHeaderView(titleItemView, null, false); 451 452 // Show again to update the title. 453 popupWindow.show(); 454 } 455 } 456 457 /** 458 * Returns the menu item within the specified parent menu that owns 459 * specified submenu. 460 * 461 * @param parent the parent menu 462 * @param submenu the submenu for which the index should be returned 463 * @return the menu item that owns the submenu, or {@code null} if not 464 * present 465 */ findMenuItemForSubmenu( @onNull MenuBuilder parent, @NonNull MenuBuilder submenu)466 private MenuItem findMenuItemForSubmenu( 467 @NonNull MenuBuilder parent, @NonNull MenuBuilder submenu) { 468 for (int i = 0, count = parent.size(); i < count; i++) { 469 final MenuItem item = parent.getItem(i); 470 if (item.hasSubMenu() && submenu == item.getSubMenu()) { 471 return item; 472 } 473 } 474 475 return null; 476 } 477 478 /** 479 * Attempts to find the view for the menu item that owns the specified 480 * submenu. 481 * 482 * @param parentInfo info for the parent menu 483 * @param submenu the submenu whose parent view should be obtained 484 * @return the parent view, or {@code null} if one could not be found 485 */ 486 @Nullable findParentViewForSubmenu( @onNull CascadingMenuInfo parentInfo, @NonNull MenuBuilder submenu)487 private View findParentViewForSubmenu( 488 @NonNull CascadingMenuInfo parentInfo, @NonNull MenuBuilder submenu) { 489 final MenuItem owner = findMenuItemForSubmenu(parentInfo.menu, submenu); 490 if (owner == null) { 491 // Couldn't find the submenu owner. 492 return null; 493 } 494 495 // The adapter may be wrapped. Adjust the index if necessary. 496 final int headersCount; 497 final MenuAdapter menuAdapter; 498 final ListView listView = parentInfo.getListView(); 499 final ListAdapter listAdapter = listView.getAdapter(); 500 if (listAdapter instanceof HeaderViewListAdapter) { 501 final HeaderViewListAdapter headerAdapter = (HeaderViewListAdapter) listAdapter; 502 headersCount = headerAdapter.getHeadersCount(); 503 menuAdapter = (MenuAdapter) headerAdapter.getWrappedAdapter(); 504 } else { 505 headersCount = 0; 506 menuAdapter = (MenuAdapter) listAdapter; 507 } 508 509 // Find the index within the menu adapter's data set of the menu item. 510 int ownerPosition = AbsListView.INVALID_POSITION; 511 for (int i = 0, count = menuAdapter.getCount(); i < count; i++) { 512 if (owner == menuAdapter.getItem(i)) { 513 ownerPosition = i; 514 break; 515 } 516 } 517 if (ownerPosition == AbsListView.INVALID_POSITION) { 518 // Couldn't find the owner within the menu adapter. 519 return null; 520 } 521 522 // Adjust the index for the adapter used to display views. 523 ownerPosition += headersCount; 524 525 // Adjust the index for the visible views. 526 final int ownerViewPosition = ownerPosition - listView.getFirstVisiblePosition(); 527 if (ownerViewPosition < 0 || ownerViewPosition >= listView.getChildCount()) { 528 // Not visible on screen. 529 return null; 530 } 531 532 return listView.getChildAt(ownerViewPosition); 533 } 534 535 /** 536 * @return {@code true} if the popup is currently showing, {@code false} otherwise. 537 */ 538 @Override isShowing()539 public boolean isShowing() { 540 return mShowingMenus.size() > 0 && mShowingMenus.get(0).window.isShowing(); 541 } 542 543 /** 544 * Called when one or more of the popup windows was dismissed. 545 */ 546 @Override onDismiss()547 public void onDismiss() { 548 // The dismiss listener doesn't pass the calling window, so walk 549 // through the stack to figure out which one was just dismissed. 550 CascadingMenuInfo dismissedInfo = null; 551 for (int i = 0, count = mShowingMenus.size(); i < count; i++) { 552 final CascadingMenuInfo info = mShowingMenus.get(i); 553 if (!info.window.isShowing()) { 554 dismissedInfo = info; 555 break; 556 } 557 } 558 559 // Close all menus starting from the dismissed menu, passing false 560 // since we are manually closing only a subset of windows. 561 if (dismissedInfo != null) { 562 dismissedInfo.menu.close(false); 563 } 564 } 565 566 @Override updateMenuView(boolean cleared)567 public void updateMenuView(boolean cleared) { 568 for (CascadingMenuInfo info : mShowingMenus) { 569 toMenuAdapter(info.getListView().getAdapter()).notifyDataSetChanged(); 570 } 571 } 572 573 @Override setCallback(Callback cb)574 public void setCallback(Callback cb) { 575 mPresenterCallback = cb; 576 } 577 578 @Override onSubMenuSelected(SubMenuBuilder subMenu)579 public boolean onSubMenuSelected(SubMenuBuilder subMenu) { 580 // Don't allow double-opening of the same submenu. 581 for (CascadingMenuInfo info : mShowingMenus) { 582 if (subMenu == info.menu) { 583 // Just re-focus that one. 584 info.getListView().requestFocus(); 585 return true; 586 } 587 } 588 589 if (subMenu.hasVisibleItems()) { 590 addMenu(subMenu); 591 592 if (mPresenterCallback != null) { 593 mPresenterCallback.onOpenSubMenu(subMenu); 594 } 595 return true; 596 } 597 return false; 598 } 599 600 /** 601 * Finds the index of the specified menu within the list of added menus. 602 * 603 * @param menu the menu to find 604 * @return the index of the menu, or {@code -1} if not present 605 */ findIndexOfAddedMenu(@onNull MenuBuilder menu)606 private int findIndexOfAddedMenu(@NonNull MenuBuilder menu) { 607 for (int i = 0, count = mShowingMenus.size(); i < count; i++) { 608 final CascadingMenuInfo info = mShowingMenus.get(i); 609 if (menu == info.menu) { 610 return i; 611 } 612 } 613 614 return -1; 615 } 616 617 @Override onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing)618 public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { 619 final int menuIndex = findIndexOfAddedMenu(menu); 620 if (menuIndex < 0) { 621 return; 622 } 623 624 // Recursively close descendant menus. 625 final int nextMenuIndex = menuIndex + 1; 626 if (nextMenuIndex < mShowingMenus.size()) { 627 final CascadingMenuInfo childInfo = mShowingMenus.get(nextMenuIndex); 628 childInfo.menu.close(false /* closeAllMenus */); 629 } 630 631 // Close the target menu. 632 final CascadingMenuInfo info = mShowingMenus.remove(menuIndex); 633 info.menu.removeMenuPresenter(this); 634 if (mShouldCloseImmediately) { 635 // Disable all exit animations. 636 info.window.setExitTransition(null); 637 info.window.setAnimationStyle(0); 638 } 639 info.window.dismiss(); 640 641 final int count = mShowingMenus.size(); 642 if (count > 0) { 643 mLastPosition = mShowingMenus.get(count - 1).position; 644 } else { 645 mLastPosition = getInitialMenuPosition(); 646 } 647 648 if (count == 0) { 649 // This was the last window. Clean up. 650 dismiss(); 651 652 if (mPresenterCallback != null) { 653 mPresenterCallback.onCloseMenu(menu, true); 654 } 655 656 if (mTreeObserver != null) { 657 if (mTreeObserver.isAlive()) { 658 mTreeObserver.removeGlobalOnLayoutListener(mGlobalLayoutListener); 659 } 660 mTreeObserver = null; 661 } 662 mShownAnchorView.removeOnAttachStateChangeListener(mAttachStateChangeListener); 663 664 // If every [sub]menu was dismissed, that means the whole thing was 665 // dismissed, so notify the owner. 666 mOnDismissListener.onDismiss(); 667 } else if (allMenusAreClosing) { 668 // Close all menus starting from the root. This will recursively 669 // close any remaining menus, so we don't need to propagate the 670 // "closeAllMenus" flag. The last window will clean up. 671 final CascadingMenuInfo rootInfo = mShowingMenus.get(0); 672 rootInfo.menu.close(false /* closeAllMenus */); 673 } 674 } 675 676 @Override flagActionItems()677 public boolean flagActionItems() { 678 return false; 679 } 680 681 @Override onSaveInstanceState()682 public Parcelable onSaveInstanceState() { 683 return null; 684 } 685 686 @Override onRestoreInstanceState(Parcelable state)687 public void onRestoreInstanceState(Parcelable state) { 688 } 689 690 @Override setGravity(int dropDownGravity)691 public void setGravity(int dropDownGravity) { 692 if (mRawDropDownGravity != dropDownGravity) { 693 mRawDropDownGravity = dropDownGravity; 694 mDropDownGravity = Gravity.getAbsoluteGravity( 695 dropDownGravity, mAnchorView.getLayoutDirection()); 696 } 697 } 698 699 @Override setAnchorView(@onNull View anchor)700 public void setAnchorView(@NonNull View anchor) { 701 if (mAnchorView != anchor) { 702 mAnchorView = anchor; 703 704 // Gravity resolution may have changed, update from raw gravity. 705 mDropDownGravity = Gravity.getAbsoluteGravity( 706 mRawDropDownGravity, mAnchorView.getLayoutDirection()); 707 } 708 } 709 710 @Override setOnDismissListener(OnDismissListener listener)711 public void setOnDismissListener(OnDismissListener listener) { 712 mOnDismissListener = listener; 713 } 714 715 @Override getListView()716 public ListView getListView() { 717 return mShowingMenus.isEmpty() ? null : mShowingMenus.get(mShowingMenus.size() - 1).getListView(); 718 } 719 720 @Override setHorizontalOffset(int x)721 public void setHorizontalOffset(int x) { 722 mHasXOffset = true; 723 mXOffset = x; 724 } 725 726 @Override setVerticalOffset(int y)727 public void setVerticalOffset(int y) { 728 mHasYOffset = true; 729 mYOffset = y; 730 } 731 732 @Override setShowTitle(boolean showTitle)733 public void setShowTitle(boolean showTitle) { 734 mShowTitle = showTitle; 735 } 736 737 private static class CascadingMenuInfo { 738 public final MenuPopupWindow window; 739 public final MenuBuilder menu; 740 public final int position; 741 CascadingMenuInfo(@onNull MenuPopupWindow window, @NonNull MenuBuilder menu, int position)742 public CascadingMenuInfo(@NonNull MenuPopupWindow window, @NonNull MenuBuilder menu, 743 int position) { 744 this.window = window; 745 this.menu = menu; 746 this.position = position; 747 } 748 getListView()749 public ListView getListView() { 750 return window.getListView(); 751 } 752 } 753 } 754