Merge "Cascading popup menus: open submenu on mouse hover."

This commit is contained in:
Oren Blasberg
2015-09-16 20:59:11 +00:00
committed by Android (Google) Code Review
7 changed files with 266 additions and 114 deletions

View File

@@ -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();

View File

@@ -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);
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
/**

View File

@@ -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<MenuPopupWindow>();
mListViews = new ArrayList<DropDownListView>();
mOffsets = new ArrayList<int[]>();
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);
}
}

View File

@@ -40,6 +40,10 @@ public class MenuAdapter extends BaseAdapter {
findExpandedIndex();
}
public boolean getForceShowIcon() {
return mForceShowIcon;
}
public void setForceShowIcon(boolean forceShow) {
mForceShowIcon = forceShow;
}

View File

@@ -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);
}
}