/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.internal.view.menu; import com.android.internal.view.menu.MenuPresenter.Callback; import android.annotation.AttrRes; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.StyleRes; import android.content.Context; import android.graphics.Point; import android.graphics.Rect; import android.util.DisplayMetrics; import android.view.Display; import android.view.Gravity; import android.view.View; import android.view.WindowManager; import android.widget.PopupWindow.OnDismissListener; /** * Presents a menu as a small, simple popup anchored to another view. */ public class MenuPopupHelper implements MenuHelper { private static final int TOUCH_EPICENTER_SIZE_DP = 48; private final Context mContext; // Immutable cached popup menu properties. private final MenuBuilder mMenu; private final boolean mOverflowOnly; private final int mPopupStyleAttr; private final int mPopupStyleRes; // Mutable cached popup menu properties. private View mAnchorView; private int mDropDownGravity = Gravity.START; private boolean mForceShowIcon; private Callback mPresenterCallback; private MenuPopup mPopup; private OnDismissListener mOnDismissListener; public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu) { this(context, menu, null, false, com.android.internal.R.attr.popupMenuStyle, 0); } public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu, @NonNull View anchorView) { this(context, menu, anchorView, false, com.android.internal.R.attr.popupMenuStyle, 0); } public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu, @NonNull View anchorView, boolean overflowOnly, @AttrRes int popupStyleAttr) { this(context, menu, anchorView, overflowOnly, popupStyleAttr, 0); } public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu, @NonNull View anchorView, boolean overflowOnly, @AttrRes int popupStyleAttr, @StyleRes int popupStyleRes) { mContext = context; mMenu = menu; mAnchorView = anchorView; mOverflowOnly = overflowOnly; mPopupStyleAttr = popupStyleAttr; mPopupStyleRes = popupStyleRes; } public void setOnDismissListener(@Nullable OnDismissListener listener) { mOnDismissListener = listener; } /** * Sets the view to which the popup window is anchored. *

* Changes take effect on the next call to show(). * * @param anchor the view to which the popup window should be anchored */ public void setAnchorView(@NonNull View anchor) { mAnchorView = anchor; } /** * Sets whether the popup menu's adapter is forced to show icons in the * menu item views. *

* Changes take effect on the next call to show(). * * @param forceShowIcon {@code true} to force icons to be shown, or * {@code false} for icons to be optionally shown */ public void setForceShowIcon(boolean forceShowIcon) { mForceShowIcon = forceShowIcon; if (mPopup != null) { mPopup.setForceShowIcon(forceShowIcon); } } /** * Sets the alignment of the popup window relative to the anchor view. *

* Changes take effect on the next call to show(). * * @param gravity alignment of the popup relative to the anchor */ public void setGravity(int gravity) { mDropDownGravity = gravity; } /** * @return alignment of the popup relative to the anchor */ public int getGravity() { return mDropDownGravity; } public void show() { if (!tryShow()) { throw new IllegalStateException("MenuPopupHelper cannot be used without an anchor"); } } public void show(int x, int y) { if (!tryShow(x, y)) { throw new IllegalStateException("MenuPopupHelper cannot be used without an anchor"); } } @NonNull public MenuPopup getPopup() { if (mPopup == null) { mPopup = createPopup(); } return mPopup; } /** * Attempts to show the popup anchored to the view specified by {@link #setAnchorView(View)}. * * @return {@code true} if the popup was shown or was already showing prior to calling this * method, {@code false} otherwise */ public boolean tryShow() { if (isShowing()) { return true; } if (mAnchorView == null) { return false; } showPopup(0, 0, false, false); return true; } /** * Shows the popup menu and makes a best-effort to anchor it to the * specified (x,y) coordinate relative to the anchor view. *

* Additionally, the popup's transition epicenter (see * {@link android.widget.PopupWindow#setEpicenterBounds(Rect)} will be * centered on the specified coordinate, rather than using the bounds of * the anchor view. *

* If the popup's resolved gravity is {@link Gravity#LEFT}, this will * display the popup with its top-left corner at (x,y) relative to the * anchor view. If the resolved gravity is {@link Gravity#RIGHT}, the * popup's top-right corner will be at (x,y). *

* If the popup cannot be displayed fully on-screen, this method will * attempt to scroll the anchor view's ancestors and/or offset the popup * such that it may be displayed fully on-screen. * * @param x x coordinate relative to the anchor view * @param y y coordinate relative to the anchor view * @return {@code true} if the popup was shown or was already showing prior * to calling this method, {@code false} otherwise */ public boolean tryShow(int x, int y) { if (isShowing()) { return true; } if (mAnchorView == null) { return false; } showPopup(x, y, true, true); return true; } /** * Creates the popup and assigns cached properties. * * @return an initialized popup */ @NonNull private MenuPopup createPopup() { final WindowManager windowManager = (WindowManager) mContext.getSystemService( Context.WINDOW_SERVICE); final Display display = windowManager.getDefaultDisplay(); final Point displaySize = new Point(); display.getRealSize(displaySize); final int smallestWidth = Math.min(displaySize.x, displaySize.y); final int minSmallestWidthCascading = mContext.getResources().getDimensionPixelSize( com.android.internal.R.dimen.cascading_menus_min_smallest_width); final boolean enableCascadingSubmenus = smallestWidth >= minSmallestWidthCascading; final MenuPopup popup; if (enableCascadingSubmenus) { popup = new CascadingMenuPopup(mContext, mAnchorView, mPopupStyleAttr, mPopupStyleRes, mOverflowOnly); } else { popup = new StandardMenuPopup(mContext, mMenu, mAnchorView, mPopupStyleAttr, mPopupStyleRes, mOverflowOnly); } // Assign immutable properties. popup.addMenu(mMenu); popup.setOnDismissListener(mInternalOnDismissListener); // Assign mutable properties. These may be reassigned later. popup.setAnchorView(mAnchorView); popup.setCallback(mPresenterCallback); popup.setForceShowIcon(mForceShowIcon); popup.setGravity(mDropDownGravity); return popup; } private void showPopup(int xOffset, int yOffset, boolean useOffsets, boolean showTitle) { final MenuPopup popup = getPopup(); popup.setShowTitle(showTitle); if (useOffsets) { // If the resolved drop-down gravity is RIGHT, the popup's right // edge will be aligned with the anchor view. Adjust by the anchor // width such that the top-right corner is at the X offset. final int hgrav = Gravity.getAbsoluteGravity(mDropDownGravity, mAnchorView.getLayoutDirection()) & Gravity.HORIZONTAL_GRAVITY_MASK; if (hgrav == Gravity.RIGHT) { xOffset += mAnchorView.getWidth(); } popup.setHorizontalOffset(xOffset); popup.setVerticalOffset(yOffset); // Set the transition epicenter to be roughly finger (or mouse // cursor) sized and centered around the offset position. This // will give the appearance that the window is emerging from // the touch point. final float density = mContext.getResources().getDisplayMetrics().density; final int halfSize = (int) (TOUCH_EPICENTER_SIZE_DP * density / 2); final Rect epicenter = new Rect(xOffset - halfSize, yOffset - halfSize, xOffset + halfSize, yOffset + halfSize); popup.setEpicenterBounds(epicenter); } popup.show(); } /** * Dismisses the popup, if showing. */ @Override public void dismiss() { if (isShowing()) { mPopup.dismiss(); } } /** * Called after the popup has been dismissed. *

* Note: Subclasses should call the super implementation * last to ensure that any necessary tear down has occurred before the * listener specified by {@link #setOnDismissListener(OnDismissListener)} * is called. */ protected void onDismiss() { mPopup = null; if (mOnDismissListener != null) { mOnDismissListener.onDismiss(); } } public boolean isShowing() { return mPopup != null && mPopup.isShowing(); } @Override public void setPresenterCallback(@Nullable MenuPresenter.Callback cb) { mPresenterCallback = cb; if (mPopup != null) { mPopup.setCallback(cb); } } /** * Listener used to proxy dismiss callbacks to the helper's owner. */ private final OnDismissListener mInternalOnDismissListener = new OnDismissListener() { @Override public void onDismiss() { MenuPopupHelper.this.onDismiss(); } }; }