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