blob: 415f32528914cc9cf93ea62e3846734e8aca8b7f [file] [log] [blame]
package com.android.internal.view.menu;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import android.annotation.IntDef;
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;
import com.android.internal.util.Preconditions;
/**
* A popup for a menu which will allow multiple submenus to appear in a cascading fashion, side by
* side.
* @hide
*/
final class CascadingMenuPopup extends MenuPopup implements AdapterView.OnItemClickListener,
MenuPresenter, OnKeyListener, PopupWindow.OnDismissListener {
@Retention(RetentionPolicy.SOURCE)
@IntDef({HORIZ_POSITION_LEFT, HORIZ_POSITION_RIGHT})
public @interface HorizPosition {}
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;
private View mShownAnchorView;
private List<DropDownListView> mListViews;
private List<MenuPopupWindow> mPopupWindows;
private List<int[]> mOffsets;
private int mPreferredPosition;
private boolean mForceShowIcon;
private Callback mPresenterCallback;
private ViewTreeObserver mTreeObserver;
private PopupWindow.OnDismissListener mOnDismissListener;
/**
* Initializes a new cascading-capable menu popup.
*
* @param parent 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) {
mContext = Preconditions.checkNotNull(context);
mAnchorView = Preconditions.checkNotNull(anchor);
mPopupStyleAttr = popupStyleAttr;
mPopupStyleRes = popupStyleRes;
mOverflowOnly = overflowOnly;
mForceShowIcon = false;
final Resources res = context.getResources();
final Configuration config = res.getConfiguration();
mLayoutDirection = config.getLayoutDirection();
mPreferredPosition = mLayoutDirection == View.LAYOUT_DIRECTION_RTL ? HORIZ_POSITION_LEFT :
HORIZ_POSITION_RIGHT;
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[]>();
mSubMenuHoverHandler = new Handler();
}
@Override
public void setForceShowIcon(boolean forceShow) {
mForceShowIcon = forceShow;
}
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);
return popupWindow;
}
@Override
public void show() {
if (isShowing()) {
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);
popupWindow.show();
mListViews.add((DropDownListView) popupWindow.getListView());
}
mShownAnchorView = mAnchorView;
if (mShownAnchorView != null) {
final boolean addGlobalListener = mTreeObserver == null;
mTreeObserver = mShownAnchorView.getViewTreeObserver(); // Refresh to latest
if (addGlobalListener) {
mTreeObserver.addOnGlobalLayoutListener(mGlobalLayoutListener);
}
mShownAnchorView.addOnAttachStateChangeListener(mAttachStateChangeListener);
}
}
@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();
}
}
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
MenuAdapter adapter = (MenuAdapter) parent.getAdapter();
adapter.mAdapterMenu.performItemAction(adapter.getItem(position), 0);
}
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_MENU) {
dismiss();
return true;
}
return false;
}
/**
* Determines whether the next submenu (of the given width) should display on the right or on
* the left of the most recent menu.
*
* @param nextMenuWidth Width of the next submenu to display.
* @return The position to display it.
*/
@HorizPosition
private int getNextMenuPosition(int nextMenuWidth) {
ListView lastListView = mListViews.get(mListViews.size() - 1);
final int[] screenLocation = new int[2];
lastListView.getLocationOnScreen(screenLocation);
final Rect displayFrame = new Rect();
mShownAnchorView.getWindowVisibleDisplayFrame(displayFrame);
if (mPreferredPosition == HORIZ_POSITION_RIGHT) {
final int right = screenLocation[0] + lastListView.getWidth() + nextMenuWidth;
if (right > displayFrame.right) {
return HORIZ_POSITION_LEFT;
}
return HORIZ_POSITION_RIGHT;
} else { // LEFT
final int left = screenLocation[0] - nextMenuWidth;
if (left < 0) {
return HORIZ_POSITION_RIGHT;
}
return HORIZ_POSITION_LEFT;
}
}
@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);
adapter.setForceShowIcon(mForceShowIcon);
popupWindow.setAdapter(adapter);
int menuWidth = measureIndividualMenuWidth(adapter, null, mContext, mMenuMaxWidth);
int x = 0;
int y = 0;
if (addSubMenu) {
popupWindow.setTouchModal(false);
popupWindow.setEnterTransition(null);
ListView lastListView = mListViews.get(mListViews.size() - 1);
@HorizPosition int nextMenuPosition = getNextMenuPosition(menuWidth);
boolean showOnRight = nextMenuPosition == HORIZ_POSITION_RIGHT;
mPreferredPosition = nextMenuPosition;
int[] lastLocation = new int[2];
lastListView.getLocationOnScreen(lastLocation);
int[] lastOffset = mOffsets.get(mOffsets.size() - 1);
// Note: By now, mDropDownGravity is the absolute gravity, so this should work in both
// LTR and RTL.
if ((mDropDownGravity & Gravity.RIGHT) == Gravity.RIGHT) {
if (showOnRight) {
x = lastOffset[0] + menuWidth;
} else {
x = lastOffset[0] - lastListView.getWidth();
}
} else {
if (showOnRight) {
x = lastOffset[0] + lastListView.getWidth();
} else {
x = lastOffset[0] - menuWidth;
}
}
y = lastOffset[1] + lastListView.getSelectedView().getTop() -
lastListView.getChildAt(0).getTop();
}
popupWindow.setWidth(menuWidth);
popupWindow.setHorizontalOffset(x);
popupWindow.setVerticalOffset(y);
mPopupWindows.add(popupWindow);
// 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();
mListViews.add((DropDownListView) popupWindow.getListView());
}
int[] offsets = {x, y};
mOffsets.add(offsets);
}
/**
* @return {@code true} if the popup is currently showing, {@code false} otherwise.
*/
@Override
public boolean isShowing() {
return mPopupWindows.size() > 0 && mPopupWindows.get(0).isShowing();
}
/**
* Called when one or more of the popup windows was dismissed.
*/
@Override
public void onDismiss() {
int dismissedIndex = -1;
for (int i = 0; i < mPopupWindows.size(); i++) {
if (!mPopupWindows.get(i).isShowing()) {
dismissedIndex = i;
break;
}
}
if (dismissedIndex != -1) {
for (int i = dismissedIndex; i < mListViews.size(); i++) {
ListView view = mListViews.get(i);
MenuAdapter adapter = (MenuAdapter) view.getAdapter();
adapter.mAdapterMenu.close();
}
}
}
@Override
public void updateMenuView(boolean cleared) {
for (ListView view : mListViews) {
((MenuAdapter) view.getAdapter()).notifyDataSetChanged();
}
}
@Override
public void setCallback(Callback cb) {
mPresenterCallback = cb;
}
@Override
public boolean onSubMenuSelected(SubMenuBuilder subMenu) {
// Don't allow double-opening of the same submenu.
for (ListView view : mListViews) {
if (((MenuAdapter) view.getAdapter()).mAdapterMenu.equals(subMenu)) {
// Just re-focus that one.
view.requestFocus();
return true;
}
}
if (subMenu.hasVisibleItems()) {
this.addMenu(subMenu);
if (mPresenterCallback != null) {
mPresenterCallback.onOpenSubMenu(subMenu);
}
return true;
}
return false;
}
@Override
public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
int menuIndex = -1;
boolean wasSelected = false;
for (int i = 0; i < mListViews.size(); i++) {
ListView view = mListViews.get(i);
MenuAdapter adapter = (MenuAdapter) view.getAdapter();
if (menuIndex == -1 && menu == adapter.mAdapterMenu) {
menuIndex = i;
wasSelected = view.getSelectedView() != null;
}
// 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);
}
}
// 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();
// If there's still a menu open, refocus the new leaf [sub]menu.
if (mListViews.size() > 0) {
mListViews.get(mListViews.size() - 1).requestFocus();
}
}
if (mListViews.size() == 0 || wasSelected) {
dismiss();
if (mPresenterCallback != null) {
mPresenterCallback.onCloseMenu(menu, allMenusAreClosing);
}
}
if (mPopupWindows.size() == 0) {
if (mTreeObserver != null) {
if (mTreeObserver.isAlive()) {
mTreeObserver.removeGlobalOnLayoutListener(mGlobalLayoutListener);
}
mTreeObserver = null;
}
mShownAnchorView.removeOnAttachStateChangeListener(mAttachStateChangeListener);
// If every [sub]menu was dismissed, that means the whole thing was dismissed, so notify
// the owner.
mOnDismissListener.onDismiss();
}
}
@Override
public boolean flagActionItems() {
return false;
}
@Override
public Parcelable onSaveInstanceState() {
return null;
}
@Override
public void onRestoreInstanceState(Parcelable state) {
}
@Override
public void setGravity(int dropDownGravity) {
mDropDownGravity = Gravity.getAbsoluteGravity(dropDownGravity, mLayoutDirection);
}
@Override
public void setAnchorView(View anchor) {
mAnchorView = anchor;
}
@Override
public void setOnDismissListener(OnDismissListener listener) {
mOnDismissListener = listener;
}
@Override
public ListView getListView() {
return mListViews.size() > 0 ? mListViews.get(mListViews.size() - 1) : null;
}
}