diff options
author | 2015-11-04 15:22:58 +0000 | |
---|---|---|
committer | 2015-11-04 15:22:58 +0000 | |
commit | 0a1a5e3781acfb2b34d4671ea3ccc3f85f48fb62 (patch) | |
tree | 18409e55d9d0b89fcc4361b88c08385f1ff8c4e3 | |
parent | 78a8df49190210506a6971eeac3dfb5acd7770f1 (diff) | |
parent | 00aa5103e2f71ad3f29f53168e37ef7da8ca03f2 (diff) |
Merge "Reduce CascadingMenuPopup's reliance on internal ListView state"
8 files changed, 421 insertions, 267 deletions
diff --git a/core/java/android/widget/ActionMenuPresenter.java b/core/java/android/widget/ActionMenuPresenter.java index 2dd84e3fbcdc..5eea252a698f 100644 --- a/core/java/android/widget/ActionMenuPresenter.java +++ b/core/java/android/widget/ActionMenuPresenter.java @@ -788,7 +788,7 @@ public class ActionMenuPresenter extends BaseMenuPresenter // Not a submenu, but treat it like one. super.onSubMenuSelected(null); } else { - mMenu.close(false); + mMenu.close(false /* closeAllMenus */); } } @@ -981,7 +981,7 @@ public class ActionMenuPresenter extends BaseMenuPresenter @Override public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { if (menu instanceof SubMenuBuilder) { - ((SubMenuBuilder) menu).getRootMenu().close(false); + menu.getRootMenu().close(false /* closeAllMenus */); } final Callback cb = getCallback(); if (cb != null) { diff --git a/core/java/android/widget/DropDownListView.java b/core/java/android/widget/DropDownListView.java index c869ccbdef58..2fb210124f11 100644 --- a/core/java/android/widget/DropDownListView.java +++ b/core/java/android/widget/DropDownListView.java @@ -127,13 +127,15 @@ public class DropDownListView extends ListView { } @Override - protected boolean shouldShowSelector() { - View selectedView = getSelectedView(); - return selectedView != null && selectedView.isEnabled() || super.shouldShowSelector(); + boolean shouldShowSelector() { + return isHovered() || super.shouldShowSelector(); } @Override public boolean onHoverEvent(MotionEvent ev) { + // Allow the super class to handle hover state management first. + final boolean handled = super.onHoverEvent(ev); + final int action = ev.getActionMasked(); if (action == MotionEvent.ACTION_HOVER_ENTER || action == MotionEvent.ACTION_HOVER_MOVE) { @@ -154,26 +156,11 @@ public class DropDownListView extends ListView { // Do not cancel the selected position if the selection is visible by other reasons. if (!super.shouldShowSelector()) { setSelectedPositionInt(INVALID_POSITION); + setNextSelectedPositionInt(INVALID_POSITION); } } - return super.onHoverEvent(ev); - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - final int x = (int) event.getX(); - final int y = (int) event.getY(); - final int position = pointToPosition(x, y); - if (position == INVALID_POSITION) { - return super.onTouchEvent(event); - } - - if (position != mSelectedPosition) { - setSelectedPositionInt(position); - setNextSelectedPositionInt(position); - } - return super.onTouchEvent(event); + return handled; } /** diff --git a/core/java/android/widget/MenuItemHoverListener.java b/core/java/android/widget/MenuItemHoverListener.java index 87c5c852973e..13f0e6a0f43c 100644 --- a/core/java/android/widget/MenuItemHoverListener.java +++ b/core/java/android/widget/MenuItemHoverListener.java @@ -2,6 +2,9 @@ package android.widget; import com.android.internal.view.menu.MenuBuilder; +import android.annotation.NonNull; +import android.view.MenuItem; + /** * An interface notified when a menu item is hovered. Useful for cases when hover should trigger * some behavior at a higher level, like managing the opening and closing of submenus. @@ -9,5 +12,22 @@ import com.android.internal.view.menu.MenuBuilder; * @hide */ public interface MenuItemHoverListener { - public void onItemHovered(MenuBuilder menu, int position); + /** + * Called when hover exits a menu item. + * <p> + * If hover is moving to another item, this method will be called before + * {@link #onItemHoverEnter(MenuBuilder, MenuItem)} for the newly-hovered item. + * + * @param menu the item's parent menu + * @param item the hovered menu item + */ + void onItemHoverExit(@NonNull MenuBuilder menu, @NonNull MenuItem item); + + /** + * Called when hover enters a menu item. + * + * @param menu the item's parent menu + * @param item the hovered menu item + */ + void onItemHoverEnter(@NonNull MenuBuilder menu, @NonNull MenuItem item); } diff --git a/core/java/android/widget/MenuPopupWindow.java b/core/java/android/widget/MenuPopupWindow.java index 1fb62d0fa6c9..85e26d0c6837 100644 --- a/core/java/android/widget/MenuPopupWindow.java +++ b/core/java/android/widget/MenuPopupWindow.java @@ -16,12 +16,14 @@ package android.widget; +import android.annotation.NonNull; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; import android.transition.Transition; import android.util.AttributeSet; import android.view.KeyEvent; +import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; @@ -72,10 +74,18 @@ public class MenuPopupWindow extends ListPopupWindow implements MenuItemHoverLis } @Override - public void onItemHovered(MenuBuilder menu, int position) { + public void onItemHoverEnter(@NonNull MenuBuilder menu, @NonNull MenuItem item) { // Forward up the chain if (mHoverListener != null) { - mHoverListener.onItemHovered(menu, position); + mHoverListener.onItemHoverEnter(menu, item); + } + } + + @Override + public void onItemHoverExit(@NonNull MenuBuilder menu, @NonNull MenuItem item) { + // Forward up the chain + if (mHoverListener != null) { + mHoverListener.onItemHoverExit(menu, item); } } @@ -87,6 +97,7 @@ public class MenuPopupWindow extends ListPopupWindow implements MenuItemHoverLis final int mRetreatKey; private MenuItemHoverListener mHoverListener; + private MenuItem mHoveredMenuItem; public MenuDropDownListView(Context context, boolean hijackFocus) { super(context, hijackFocus); @@ -115,8 +126,7 @@ public class MenuPopupWindow extends ListPopupWindow implements MenuItemHoverLis public boolean onKeyDown(int keyCode, KeyEvent event) { ListMenuItemView selectedItem = (ListMenuItemView) getSelectedView(); if (selectedItem != null && keyCode == mAdvanceKey) { - if (selectedItem.isEnabled() && - ((ListMenuItemView) selectedItem).getItemData().hasSubMenu()) { + if (selectedItem.isEnabled() && selectedItem.getItemData().hasSubMenu()) { performItemClick( selectedItem, getSelectedItemPosition(), @@ -127,7 +137,8 @@ public class MenuPopupWindow extends ListPopupWindow implements MenuItemHoverLis setSelectedPositionInt(INVALID_POSITION); setNextSelectedPositionInt(INVALID_POSITION); - ((MenuAdapter) getAdapter()).getAdapterMenu().close(); + // Close only the top-level menu. + ((MenuAdapter) getAdapter()).getAdapterMenu().close(false /* closeAllMenus */); return true; } return super.onKeyDown(keyCode, event); @@ -135,36 +146,49 @@ public class MenuPopupWindow extends ListPopupWindow implements MenuItemHoverLis @Override public boolean onHoverEvent(MotionEvent ev) { - boolean dispatchHover = false; - final int position = pointToPosition((int) ev.getX(), (int) ev.getY()); - - final int action = ev.getActionMasked(); - if (action == MotionEvent.ACTION_HOVER_ENTER - || action == MotionEvent.ACTION_HOVER_MOVE) { - if (position != INVALID_POSITION && position != mSelectedPosition) { - final View hoveredItem = getChildAt(position - getFirstVisiblePosition()); - if (hoveredItem.isEnabled()) { - dispatchHover = true; - } - } - } - - boolean superVal = super.onHoverEvent(ev); - - if (dispatchHover && mHoverListener != null) { - ListAdapter adapter = getAdapter(); - MenuAdapter menuAdapter; + // Dispatch any changes in hovered item index to the listener. + if (mHoverListener != null) { + // The adapter may be wrapped. Adjust the index if necessary. + final int headersCount; + final MenuAdapter menuAdapter; + final ListAdapter adapter = getAdapter(); if (adapter instanceof HeaderViewListAdapter) { - menuAdapter = (MenuAdapter) ((HeaderViewListAdapter) adapter) - .getWrappedAdapter(); + final HeaderViewListAdapter headerAdapter = (HeaderViewListAdapter) adapter; + headersCount = headerAdapter.getHeadersCount(); + menuAdapter = (MenuAdapter) headerAdapter.getWrappedAdapter(); } else { + headersCount = 0; menuAdapter = (MenuAdapter) adapter; } - mHoverListener.onItemHovered(menuAdapter.getAdapterMenu(), position); + // Find the menu item for the view at the event coordinates. + MenuItem menuItem = null; + if (ev.getAction() != MotionEvent.ACTION_HOVER_EXIT) { + final int position = pointToPosition((int) ev.getX(), (int) ev.getY()); + if (position != INVALID_POSITION) { + final int itemPosition = position - headersCount; + if (itemPosition >= 0 && itemPosition < menuAdapter.getCount()) { + menuItem = menuAdapter.getItem(itemPosition); + } + } + } + + final MenuItem oldMenuItem = mHoveredMenuItem; + if (oldMenuItem != menuItem) { + final MenuBuilder menu = menuAdapter.getAdapterMenu(); + if (oldMenuItem != null) { + mHoverListener.onItemHoverExit(menu, oldMenuItem); + } + + mHoveredMenuItem = menuItem; + + if (menuItem != null) { + mHoverListener.onItemHoverEnter(menu, menuItem); + } + } } - return superVal; + return super.onHoverEvent(ev); } } }
\ No newline at end of file diff --git a/core/java/com/android/internal/view/menu/CascadingMenuPopup.java b/core/java/com/android/internal/view/menu/CascadingMenuPopup.java index 293e2ade7229..e9b8447459cd 100644 --- a/core/java/com/android/internal/view/menu/CascadingMenuPopup.java +++ b/core/java/com/android/internal/view/menu/CascadingMenuPopup.java @@ -5,28 +5,33 @@ import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; +import android.annotation.AttrRes; import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.StyleRes; import android.content.Context; -import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Rect; import android.os.Handler; import android.os.Parcelable; +import android.os.SystemClock; import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewTreeObserver; import android.view.View.OnAttachStateChangeListener; import android.view.View.OnKeyListener; import android.view.ViewTreeObserver.OnGlobalLayoutListener; -import android.widget.DropDownListView; +import android.widget.AbsListView; import android.widget.FrameLayout; -import android.widget.MenuItemHoverListener; +import android.widget.HeaderViewListAdapter; import android.widget.ListAdapter; +import android.widget.MenuItemHoverListener; import android.widget.ListView; import android.widget.MenuPopupWindow; -import android.widget.MenuPopupWindow.MenuDropDownListView; import android.widget.PopupWindow; import android.widget.PopupWindow.OnDismissListener; import android.widget.TextView; @@ -47,6 +52,10 @@ final class CascadingMenuPopup extends MenuPopup implements MenuPresenter, OnKey private static final int HORIZ_POSITION_LEFT = 0; private static final int HORIZ_POSITION_RIGHT = 1; + /** + * Delay between hovering over a menu item with a mouse and receiving + * side-effects (ex. opening a sub-menu or closing unrelated menus). + */ private static final int SUBMENU_TIMEOUT_MS = 200; private final Context mContext; @@ -54,9 +63,14 @@ final class CascadingMenuPopup extends MenuPopup implements MenuPresenter, OnKey private final int mPopupStyleAttr; private final int mPopupStyleRes; private final boolean mOverflowOnly; - private final int mLayoutDirection; private final Handler mSubMenuHoverHandler; + /** + * List of open menus. The first item is the root menu and each + * subsequent item is a direct submenu of the previous item. + */ + private final List<CascadingMenuInfo> mAddedMenus = new ArrayList<>(); + private final OnGlobalLayoutListener mGlobalLayoutListener = new OnGlobalLayoutListener() { @Override public void onGlobalLayout() { @@ -66,8 +80,8 @@ final class CascadingMenuPopup extends MenuPopup implements MenuPresenter, OnKey dismiss(); } else if (isShowing()) { // Recompute window sizes and positions. - for (MenuPopupWindow popup : mPopupWindows) { - popup.show(); + for (CascadingMenuInfo info : mAddedMenus) { + info.window.show(); } } } @@ -94,13 +108,23 @@ final class CascadingMenuPopup extends MenuPopup implements MenuPresenter, OnKey private final MenuItemHoverListener mMenuItemHoverListener = new MenuItemHoverListener() { @Override - public void onItemHovered(MenuBuilder menu, int position) { - int menuIndex = -1; - for (int i = 0; i < mListViews.size(); i++) { - final MenuDropDownListView view = (MenuDropDownListView) mListViews.get(i); - final MenuAdapter adapter = toMenuAdapter(view.getAdapter()); + public void onItemHoverExit(@NonNull MenuBuilder menu, @NonNull MenuItem item) { + // If the mouse moves between two windows, hover enter/exit pairs + // may be received out of order. So, instead of canceling all + // pending runnables, only cancel runnables for the host menu. + mSubMenuHoverHandler.removeCallbacksAndMessages(menu); + } - if (adapter.getAdapterMenu() == menu) { + @Override + public void onItemHoverEnter( + @NonNull final MenuBuilder menu, @NonNull final MenuItem item) { + // Something new was hovered, cancel all scheduled runnables. + mSubMenuHoverHandler.removeCallbacksAndMessages(null); + + // Find the position of the hovered menu within the added menus. + int menuIndex = -1; + for (int i = 0, count = mAddedMenus.size(); i < count; i++) { + if (menu == mAddedMenus.get(i).menu) { menuIndex = i; break; } @@ -110,82 +134,43 @@ final class CascadingMenuPopup extends MenuPopup implements MenuPresenter, OnKey return; } - final MenuDropDownListView view = (MenuDropDownListView) mListViews.get(menuIndex); - final ListMenuItemView selectedItemView = (ListMenuItemView) view.getSelectedView(); - - if (selectedItemView != null && selectedItemView.isEnabled() - && selectedItemView.getItemData().hasSubMenu()) { - // If the currently selected item corresponds to a submenu, schedule to open the - // submenu on a timeout. - - mSubMenuHoverHandler.removeCallbacksAndMessages(null); - mSubMenuHoverHandler.postDelayed(new Runnable() { - @Override - public void run() { - // Make sure the submenu item is still the one selected. - if (view.getSelectedView() == selectedItemView - && selectedItemView.isEnabled() - && selectedItemView.getItemData().hasSubMenu()) { - // Close any other submenus that might be open at the current or - // a deeper level. - int nextIndex = mListViews.indexOf(view) + 1; - if (nextIndex < mListViews.size()) { - MenuAdapter nextSubMenuAdapter = - toMenuAdapter(mListViews.get(nextIndex).getAdapter()); - // Disable exit animation, to prevent overlapping fading out - // submenus. - mPopupWindows.get(nextIndex).setExitTransition(null); - nextSubMenuAdapter.getAdapterMenu().close(); - } - - // Then open the selected submenu. - view.performItemClick( - selectedItemView, - view.getSelectedItemPosition(), - view.getSelectedItemId()); - } + final CascadingMenuInfo nextInfo; + final int nextIndex = menuIndex + 1; + if (nextIndex < mAddedMenus.size()) { + nextInfo = mAddedMenus.get(nextIndex); + } else { + nextInfo = null; + } + + final Runnable runnable = new Runnable() { + @Override + public void run() { + // Close any other submenus that might be open at the + // current or a deeper level. + if (nextInfo != null) { + // Disable exit animations to prevent overlapping + // fading out submenus. + mShouldCloseImmediately = true; + nextInfo.menu.close(false /* closeAllMenus */); + mShouldCloseImmediately = false; } - }, SUBMENU_TIMEOUT_MS); - } else if (menuIndex + 1 < mListViews.size()) { - // If the currently selected item does NOT corresponds to a submenu, check if there - // is a submenu already open that is one level deeper. If so, schedule to close it - // on a timeout. - - final MenuDropDownListView nextView = - (MenuDropDownListView) mListViews.get(menuIndex + 1); - final MenuAdapter nextAdapter = toMenuAdapter(nextView.getAdapter()); - - mSubMenuHoverHandler.removeCallbacksAndMessages(null); - mSubMenuHoverHandler.postDelayed(new Runnable() { - @Override - public void run() { - // Make sure the menu wasn't already closed by something else and that - // it wasn't re-hovered by the user since this was scheduled. - int nextMenuIndex = mListViews.indexOf(nextView); - - if (nextMenuIndex != -1 && nextView.getSelectedView() == null) { - // Disable exit animation, to prevent overlapping fading out submenus. - for (int i = nextMenuIndex; i < mPopupWindows.size(); i++) { - final MenuPopupWindow popupWindow = mPopupWindows.get(i); - popupWindow.setExitTransition(null); - popupWindow.setAnimationStyle(0); - } - nextAdapter.getAdapterMenu().close(); - } + + // Then open the selected submenu, if there is one. + if (item.isEnabled() && item.hasSubMenu()) { + menu.performItemAction(item, 0); } - }, SUBMENU_TIMEOUT_MS); - } + } + }; + final long uptimeMillis = SystemClock.uptimeMillis() + SUBMENU_TIMEOUT_MS; + mSubMenuHoverHandler.postAtTime(runnable, menu, uptimeMillis); } }; + private int mRawDropDownGravity = Gravity.NO_GRAVITY; private int mDropDownGravity = Gravity.NO_GRAVITY; private View mAnchorView; private View mShownAnchorView; - private List<DropDownListView> mListViews; - private List<MenuPopupWindow> mPopupWindows; private int mLastPosition; - private List<Integer> mPositions; - private List<int[]> mOffsets; private int mInitXOffset; private int mInitYOffset; private boolean mForceShowIcon; @@ -194,13 +179,16 @@ final class CascadingMenuPopup extends MenuPopup implements MenuPresenter, OnKey private ViewTreeObserver mTreeObserver; private PopupWindow.OnDismissListener mOnDismissListener; + /** Whether popup menus should disable exit animations when closing. */ + private boolean mShouldCloseImmediately; + /** * Initializes a new cascading-capable menu popup. * - * @param parent A parent view to get the {@link android.view.View#getWindowToken()} token from. + * @param anchor A parent view to get the {@link android.view.View#getWindowToken()} token from. */ - public CascadingMenuPopup(Context context, View anchor, int popupStyleAttr, - int popupStyleRes, boolean overflowOnly) { + public CascadingMenuPopup(@NonNull Context context, @NonNull View anchor, + @AttrRes int popupStyleAttr, @StyleRes int popupStyleRes, boolean overflowOnly) { mContext = Preconditions.checkNotNull(context); mAnchorView = Preconditions.checkNotNull(anchor); mPopupStyleAttr = popupStyleAttr; @@ -208,18 +196,12 @@ final class CascadingMenuPopup extends MenuPopup implements MenuPresenter, OnKey mOverflowOnly = overflowOnly; mForceShowIcon = false; + mLastPosition = getInitialMenuPosition(); final Resources res = context.getResources(); - final Configuration config = res.getConfiguration(); - mLayoutDirection = config.getLayoutDirection(); - mLastPosition = getInitialMenuPosition(); mMenuMaxWidth = Math.max(res.getDisplayMetrics().widthPixels / 2, res.getDimensionPixelSize(com.android.internal.R.dimen.config_prefDialogWidth)); - mPopupWindows = new ArrayList<MenuPopupWindow>(); - mListViews = new ArrayList<DropDownListView>(); - mOffsets = new ArrayList<int[]>(); - mPositions = new ArrayList<Integer>(); mSubMenuHoverHandler = new Handler(); } @@ -246,28 +228,28 @@ final class CascadingMenuPopup extends MenuPopup implements MenuPresenter, OnKey return; } - // Show any menus that have been added via #addMenu(MenuBuilder) but which have not yet been - // shown. - // In a typical use case, #addMenu(MenuBuilder) would be called once, followed by a call to - // this #show() method -- which would actually show the popup on the screen. - for (int i = 0; i < mPopupWindows.size(); i++) { - MenuPopupWindow popupWindow = mPopupWindows.get(i); + // Show any menus that have been added via #addMenu(MenuBuilder) but + // which have not yet been shown. In a typical use case, + // #addMenu(MenuBuilder) would be called once, followed by a call to + // this #show() method -- which would actually show the popup on the + // screen. + for (int i = 0, count = mAddedMenus.size(); i < count; i++) { + final CascadingMenuInfo info = mAddedMenus.get(i); + final MenuPopupWindow popupWindow = info.window; popupWindow.show(); - DropDownListView listView = (DropDownListView) popupWindow.getListView(); - mListViews.add(listView); - MenuBuilder menu = toMenuAdapter(listView.getAdapter()).getAdapterMenu(); + final MenuBuilder menu = info.menu; if (i == 0 && mShowTitle && menu.getHeaderTitle() != null) { FrameLayout titleItemView = (FrameLayout) LayoutInflater.from(mContext).inflate( com.android.internal.R.layout.popup_menu_header_item_layout, - listView, + info.getListView(), false); TextView titleView = (TextView) titleItemView.findViewById( com.android.internal.R.id.title); titleView.setText(menu.getHeaderTitle()); titleItemView.setEnabled(false); - listView.addHeaderView(titleItemView, null, false); + info.getListView().addHeaderView(titleItemView, null, false); // Update to show the title. popupWindow.show(); @@ -287,12 +269,19 @@ final class CascadingMenuPopup extends MenuPopup implements MenuPresenter, OnKey @Override public void dismiss() { - // Need to make another list to avoid a concurrent modification exception, as #onDismiss - // may clear mPopupWindows while we are iterating. - List<MenuPopupWindow> popupWindows = new ArrayList<MenuPopupWindow>(mPopupWindows); - for (MenuPopupWindow popupWindow : popupWindows) { - if (popupWindow != null && popupWindow.isShowing()) { - popupWindow.dismiss(); + // Need to make another list to avoid a concurrent modification + // exception, as #onDismiss may clear mPopupWindows while we are + // iterating. Remove from the last added menu so that the callbacks + // are received in order from foreground to background. + final int length = mAddedMenus.size(); + if (length > 0) { + final CascadingMenuInfo[] addedMenus = + mAddedMenus.toArray(new CascadingMenuInfo[length]); + for (int i = length - 1; i >= 0; i--) { + final CascadingMenuInfo info = addedMenus[i]; + if (info.window.isShowing()) { + info.window.dismiss(); + } } } } @@ -312,7 +301,8 @@ final class CascadingMenuPopup extends MenuPopup implements MenuPresenter, OnKey */ @HorizPosition private int getInitialMenuPosition() { - return mLayoutDirection == View.LAYOUT_DIRECTION_RTL ? HORIZ_POSITION_LEFT : + final int layoutDirection = mAnchorView.getLayoutDirection(); + return layoutDirection == View.LAYOUT_DIRECTION_RTL ? HORIZ_POSITION_LEFT : HORIZ_POSITION_RIGHT; } @@ -325,7 +315,7 @@ final class CascadingMenuPopup extends MenuPopup implements MenuPresenter, OnKey */ @HorizPosition private int getNextMenuPosition(int nextMenuWidth) { - ListView lastListView = mListViews.get(mListViews.size() - 1); + ListView lastListView = mAddedMenus.get(mAddedMenus.size() - 1).getListView(); final int[] screenLocation = new int[2]; lastListView.getLocationOnScreen(screenLocation); @@ -350,76 +340,159 @@ final class CascadingMenuPopup extends MenuPopup implements MenuPresenter, OnKey @Override public void addMenu(MenuBuilder menu) { - boolean addSubMenu = mListViews.size() > 0; - menu.addMenuPresenter(this, mContext); - MenuPopupWindow popupWindow = createPopupWindow(); - - MenuAdapter adapter = new MenuAdapter(menu, LayoutInflater.from(mContext), mOverflowOnly); + final LayoutInflater inflater = LayoutInflater.from(mContext); + final MenuAdapter adapter = new MenuAdapter(menu, inflater, mOverflowOnly); adapter.setForceShowIcon(mForceShowIcon); + final int menuWidth = measureIndividualMenuWidth(adapter, null, mContext, mMenuMaxWidth); + final MenuPopupWindow popupWindow = createPopupWindow(); popupWindow.setAdapter(adapter); + popupWindow.setWidth(menuWidth); + popupWindow.setDropDownGravity(mDropDownGravity); - int menuWidth = measureIndividualMenuWidth(adapter, null, mContext, mMenuMaxWidth); - - int x = 0; - int y = 0; + final CascadingMenuInfo parentInfo; + final View parentView; + if (mAddedMenus.size() > 0) { + parentInfo = mAddedMenus.get(mAddedMenus.size() - 1); + parentView = findParentViewForSubmenu(parentInfo, menu); + } else { + parentInfo = null; + parentView = null; + } - if (addSubMenu) { + final int x; + final int y; + if (parentView != null) { + // This menu is a cascading submenu anchored to a parent view. popupWindow.setTouchModal(false); popupWindow.setEnterTransition(null); - ListView lastListView = mListViews.get(mListViews.size() - 1); - @HorizPosition int nextMenuPosition = getNextMenuPosition(menuWidth); - boolean showOnRight = nextMenuPosition == HORIZ_POSITION_RIGHT; + final @HorizPosition int nextMenuPosition = getNextMenuPosition(menuWidth); + final boolean showOnRight = nextMenuPosition == HORIZ_POSITION_RIGHT; mLastPosition = nextMenuPosition; - int[] lastLocation = new int[2]; - lastListView.getLocationOnScreen(lastLocation); + final int[] tempLocation = new int[2]; - int[] lastOffset = mOffsets.get(mOffsets.size() - 1); + // This popup menu will be positioned relative to the top-left edge + // of the view representing its parent menu. + parentView.getLocationInWindow(tempLocation); + final int parentOffsetLeft = parentInfo.window.getHorizontalOffset() + tempLocation[0]; + final int parentOffsetTop = parentInfo.window.getVerticalOffset() + tempLocation[1]; - // Note: By now, mDropDownGravity is the absolute gravity, so this should work in both - // LTR and RTL. + // By now, mDropDownGravity is the resolved absolute gravity, so + // this should work in both LTR and RTL. if ((mDropDownGravity & Gravity.RIGHT) == Gravity.RIGHT) { if (showOnRight) { - x = lastOffset[0] + menuWidth; + x = parentOffsetLeft + menuWidth; } else { - x = lastOffset[0] - lastListView.getWidth(); + x = parentOffsetLeft - parentView.getWidth(); } } else { if (showOnRight) { - x = lastOffset[0] + lastListView.getWidth(); + x = parentOffsetLeft + parentView.getWidth(); } else { - x = lastOffset[0] - menuWidth; + x = parentOffsetLeft - menuWidth; } } - y = lastOffset[1] + lastListView.getSelectedView().getTop() - - lastListView.getChildAt(0).getTop(); + y = parentOffsetTop; } else { x = mInitXOffset; y = mInitYOffset; } - popupWindow.setWidth(menuWidth); popupWindow.setHorizontalOffset(x); popupWindow.setVerticalOffset(y); - mPopupWindows.add(popupWindow); + + final CascadingMenuInfo menuInfo = new CascadingMenuInfo(popupWindow, menu, mLastPosition); + mAddedMenus.add(menuInfo); // NOTE: This case handles showing submenus once the CascadingMenuPopup has already // been shown via a call to its #show() method. If it hasn't yet been show()n, then // we deliberately do not yet show the popupWindow, as #show() will do that later. if (isShowing()) { popupWindow.show(); - DropDownListView listView = (DropDownListView) popupWindow.getListView(); - mListViews.add(listView); + } + } + + /** + * Returns the menu item within the specified parent menu that owns + * specified submenu. + * + * @param parent the parent menu + * @param submenu the submenu for which the index should be returned + * @return the menu item that owns the submenu, or {@code null} if not + * present + */ + private MenuItem findMenuItemForSubmenu( + @NonNull MenuBuilder parent, @NonNull MenuBuilder submenu) { + for (int i = 0, count = parent.size(); i < count; i++) { + final MenuItem item = parent.getItem(i); + if (item.hasSubMenu() && submenu == item.getSubMenu()) { + return item; + } + } + + return null; + } + + /** + * Attempts to find the view for the menu item that owns the specified + * submenu. + * + * @param parentInfo info for the parent menu + * @param submenu the submenu whose parent view should be obtained + * @return the parent view, or {@code null} if one could not be found + */ + @Nullable + private View findParentViewForSubmenu( + @NonNull CascadingMenuInfo parentInfo, @NonNull MenuBuilder submenu) { + final MenuItem owner = findMenuItemForSubmenu(parentInfo.menu, submenu); + if (owner == null) { + // Couldn't find the submenu owner. + return null; + } + + // The adapter may be wrapped. Adjust the index if necessary. + final int headersCount; + final MenuAdapter menuAdapter; + final ListView listView = parentInfo.getListView(); + final ListAdapter listAdapter = listView.getAdapter(); + if (listAdapter instanceof HeaderViewListAdapter) { + final HeaderViewListAdapter headerAdapter = (HeaderViewListAdapter) listAdapter; + headersCount = headerAdapter.getHeadersCount(); + menuAdapter = (MenuAdapter) headerAdapter.getWrappedAdapter(); + } else { + headersCount = 0; + menuAdapter = (MenuAdapter) listAdapter; + } + + // Find the index within the menu adapter's data set of the menu item. + int ownerPosition = AbsListView.INVALID_POSITION; + for (int i = 0, count = menuAdapter.getCount(); i < count; i++) { + if (owner == menuAdapter.getItem(i)) { + ownerPosition = i; + break; + } + } + if (ownerPosition == AbsListView.INVALID_POSITION) { + // Couldn't find the owner within the menu adapter. + return null; + } + + // Adjust the index for the adapter used to display views. + ownerPosition += headersCount; + + // Adjust the index for the visible views. + final int ownerViewPosition = ownerPosition - listView.getFirstVisiblePosition(); + if (ownerViewPosition < 0 || ownerViewPosition >= listView.getChildCount()) { + // Not visible on screen. + return null; } - int[] offsets = {x, y}; - mOffsets.add(offsets); - mPositions.add(mLastPosition); + return listView.getChildAt(ownerViewPosition); } /** @@ -427,7 +500,7 @@ final class CascadingMenuPopup extends MenuPopup implements MenuPresenter, OnKey */ @Override public boolean isShowing() { - return mPopupWindows.size() > 0 && mPopupWindows.get(0).isShowing(); + return mAddedMenus.size() > 0 && mAddedMenus.get(0).window.isShowing(); } /** @@ -435,28 +508,28 @@ final class CascadingMenuPopup extends MenuPopup implements MenuPresenter, OnKey */ @Override public void onDismiss() { - int dismissedIndex = -1; - for (int i = 0; i < mPopupWindows.size(); i++) { - if (!mPopupWindows.get(i).isShowing()) { - dismissedIndex = i; + // The dismiss listener doesn't pass the calling window, so walk + // through the stack to figure out which one was just dismissed. + CascadingMenuInfo dismissedInfo = null; + for (int i = 0, count = mAddedMenus.size(); i < count; i++) { + final CascadingMenuInfo info = mAddedMenus.get(i); + if (!info.window.isShowing()) { + dismissedInfo = info; break; } } - if (dismissedIndex != -1) { - for (int i = dismissedIndex; i < mListViews.size(); i++) { - ListView view = mListViews.get(i); - ListAdapter adapter = view.getAdapter(); - MenuAdapter menuAdapter = toMenuAdapter(adapter); - menuAdapter.mAdapterMenu.close(); - } + // Close all menus starting from the dismissed menu, passing false + // since we are manually closing only a subset of windows. + if (dismissedInfo != null) { + dismissedInfo.menu.close(false); } } @Override public void updateMenuView(boolean cleared) { - for (ListView view : mListViews) { - toMenuAdapter(view.getAdapter()).notifyDataSetChanged(); + for (CascadingMenuInfo info : mAddedMenus) { + toMenuAdapter(info.getListView().getAdapter()).notifyDataSetChanged(); } } @@ -468,16 +541,17 @@ final class CascadingMenuPopup extends MenuPopup implements MenuPresenter, OnKey @Override public boolean onSubMenuSelected(SubMenuBuilder subMenu) { // Don't allow double-opening of the same submenu. - for (ListView view : mListViews) { - if (toMenuAdapter(view.getAdapter()).mAdapterMenu.equals(subMenu)) { + for (CascadingMenuInfo info : mAddedMenus) { + if (subMenu == info.menu) { // Just re-focus that one. - view.requestFocus(); + info.getListView().requestFocus(); return true; } } if (subMenu.hasVisibleItems()) { - this.addMenu(subMenu); + addMenu(subMenu); + if (mPresenterCallback != null) { mPresenterCallback.onOpenSubMenu(subMenu); } @@ -486,52 +560,62 @@ final class CascadingMenuPopup extends MenuPopup implements MenuPresenter, OnKey return false; } - @Override - public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { - int menuIndex = -1; - boolean wasSelected = false; + /** + * Finds the index of the specified menu within the list of added menus. + * + * @param menu the menu to find + * @return the index of the menu, or {@code -1} if not present + */ + private int findIndexOfAddedMenu(@NonNull MenuBuilder menu) { + for (int i = 0, count = mAddedMenus.size(); i < count; i++) { + final CascadingMenuInfo info = mAddedMenus.get(i); + if (menu == info.menu) { + return i; + } + } - for (int i = 0; i < mListViews.size(); i++) { - ListView view = mListViews.get(i); - MenuAdapter adapter = toMenuAdapter(view.getAdapter()); + return -1; + } - if (menuIndex == -1 && menu == adapter.mAdapterMenu) { - menuIndex = i; - wasSelected = view.getSelectedView() != null; - } + @Override + public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { + final int menuIndex = findIndexOfAddedMenu(menu); + if (menuIndex < 0) { + return; + } - // Once the menu has been found, remove it and all submenus beneath it from the - // container view. Also remove the presenter. - if (menuIndex != -1) { - adapter.mAdapterMenu.removeMenuPresenter(this); - } + // Recursively close descendant menus. + final int nextMenuIndex = menuIndex + 1; + if (nextMenuIndex < mAddedMenus.size()) { + final CascadingMenuInfo childInfo = mAddedMenus.get(nextMenuIndex); + childInfo.menu.close(false /* closeAllMenus */); } - // Then, actually remove the views for these [sub]menu(s) from our list of views. - if (menuIndex != -1) { - for (int i = menuIndex; i < mPopupWindows.size(); i++) { - mPopupWindows.get(i).dismiss(); - } - mPopupWindows.subList(menuIndex, mPopupWindows.size()).clear(); - mListViews.subList(menuIndex, mListViews.size()).clear(); - mOffsets.subList(menuIndex, mOffsets.size()).clear(); + // Close the target menu. + final CascadingMenuInfo info = mAddedMenus.remove(menuIndex); + info.menu.removeMenuPresenter(this); + if (mShouldCloseImmediately) { + // Disable all exit animations. + info.window.setExitTransition(null); + info.window.setAnimationStyle(0); + } + info.window.dismiss(); - mPositions.subList(menuIndex, mPositions.size()).clear(); - if (mPositions.size() > 0) { - mLastPosition = mPositions.get(mPositions.size() - 1); - } else { - mLastPosition = getInitialMenuPosition(); - } + final int count = mAddedMenus.size(); + if (count > 0) { + mLastPosition = mAddedMenus.get(count - 1).position; + } else { + mLastPosition = getInitialMenuPosition(); } - if (mListViews.size() == 0 || wasSelected) { + if (count == 0) { + // This was the last window. Clean up. dismiss(); + if (mPresenterCallback != null) { - mPresenterCallback.onCloseMenu(menu, allMenusAreClosing); + mPresenterCallback.onCloseMenu(menu, true); } - } - if (mPopupWindows.size() == 0) { if (mTreeObserver != null) { if (mTreeObserver.isAlive()) { mTreeObserver.removeGlobalOnLayoutListener(mGlobalLayoutListener); @@ -539,9 +623,16 @@ final class CascadingMenuPopup extends MenuPopup implements MenuPresenter, OnKey mTreeObserver = null; } mShownAnchorView.removeOnAttachStateChangeListener(mAttachStateChangeListener); - // If every [sub]menu was dismissed, that means the whole thing was dismissed, so notify - // the owner. + + // If every [sub]menu was dismissed, that means the whole thing was + // dismissed, so notify the owner. mOnDismissListener.onDismiss(); + } else if (allMenusAreClosing) { + // Close all menus starting from the root. This will recursively + // close any remaining menus, so we don't need to propagate the + // "closeAllMenus" flag. The last window will clean up. + final CascadingMenuInfo rootInfo = mAddedMenus.get(0); + rootInfo.menu.close(false /* closeAllMenus */); } } @@ -561,12 +652,22 @@ final class CascadingMenuPopup extends MenuPopup implements MenuPresenter, OnKey @Override public void setGravity(int dropDownGravity) { - mDropDownGravity = Gravity.getAbsoluteGravity(dropDownGravity, mLayoutDirection); + if (mRawDropDownGravity != dropDownGravity) { + mRawDropDownGravity = dropDownGravity; + mDropDownGravity = Gravity.getAbsoluteGravity( + dropDownGravity, mAnchorView.getLayoutDirection()); + } } @Override - public void setAnchorView(View anchor) { - mAnchorView = anchor; + public void setAnchorView(@NonNull View anchor) { + if (mAnchorView != anchor) { + mAnchorView = anchor; + + // Gravity resolution may have changed, update from raw gravity. + mDropDownGravity = Gravity.getAbsoluteGravity( + mRawDropDownGravity, mAnchorView.getLayoutDirection()); + } } @Override @@ -576,7 +677,7 @@ final class CascadingMenuPopup extends MenuPopup implements MenuPresenter, OnKey @Override public ListView getListView() { - return mListViews.size() > 0 ? mListViews.get(mListViews.size() - 1) : null; + return mAddedMenus.isEmpty() ? null : mAddedMenus.get(mAddedMenus.size() - 1).getListView(); } @Override @@ -593,4 +694,21 @@ final class CascadingMenuPopup extends MenuPopup implements MenuPresenter, OnKey public void setShowTitle(boolean showTitle) { mShowTitle = showTitle; } + + private static class CascadingMenuInfo { + public final MenuPopupWindow window; + public final MenuBuilder menu; + public final int position; + + public CascadingMenuInfo(@NonNull MenuPopupWindow window, @NonNull MenuBuilder menu, + int position) { + this.window = window; + this.menu = menu; + this.position = position; + } + + public ListView getListView() { + return window.getListView(); + } + } }
\ No newline at end of file diff --git a/core/java/com/android/internal/view/menu/MenuBuilder.java b/core/java/com/android/internal/view/menu/MenuBuilder.java index 167392874173..465d775508e6 100644 --- a/core/java/com/android/internal/view/menu/MenuBuilder.java +++ b/core/java/com/android/internal/view/menu/MenuBuilder.java @@ -794,7 +794,7 @@ public class MenuBuilder implements Menu { } if ((flags & FLAG_ALWAYS_PERFORM_CLOSE) != 0) { - close(true); + close(true /* closeAllMenus */); } return handled; @@ -910,10 +910,12 @@ public class MenuBuilder implements Menu { final boolean providerHasSubMenu = provider != null && provider.hasSubMenu(); if (itemImpl.hasCollapsibleActionView()) { invoked |= itemImpl.expandActionView(); - if (invoked) close(true); + if (invoked) { + close(true /* closeAllMenus */); + } } else if (itemImpl.hasSubMenu() || providerHasSubMenu) { if (!mShowCascadingMenus) { - close(false); + close(false /* closeAllMenus */); } if (!itemImpl.hasSubMenu()) { @@ -925,10 +927,12 @@ public class MenuBuilder implements Menu { provider.onPrepareSubMenu(subMenu); } invoked |= dispatchSubMenuSelected(subMenu, preferredPresenter); - if (!invoked) close(true); + if (!invoked) { + close(true /* closeAllMenus */); + } } else { if ((flags & FLAG_PERFORM_NO_CLOSE) == 0) { - close(true); + close(true /* closeAllMenus */); } } @@ -936,15 +940,14 @@ public class MenuBuilder implements Menu { } /** - * Closes the visible menu. - * - * @param allMenusAreClosing Whether the menus are completely closing (true), - * or whether there is another menu coming in this menu's place - * (false). For example, if the menu is closing because a - * sub menu is about to be shown, <var>allMenusAreClosing</var> - * is false. + * Closes the menu. + * + * @param closeAllMenus {@code true} if all displayed menus and submenus + * should be completely closed (as when a menu item is + * selected) or {@code false} if only this menu should + * be closed */ - public final void close(boolean allMenusAreClosing) { + public final void close(boolean closeAllMenus) { if (mIsClosing) return; mIsClosing = true; @@ -953,7 +956,7 @@ public class MenuBuilder implements Menu { if (presenter == null) { mPresenters.remove(ref); } else { - presenter.onCloseMenu(this, allMenusAreClosing); + presenter.onCloseMenu(this, closeAllMenus); } } mIsClosing = false; @@ -961,7 +964,7 @@ public class MenuBuilder implements Menu { /** {@inheritDoc} */ public void close() { - close(true); + close(true /* closeAllMenus */); } /** diff --git a/core/java/com/android/internal/view/menu/MenuDialogHelper.java b/core/java/com/android/internal/view/menu/MenuDialogHelper.java index 5c8e057a9705..b9e0e40cfbf2 100644 --- a/core/java/com/android/internal/view/menu/MenuDialogHelper.java +++ b/core/java/com/android/internal/view/menu/MenuDialogHelper.java @@ -111,7 +111,7 @@ public class MenuDialogHelper implements DialogInterface.OnKeyListener, if (decor != null) { KeyEvent.DispatcherState ds = decor.getKeyDispatcherState(); if (ds != null && ds.isTracking(event)) { - mMenu.close(true); + mMenu.close(true /* closeAllMenus */); dialog.dismiss(); return true; } diff --git a/core/java/com/android/internal/view/menu/MenuPresenter.java b/core/java/com/android/internal/view/menu/MenuPresenter.java index f207b9834885..c847c15607e1 100644 --- a/core/java/com/android/internal/view/menu/MenuPresenter.java +++ b/core/java/com/android/internal/view/menu/MenuPresenter.java @@ -97,8 +97,10 @@ public interface MenuPresenter { * closing. Presenter implementations should close the representation * of the menu indicated as necessary and notify a registered callback. * - * @param menu Menu or submenu that is closing. - * @param allMenusAreClosing True if all associated menus are closing. + * @param menu the menu or submenu that is closing + * @param allMenusAreClosing {@code true} if all displayed menus and + * submenus are closing, {@code false} if only + * the specified menu is closing */ public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing); |