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