diff --git a/core/java/android/widget/DropDownListView.java b/core/java/android/widget/DropDownListView.java index bcbafc9af158b..c869ccbdef586 100644 --- a/core/java/android/widget/DropDownListView.java +++ b/core/java/android/widget/DropDownListView.java @@ -132,11 +132,6 @@ public class DropDownListView extends ListView { return selectedView != null && selectedView.isEnabled() || super.shouldShowSelector(); } - protected void clearSelection() { - setSelectedPositionInt(-1); - setNextSelectedPositionInt(-1); - } - @Override public boolean onHoverEvent(MotionEvent ev) { final int action = ev.getActionMasked(); diff --git a/core/java/android/widget/MenuItemHoverListener.java b/core/java/android/widget/MenuItemHoverListener.java new file mode 100644 index 0000000000000..87c5c852973e8 --- /dev/null +++ b/core/java/android/widget/MenuItemHoverListener.java @@ -0,0 +1,13 @@ +package android.widget; + +import com.android.internal.view.menu.MenuBuilder; + +/** + * 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. + * + * @hide + */ +public interface MenuItemHoverListener { + public void onItemHovered(MenuBuilder menu, int position); +} diff --git a/core/java/android/widget/MenuPopupWindow.java b/core/java/android/widget/MenuPopupWindow.java index 900aa326d502e..ba77b1b530799 100644 --- a/core/java/android/widget/MenuPopupWindow.java +++ b/core/java/android/widget/MenuPopupWindow.java @@ -22,12 +22,12 @@ import android.content.res.Resources; import android.transition.Transition; import android.util.AttributeSet; import android.view.KeyEvent; +import android.view.MotionEvent; import android.view.View; -import android.view.ViewGroup; -import android.view.ViewParent; import com.android.internal.view.menu.ListMenuItemView; import com.android.internal.view.menu.MenuAdapter; +import com.android.internal.view.menu.MenuBuilder; /** * A MenuPopupWindow represents the popup window for menu. @@ -37,20 +37,32 @@ import com.android.internal.view.menu.MenuAdapter; * * @hide */ -public class MenuPopupWindow extends ListPopupWindow { +public class MenuPopupWindow extends ListPopupWindow implements MenuItemHoverListener { + private MenuItemHoverListener mHoverListener; + public MenuPopupWindow(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } @Override DropDownListView createDropDownListView(Context context, boolean hijackFocus) { - return new MenuDropDownListView(context, hijackFocus); + MenuDropDownListView view = new MenuDropDownListView(context, hijackFocus); + view.setHoverListener(this); + return view; } public void setEnterTransition(Transition enterTransition) { mPopup.setEnterTransition(enterTransition); } + public void setExitTransition(Transition exitTransition) { + mPopup.setExitTransition(exitTransition); + } + + public void setHoverListener(MenuItemHoverListener hoverListener) { + mHoverListener = hoverListener; + } + /** * Set whether this window is touch modal or if outside touches will be sent to * other windows behind it. @@ -59,10 +71,23 @@ public class MenuPopupWindow extends ListPopupWindow { mPopup.setTouchModal(touchModal); } - private static class MenuDropDownListView extends DropDownListView { + @Override + public void onItemHovered(MenuBuilder menu, int position) { + // Forward up the chain + if (mHoverListener != null) { + mHoverListener.onItemHovered(menu, position); + } + } + + /** + * @hide + */ + public static class MenuDropDownListView extends DropDownListView { final int mAdvanceKey; final int mRetreatKey; + private MenuItemHoverListener mHoverListener; + public MenuDropDownListView(Context context, boolean hijackFocus) { super(context, hijackFocus); @@ -77,6 +102,15 @@ public class MenuPopupWindow extends ListPopupWindow { } } + public void setHoverListener(MenuItemHoverListener hoverListener) { + mHoverListener = hoverListener; + } + + public void clearSelection() { + setSelectedPositionInt(INVALID_POSITION); + setNextSelectedPositionInt(INVALID_POSITION); + } + @Override public boolean onKeyDown(int keyCode, KeyEvent event) { ListMenuItemView selectedItem = (ListMenuItemView) getSelectedView(); @@ -99,6 +133,32 @@ public class MenuPopupWindow extends ListPopupWindow { return super.onKeyDown(keyCode, event); } + @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) { + mHoverListener.onItemHovered( + ((MenuAdapter) getAdapter()).getAdapterMenu(), position); + } + + return superVal; + } } + } \ No newline at end of file diff --git a/core/java/android/widget/PopupMenu.java b/core/java/android/widget/PopupMenu.java index bd1fbb8f29c59..34a8439257007 100644 --- a/core/java/android/widget/PopupMenu.java +++ b/core/java/android/widget/PopupMenu.java @@ -270,25 +270,8 @@ public class PopupMenu implements MenuBuilder.Callback, MenuPresenter.Callback { * @hide */ public boolean onOpenSubMenu(MenuBuilder subMenu) { - if (subMenu == null) return false; - - if (!subMenu.hasVisibleItems()) { - return true; - } - - if (!mShowCascadingMenus) { - // Current menu will be dismissed by the normal helper, submenu will be shown in its - // place. (If cascading menus are enabled, the cascading implementation will show the - // submenu itself). - new MenuPopupHelper(mContext, subMenu, mAnchor).show(); - } - return true; - } - - /** - * @hide - */ - public void onCloseSubMenu(SubMenuBuilder menu) { + // The menu presenter will handle opening the submenu itself. Nothing to do here. + return false; } /** diff --git a/core/java/com/android/internal/view/menu/CascadingMenuPopup.java b/core/java/com/android/internal/view/menu/CascadingMenuPopup.java index 4e3e40f29a020..415f32528914c 100644 --- a/core/java/com/android/internal/view/menu/CascadingMenuPopup.java +++ b/core/java/com/android/internal/view/menu/CascadingMenuPopup.java @@ -10,17 +10,22 @@ 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.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; 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.AdapterView; import android.widget.DropDownListView; +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; @@ -32,8 +37,7 @@ import com.android.internal.util.Preconditions; * @hide */ final class CascadingMenuPopup extends MenuPopup implements AdapterView.OnItemClickListener, - MenuPresenter, OnKeyListener, PopupWindow.OnDismissListener, - ViewTreeObserver.OnGlobalLayoutListener, View.OnAttachStateChangeListener{ + MenuPresenter, OnKeyListener, PopupWindow.OnDismissListener { @Retention(RetentionPolicy.SOURCE) @IntDef({HORIZ_POSITION_LEFT, HORIZ_POSITION_RIGHT}) public @interface HorizPosition {} @@ -41,12 +45,133 @@ final class CascadingMenuPopup extends MenuPopup implements AdapterView.OnItemCl private static final int HORIZ_POSITION_LEFT = 0; private static final int HORIZ_POSITION_RIGHT = 1; + private static final int SUBMENU_TIMEOUT_MS = 200; + private final Context mContext; private final int mMenuMaxWidth; private final int mPopupStyleAttr; private final int mPopupStyleRes; private final boolean mOverflowOnly; private final int mLayoutDirection; + private final Handler mSubMenuHoverHandler; + + private final OnGlobalLayoutListener mGlobalLayoutListener = new OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + if (isShowing()) { + final View anchor = mShownAnchorView; + if (anchor == null || !anchor.isShown()) { + dismiss(); + } else if (isShowing()) { + // Recompute window sizes and positions. + for (MenuPopupWindow popup : mPopupWindows) { + popup.show(); + } + } + } + } + }; + + private final OnAttachStateChangeListener mAttachStateChangeListener = + new OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View v) { + } + + @Override + public void onViewDetachedFromWindow(View v) { + if (mTreeObserver != null) { + if (!mTreeObserver.isAlive()) { + mTreeObserver = v.getViewTreeObserver(); + } + mTreeObserver.removeGlobalOnLayoutListener(mGlobalLayoutListener); + } + v.removeOnAttachStateChangeListener(this); + } + }; + + 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 = (MenuAdapter) view.getAdapter(); + + if (adapter.getAdapterMenu() == menu) { + menuIndex = i; + break; + } + } + + if (menuIndex == -1) { + 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 = + (MenuAdapter) 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()); + } + } + }, 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 = (MenuAdapter) nextView.getAdapter(); + + view.clearSelection(); + + 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. + mPopupWindows.get(nextMenuIndex).setExitTransition(null); + nextAdapter.getAdapterMenu().close(); + } + } + }, SUBMENU_TIMEOUT_MS); + } + } + }; private int mDropDownGravity = Gravity.NO_GRAVITY; private View mAnchorView; @@ -86,6 +211,7 @@ final class CascadingMenuPopup extends MenuPopup implements AdapterView.OnItemCl mPopupWindows = new ArrayList(); mListViews = new ArrayList(); mOffsets = new ArrayList(); + mSubMenuHoverHandler = new Handler(); } @Override @@ -96,12 +222,12 @@ final class CascadingMenuPopup extends MenuPopup implements AdapterView.OnItemCl private MenuPopupWindow createPopupWindow() { MenuPopupWindow popupWindow = new MenuPopupWindow( mContext, null, mPopupStyleAttr, mPopupStyleRes); + popupWindow.setHoverListener(mMenuItemHoverListener); popupWindow.setOnItemClickListener(this); popupWindow.setOnDismissListener(this); popupWindow.setAnchorView(mAnchorView); popupWindow.setDropDownGravity(mDropDownGravity); popupWindow.setModal(true); - popupWindow.setTouchModal(false); return popupWindow; } @@ -125,8 +251,10 @@ final class CascadingMenuPopup extends MenuPopup implements AdapterView.OnItemCl if (mShownAnchorView != null) { final boolean addGlobalListener = mTreeObserver == null; mTreeObserver = mShownAnchorView.getViewTreeObserver(); // Refresh to latest - if (addGlobalListener) mTreeObserver.addOnGlobalLayoutListener(this); - mShownAnchorView.addOnAttachStateChangeListener(this); + if (addGlobalListener) { + mTreeObserver.addOnGlobalLayoutListener(mGlobalLayoutListener); + } + mShownAnchorView.addOnAttachStateChangeListener(mAttachStateChangeListener); } } @@ -208,6 +336,7 @@ final class CascadingMenuPopup extends MenuPopup implements AdapterView.OnItemCl int y = 0; if (addSubMenu) { + popupWindow.setTouchModal(false); popupWindow.setEnterTransition(null); ListView lastListView = mListViews.get(mListViews.size() - 1); @@ -331,7 +460,7 @@ final class CascadingMenuPopup extends MenuPopup implements AdapterView.OnItemCl if (menuIndex == -1 && menu == adapter.mAdapterMenu) { menuIndex = i; - wasSelected = view.getSelectedItem() != null; + wasSelected = view.getSelectedView() != null; } // Once the menu has been found, remove it and all submenus beneath it from the @@ -365,12 +494,12 @@ final class CascadingMenuPopup extends MenuPopup implements AdapterView.OnItemCl if (mPopupWindows.size() == 0) { if (mTreeObserver != null) { - if (!mTreeObserver.isAlive()) mTreeObserver = - mShownAnchorView.getViewTreeObserver(); - mTreeObserver.removeGlobalOnLayoutListener(this); + if (mTreeObserver.isAlive()) { + mTreeObserver.removeGlobalOnLayoutListener(mGlobalLayoutListener); + } mTreeObserver = null; } - mShownAnchorView.removeOnAttachStateChangeListener(this); + mShownAnchorView.removeOnAttachStateChangeListener(mAttachStateChangeListener); // If every [sub]menu was dismissed, that means the whole thing was dismissed, so notify // the owner. mOnDismissListener.onDismiss(); @@ -411,31 +540,4 @@ final class CascadingMenuPopup extends MenuPopup implements AdapterView.OnItemCl return mListViews.size() > 0 ? mListViews.get(mListViews.size() - 1) : null; } - @Override - public void onGlobalLayout() { - if (isShowing()) { - final View anchor = mShownAnchorView; - if (anchor == null || !anchor.isShown()) { - dismiss(); - } else if (isShowing()) { - // Recompute window sizes and positions. - for (MenuPopupWindow popup : mPopupWindows) { - popup.show(); - } - } - } - } - - @Override - public void onViewAttachedToWindow(View v) { - } - - @Override - public void onViewDetachedFromWindow(View v) { - if (mTreeObserver != null) { - if (!mTreeObserver.isAlive()) mTreeObserver = v.getViewTreeObserver(); - mTreeObserver.removeGlobalOnLayoutListener(this); - } - v.removeOnAttachStateChangeListener(this); - } } \ No newline at end of file diff --git a/core/java/com/android/internal/view/menu/MenuAdapter.java b/core/java/com/android/internal/view/menu/MenuAdapter.java index 1e03b1f8b9717..673cfd12d8789 100644 --- a/core/java/com/android/internal/view/menu/MenuAdapter.java +++ b/core/java/com/android/internal/view/menu/MenuAdapter.java @@ -40,6 +40,10 @@ public class MenuAdapter extends BaseAdapter { findExpandedIndex(); } + public boolean getForceShowIcon() { + return mForceShowIcon; + } + public void setForceShowIcon(boolean forceShow) { mForceShowIcon = forceShow; } diff --git a/core/java/com/android/internal/view/menu/StandardMenuPopup.java b/core/java/com/android/internal/view/menu/StandardMenuPopup.java index 530b16acd4ec0..8877f3d96a39e 100644 --- a/core/java/com/android/internal/view/menu/StandardMenuPopup.java +++ b/core/java/com/android/internal/view/menu/StandardMenuPopup.java @@ -9,7 +9,9 @@ import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.view.View.OnAttachStateChangeListener; import android.view.View.OnKeyListener; +import android.view.ViewTreeObserver.OnGlobalLayoutListener; import android.view.ViewTreeObserver; import android.widget.AdapterView; import android.widget.ListView; @@ -25,8 +27,7 @@ import com.android.internal.util.Preconditions; * viewport. */ final class StandardMenuPopup extends MenuPopup implements OnDismissListener, OnItemClickListener, - MenuPresenter, OnKeyListener, ViewTreeObserver.OnGlobalLayoutListener, - View.OnAttachStateChangeListener { + MenuPresenter, OnKeyListener { private final Context mContext; private final LayoutInflater mInflater; @@ -40,6 +41,37 @@ final class StandardMenuPopup extends MenuPopup implements OnDismissListener, On // StandardMenuPopup. private final MenuPopupWindow mPopup; + private final OnGlobalLayoutListener mGlobalLayoutListener = new OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + if (isShowing()) { + final View anchor = mShownAnchorView; + if (anchor == null || !anchor.isShown()) { + dismiss(); + } else if (isShowing()) { + // Recompute window size and position + mPopup.show(); + } + } + } + }; + + private final OnAttachStateChangeListener mAttachStateChangeListener = + new OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View v) { + } + + @Override + public void onViewDetachedFromWindow(View v) { + if (mTreeObserver != null) { + if (!mTreeObserver.isAlive()) mTreeObserver = v.getViewTreeObserver(); + mTreeObserver.removeGlobalOnLayoutListener(mGlobalLayoutListener); + } + v.removeOnAttachStateChangeListener(this); + } + }; + private PopupWindow.OnDismissListener mOnDismissListener; private View mAnchorView; @@ -50,7 +82,7 @@ final class StandardMenuPopup extends MenuPopup implements OnDismissListener, On private ViewGroup mMeasureParent; /** Whether the popup has been dismissed. Once dismissed, it cannot be opened again. */ - private boolean wasDismissed; + private boolean mWasDismissed; /** Whether the cached content width value is valid. */ private boolean mHasContentWidth; @@ -97,7 +129,7 @@ final class StandardMenuPopup extends MenuPopup implements OnDismissListener, On return true; } - if (mAnchorView == null) { + if (mWasDismissed || mAnchorView == null) { return false; } @@ -112,9 +144,9 @@ final class StandardMenuPopup extends MenuPopup implements OnDismissListener, On final boolean addGlobalListener = mTreeObserver == null; mTreeObserver = anchor.getViewTreeObserver(); // Refresh to latest if (addGlobalListener) { - mTreeObserver.addOnGlobalLayoutListener(this); + mTreeObserver.addOnGlobalLayoutListener(mGlobalLayoutListener); } - anchor.addOnAttachStateChangeListener(this); + anchor.addOnAttachStateChangeListener(mAttachStateChangeListener); mPopup.setAnchorView(anchor); mPopup.setDropDownGravity(mDropDownGravity); @@ -158,20 +190,20 @@ final class StandardMenuPopup extends MenuPopup implements OnDismissListener, On @Override public boolean isShowing() { - return !wasDismissed && mPopup.isShowing(); + return !mWasDismissed && mPopup.isShowing(); } @Override public void onDismiss() { - wasDismissed = true; + mWasDismissed = true; mMenu.close(); if (mTreeObserver != null) { if (!mTreeObserver.isAlive()) mTreeObserver = mShownAnchorView.getViewTreeObserver(); - mTreeObserver.removeGlobalOnLayoutListener(this); + mTreeObserver.removeGlobalOnLayoutListener(mGlobalLayoutListener); mTreeObserver = null; } - mShownAnchorView.removeOnAttachStateChangeListener(this); + mShownAnchorView.removeOnAttachStateChangeListener(mAttachStateChangeListener); mOnDismissListener.onDismiss(); } @@ -196,17 +228,7 @@ final class StandardMenuPopup extends MenuPopup implements OnDismissListener, On mContext, subMenu, mShownAnchorView, mOverflowOnly, mPopupStyleAttr, mPopupStyleRes); subPopup.setCallback(mPresenterCallback); - - boolean preserveIconSpacing = false; - final int count = subMenu.size(); - for (int i = 0; i < count; i++) { - MenuItem childItem = subMenu.getItem(i); - if (childItem.isVisible() && childItem.getIcon() != null) { - preserveIconSpacing = true; - break; - } - } - subPopup.setForceShowIcon(preserveIconSpacing); + subPopup.setForceShowIcon(mAdapter.getForceShowIcon()); if (subPopup.tryShow()) { if (mPresenterCallback != null) { @@ -234,7 +256,6 @@ final class StandardMenuPopup extends MenuPopup implements OnDismissListener, On return false; } - @Override public Parcelable onSaveInstanceState() { return null; @@ -267,30 +288,4 @@ final class StandardMenuPopup extends MenuPopup implements OnDismissListener, On public ListView getListView() { return mPopup.getListView(); } - - @Override - public void onGlobalLayout() { - if (isShowing()) { - final View anchor = mShownAnchorView; - if (anchor == null || !anchor.isShown()) { - dismiss(); - } else if (isShowing()) { - // Recompute window size and position - mPopup.show(); - } - } - } - - @Override - public void onViewAttachedToWindow(View v) { - } - - @Override - public void onViewDetachedFromWindow(View v) { - if (mTreeObserver != null) { - if (!mTreeObserver.isAlive()) mTreeObserver = v.getViewTreeObserver(); - mTreeObserver.removeGlobalOnLayoutListener(this); - } - v.removeOnAttachStateChangeListener(this); - } } \ No newline at end of file