1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.internal.view.menu;
18 
19 import com.android.internal.view.menu.MenuPresenter.Callback;
20 
21 import android.annotation.AttrRes;
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.annotation.StyleRes;
25 import android.content.Context;
26 import android.graphics.Point;
27 import android.graphics.Rect;
28 import android.util.DisplayMetrics;
29 import android.view.Display;
30 import android.view.Gravity;
31 import android.view.View;
32 import android.view.WindowManager;
33 import android.widget.PopupWindow.OnDismissListener;
34 
35 /**
36  * Presents a menu as a small, simple popup anchored to another view.
37  */
38 public class MenuPopupHelper implements MenuHelper {
39     private static final int TOUCH_EPICENTER_SIZE_DP = 48;
40 
41     private final Context mContext;
42 
43     // Immutable cached popup menu properties.
44     private final MenuBuilder mMenu;
45     private final boolean mOverflowOnly;
46     private final int mPopupStyleAttr;
47     private final int mPopupStyleRes;
48 
49     // Mutable cached popup menu properties.
50     private View mAnchorView;
51     private int mDropDownGravity = Gravity.START;
52     private boolean mForceShowIcon;
53     private Callback mPresenterCallback;
54 
55     private MenuPopup mPopup;
56     private OnDismissListener mOnDismissListener;
57 
MenuPopupHelper(@onNull Context context, @NonNull MenuBuilder menu)58     public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu) {
59         this(context, menu, null, false, com.android.internal.R.attr.popupMenuStyle, 0);
60     }
61 
MenuPopupHelper(@onNull Context context, @NonNull MenuBuilder menu, @NonNull View anchorView)62     public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu,
63             @NonNull View anchorView) {
64         this(context, menu, anchorView, false, com.android.internal.R.attr.popupMenuStyle, 0);
65     }
66 
MenuPopupHelper(@onNull Context context, @NonNull MenuBuilder menu, @NonNull View anchorView, boolean overflowOnly, @AttrRes int popupStyleAttr)67     public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu,
68             @NonNull View anchorView,
69             boolean overflowOnly, @AttrRes int popupStyleAttr) {
70         this(context, menu, anchorView, overflowOnly, popupStyleAttr, 0);
71     }
72 
MenuPopupHelper(@onNull Context context, @NonNull MenuBuilder menu, @NonNull View anchorView, boolean overflowOnly, @AttrRes int popupStyleAttr, @StyleRes int popupStyleRes)73     public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu,
74             @NonNull View anchorView, boolean overflowOnly, @AttrRes int popupStyleAttr,
75             @StyleRes int popupStyleRes) {
76         mContext = context;
77         mMenu = menu;
78         mAnchorView = anchorView;
79         mOverflowOnly = overflowOnly;
80         mPopupStyleAttr = popupStyleAttr;
81         mPopupStyleRes = popupStyleRes;
82     }
83 
setOnDismissListener(@ullable OnDismissListener listener)84     public void setOnDismissListener(@Nullable OnDismissListener listener) {
85         mOnDismissListener = listener;
86     }
87 
88     /**
89       * Sets the view to which the popup window is anchored.
90       * <p>
91       * Changes take effect on the next call to show().
92       *
93       * @param anchor the view to which the popup window should be anchored
94       */
setAnchorView(@onNull View anchor)95     public void setAnchorView(@NonNull View anchor) {
96         mAnchorView = anchor;
97     }
98 
99     /**
100      * Sets whether the popup menu's adapter is forced to show icons in the
101      * menu item views.
102      * <p>
103      * Changes take effect on the next call to show().
104      *
105      * @param forceShowIcon {@code true} to force icons to be shown, or
106      *                  {@code false} for icons to be optionally shown
107      */
setForceShowIcon(boolean forceShowIcon)108     public void setForceShowIcon(boolean forceShowIcon) {
109         mForceShowIcon = forceShowIcon;
110         if (mPopup != null) {
111             mPopup.setForceShowIcon(forceShowIcon);
112         }
113     }
114 
115     /**
116       * Sets the alignment of the popup window relative to the anchor view.
117       * <p>
118       * Changes take effect on the next call to show().
119       *
120       * @param gravity alignment of the popup relative to the anchor
121       */
setGravity(int gravity)122     public void setGravity(int gravity) {
123         mDropDownGravity = gravity;
124     }
125 
126     /**
127      * @return alignment of the popup relative to the anchor
128      */
getGravity()129     public int getGravity() {
130         return mDropDownGravity;
131     }
132 
show()133     public void show() {
134         if (!tryShow()) {
135             throw new IllegalStateException("MenuPopupHelper cannot be used without an anchor");
136         }
137     }
138 
show(int x, int y)139     public void show(int x, int y) {
140         if (!tryShow(x, y)) {
141             throw new IllegalStateException("MenuPopupHelper cannot be used without an anchor");
142         }
143     }
144 
145     @NonNull
getPopup()146     public MenuPopup getPopup() {
147         if (mPopup == null) {
148             mPopup = createPopup();
149         }
150         return mPopup;
151     }
152 
153     /**
154      * Attempts to show the popup anchored to the view specified by {@link #setAnchorView(View)}.
155      *
156      * @return {@code true} if the popup was shown or was already showing prior to calling this
157      *         method, {@code false} otherwise
158      */
tryShow()159     public boolean tryShow() {
160         if (isShowing()) {
161             return true;
162         }
163 
164         if (mAnchorView == null) {
165             return false;
166         }
167 
168         showPopup(0, 0, false, false);
169         return true;
170     }
171 
172     /**
173      * Shows the popup menu and makes a best-effort to anchor it to the
174      * specified (x,y) coordinate relative to the anchor view.
175      * <p>
176      * Additionally, the popup's transition epicenter (see
177      * {@link android.widget.PopupWindow#setEpicenterBounds(Rect)} will be
178      * centered on the specified coordinate, rather than using the bounds of
179      * the anchor view.
180      * <p>
181      * If the popup's resolved gravity is {@link Gravity#LEFT}, this will
182      * display the popup with its top-left corner at (x,y) relative to the
183      * anchor view. If the resolved gravity is {@link Gravity#RIGHT}, the
184      * popup's top-right corner will be at (x,y).
185      * <p>
186      * If the popup cannot be displayed fully on-screen, this method will
187      * attempt to scroll the anchor view's ancestors and/or offset the popup
188      * such that it may be displayed fully on-screen.
189      *
190      * @param x x coordinate relative to the anchor view
191      * @param y y coordinate relative to the anchor view
192      * @return {@code true} if the popup was shown or was already showing prior
193      *         to calling this method, {@code false} otherwise
194      */
tryShow(int x, int y)195     public boolean tryShow(int x, int y) {
196         if (isShowing()) {
197             return true;
198         }
199 
200         if (mAnchorView == null) {
201             return false;
202         }
203 
204         showPopup(x, y, true, true);
205         return true;
206     }
207 
208     /**
209      * Creates the popup and assigns cached properties.
210      *
211      * @return an initialized popup
212      */
213     @NonNull
createPopup()214     private MenuPopup createPopup() {
215         final WindowManager windowManager = (WindowManager) mContext.getSystemService(
216             Context.WINDOW_SERVICE);
217         final Display display = windowManager.getDefaultDisplay();
218         final Point displaySize = new Point();
219         display.getRealSize(displaySize);
220 
221         final int smallestWidth = Math.min(displaySize.x, displaySize.y);
222         final int minSmallestWidthCascading = mContext.getResources().getDimensionPixelSize(
223             com.android.internal.R.dimen.cascading_menus_min_smallest_width);
224         final boolean enableCascadingSubmenus = smallestWidth >= minSmallestWidthCascading;
225 
226         final MenuPopup popup;
227         if (enableCascadingSubmenus) {
228             popup = new CascadingMenuPopup(mContext, mAnchorView, mPopupStyleAttr,
229                     mPopupStyleRes, mOverflowOnly);
230         } else {
231             popup = new StandardMenuPopup(mContext, mMenu, mAnchorView, mPopupStyleAttr,
232                     mPopupStyleRes, mOverflowOnly);
233         }
234 
235         // Assign immutable properties.
236         popup.addMenu(mMenu);
237         popup.setOnDismissListener(mInternalOnDismissListener);
238 
239         // Assign mutable properties. These may be reassigned later.
240         popup.setAnchorView(mAnchorView);
241         popup.setCallback(mPresenterCallback);
242         popup.setForceShowIcon(mForceShowIcon);
243         popup.setGravity(mDropDownGravity);
244 
245         return popup;
246     }
247 
showPopup(int xOffset, int yOffset, boolean useOffsets, boolean showTitle)248     private void showPopup(int xOffset, int yOffset, boolean useOffsets, boolean showTitle) {
249         final MenuPopup popup = getPopup();
250         popup.setShowTitle(showTitle);
251 
252         if (useOffsets) {
253             // If the resolved drop-down gravity is RIGHT, the popup's right
254             // edge will be aligned with the anchor view. Adjust by the anchor
255             // width such that the top-right corner is at the X offset.
256             final int hgrav = Gravity.getAbsoluteGravity(mDropDownGravity,
257                     mAnchorView.getLayoutDirection()) & Gravity.HORIZONTAL_GRAVITY_MASK;
258             if (hgrav == Gravity.RIGHT) {
259                 xOffset -= mAnchorView.getWidth();
260             }
261 
262             popup.setHorizontalOffset(xOffset);
263             popup.setVerticalOffset(yOffset);
264 
265             // Set the transition epicenter to be roughly finger (or mouse
266             // cursor) sized and centered around the offset position. This
267             // will give the appearance that the window is emerging from
268             // the touch point.
269             final float density = mContext.getResources().getDisplayMetrics().density;
270             final int halfSize = (int) (TOUCH_EPICENTER_SIZE_DP * density / 2);
271             final Rect epicenter = new Rect(xOffset - halfSize, yOffset - halfSize,
272                     xOffset + halfSize, yOffset + halfSize);
273             popup.setEpicenterBounds(epicenter);
274         }
275 
276         popup.show();
277     }
278 
279     /**
280      * Dismisses the popup, if showing.
281      */
282     @Override
dismiss()283     public void dismiss() {
284         if (isShowing()) {
285             mPopup.dismiss();
286         }
287     }
288 
289     /**
290      * Called after the popup has been dismissed.
291      * <p>
292      * <strong>Note:</strong> Subclasses should call the super implementation
293      * last to ensure that any necessary tear down has occurred before the
294      * listener specified by {@link #setOnDismissListener(OnDismissListener)}
295      * is called.
296      */
onDismiss()297     protected void onDismiss() {
298         mPopup = null;
299 
300         if (mOnDismissListener != null) {
301             mOnDismissListener.onDismiss();
302         }
303     }
304 
isShowing()305     public boolean isShowing() {
306         return mPopup != null && mPopup.isShowing();
307     }
308 
309     @Override
setPresenterCallback(@ullable MenuPresenter.Callback cb)310     public void setPresenterCallback(@Nullable MenuPresenter.Callback cb) {
311         mPresenterCallback = cb;
312         if (mPopup != null) {
313             mPopup.setCallback(cb);
314         }
315     }
316 
317     /**
318      * Listener used to proxy dismiss callbacks to the helper's owner.
319      */
320     private final OnDismissListener mInternalOnDismissListener = new OnDismissListener() {
321         @Override
322         public void onDismiss() {
323             MenuPopupHelper.this.onDismiss();
324         }
325     };
326 }
327