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