summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/widget/ActionMenuPresenter.java4
-rw-r--r--core/java/android/widget/DropDownListView.java27
-rw-r--r--core/java/android/widget/MenuItemHoverListener.java22
-rw-r--r--core/java/android/widget/MenuPopupWindow.java80
-rw-r--r--core/java/com/android/internal/view/menu/CascadingMenuPopup.java514
-rw-r--r--core/java/com/android/internal/view/menu/MenuBuilder.java33
-rw-r--r--core/java/com/android/internal/view/menu/MenuDialogHelper.java2
-rw-r--r--core/java/com/android/internal/view/menu/MenuPresenter.java6
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);