blob: e6d34600485cf2fbc2aac4c08a684c11b60ba2e7 [file] [log] [blame]
Adam Powell42675342010-07-09 18:02:59 -07001/*
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
17package com.android.internal.view.menu;
18
Oren Blasberg99162822015-09-10 14:37:26 -070019import com.android.internal.view.menu.MenuPresenter.Callback;
20
Alan Viverette708aa9d2015-11-20 15:21:30 -050021import android.annotation.AttrRes;
22import android.annotation.NonNull;
23import android.annotation.Nullable;
24import android.annotation.StyleRes;
Mathew Inwoodaf972c82018-08-20 14:13:20 +010025import android.annotation.UnsupportedAppUsage;
Adam Powell42675342010-07-09 18:02:59 -070026import android.content.Context;
Oren Blasberg93c19582016-04-14 14:08:17 -070027import android.graphics.Point;
Alan Viverette91098572016-01-19 14:07:31 -050028import android.graphics.Rect;
29import android.util.DisplayMetrics;
Oren Blasberg93c19582016-04-14 14:08:17 -070030import android.view.Display;
Adam Powell54c94de2013-09-26 15:36:34 -070031import android.view.Gravity;
Adam Powell42675342010-07-09 18:02:59 -070032import android.view.View;
Oren Blasberg93c19582016-04-14 14:08:17 -070033import android.view.WindowManager;
Alan Viverette708aa9d2015-11-20 15:21:30 -050034import android.widget.PopupWindow.OnDismissListener;
Adam Powell42675342010-07-09 18:02:59 -070035
36/**
Adam Powell696cba52011-03-29 10:38:16 -070037 * Presents a menu as a small, simple popup anchored to another view.
Adam Powell42675342010-07-09 18:02:59 -070038 */
Alan Viverette021627e2015-11-25 14:22:00 -050039public class MenuPopupHelper implements MenuHelper {
Alan Viverette91098572016-01-19 14:07:31 -050040 private static final int TOUCH_EPICENTER_SIZE_DP = 48;
41
Alan Viverette0bce6ab2013-06-26 17:46:16 -070042 private final Context mContext;
Alan Viverette708aa9d2015-11-20 15:21:30 -050043
44 // Immutable cached popup menu properties.
Alan Viverette0bce6ab2013-06-26 17:46:16 -070045 private final MenuBuilder mMenu;
Alan Viverette0bce6ab2013-06-26 17:46:16 -070046 private final boolean mOverflowOnly;
Alan Viverette560f1702014-05-05 14:40:07 -070047 private final int mPopupStyleAttr;
Alan Viverette29632522014-10-15 17:19:30 -070048 private final int mPopupStyleRes;
Alan Viverette0bce6ab2013-06-26 17:46:16 -070049
Alan Viverette708aa9d2015-11-20 15:21:30 -050050 // Mutable cached popup menu properties.
Adam Powell4afd62b2011-02-18 15:02:18 -080051 private View mAnchorView;
Alan Viveretted6443f62015-11-20 13:57:15 -050052 private int mDropDownGravity = Gravity.START;
Mathew Inwoodaf972c82018-08-20 14:13:20 +010053 @UnsupportedAppUsage
Oren Blasberged391262015-09-01 12:12:51 -070054 private boolean mForceShowIcon;
Oren Blasberg99162822015-09-10 14:37:26 -070055 private Callback mPresenterCallback;
Alan Viveretted6443f62015-11-20 13:57:15 -050056
Alan Viverette708aa9d2015-11-20 15:21:30 -050057 private MenuPopup mPopup;
58 private OnDismissListener mOnDismissListener;
Adam Powell54c94de2013-09-26 15:36:34 -070059
Mathew Inwoodaf972c82018-08-20 14:13:20 +010060 @UnsupportedAppUsage
Alan Viverette708aa9d2015-11-20 15:21:30 -050061 public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu) {
Alan Viverette29632522014-10-15 17:19:30 -070062 this(context, menu, null, false, com.android.internal.R.attr.popupMenuStyle, 0);
Adam Powell8028dd32010-07-15 10:16:33 -070063 }
64
Mathew Inwoodaf972c82018-08-20 14:13:20 +010065 @UnsupportedAppUsage
Alan Viverette708aa9d2015-11-20 15:21:30 -050066 public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu,
67 @NonNull View anchorView) {
Alan Viverette29632522014-10-15 17:19:30 -070068 this(context, menu, anchorView, false, com.android.internal.R.attr.popupMenuStyle, 0);
Adam Powell8028dd32010-07-15 10:16:33 -070069 }
70
Alan Viverette708aa9d2015-11-20 15:21:30 -050071 public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu,
72 @NonNull View anchorView,
73 boolean overflowOnly, @AttrRes int popupStyleAttr) {
Alan Viverette29632522014-10-15 17:19:30 -070074 this(context, menu, anchorView, overflowOnly, popupStyleAttr, 0);
75 }
76
Alan Viverette708aa9d2015-11-20 15:21:30 -050077 public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu,
78 @NonNull View anchorView, boolean overflowOnly, @AttrRes int popupStyleAttr,
79 @StyleRes int popupStyleRes) {
Adam Powell42675342010-07-09 18:02:59 -070080 mContext = context;
Adam Powell8028dd32010-07-15 10:16:33 -070081 mMenu = menu;
Alan Viverette708aa9d2015-11-20 15:21:30 -050082 mAnchorView = anchorView;
Adam Powell8028dd32010-07-15 10:16:33 -070083 mOverflowOnly = overflowOnly;
Alan Viverette560f1702014-05-05 14:40:07 -070084 mPopupStyleAttr = popupStyleAttr;
Alan Viverette29632522014-10-15 17:19:30 -070085 mPopupStyleRes = popupStyleRes;
Oren Blasbergb23976e2015-09-01 14:55:42 -070086 }
Adam Powell696cba52011-03-29 10:38:16 -070087
Alan Viverette708aa9d2015-11-20 15:21:30 -050088 public void setOnDismissListener(@Nullable OnDismissListener listener) {
89 mOnDismissListener = listener;
Adam Powell42675342010-07-09 18:02:59 -070090 }
91
Alan Viverette708aa9d2015-11-20 15:21:30 -050092 /**
93 * Sets the view to which the popup window is anchored.
94 * <p>
95 * Changes take effect on the next call to show().
96 *
97 * @param anchor the view to which the popup window should be anchored
98 */
Mathew Inwoodaf972c82018-08-20 14:13:20 +010099 @UnsupportedAppUsage
Alan Viverette708aa9d2015-11-20 15:21:30 -0500100 public void setAnchorView(@NonNull View anchor) {
Adam Powell4afd62b2011-02-18 15:02:18 -0800101 mAnchorView = anchor;
Adam Powellf0ad6e62011-01-10 17:14:06 -0800102 }
103
Alan Viverette708aa9d2015-11-20 15:21:30 -0500104 /**
105 * Sets whether the popup menu's adapter is forced to show icons in the
106 * menu item views.
107 * <p>
108 * Changes take effect on the next call to show().
109 *
110 * @param forceShowIcon {@code true} to force icons to be shown, or
111 * {@code false} for icons to be optionally shown
112 */
Mathew Inwoodaf972c82018-08-20 14:13:20 +0100113 @UnsupportedAppUsage
Alan Viverette708aa9d2015-11-20 15:21:30 -0500114 public void setForceShowIcon(boolean forceShowIcon) {
115 mForceShowIcon = forceShowIcon;
Oren Blasberg7b457452016-04-19 15:44:46 -0700116 if (mPopup != null) {
117 mPopup.setForceShowIcon(forceShowIcon);
118 }
Adam Powell91511032011-07-13 10:24:06 -0700119 }
120
Alan Viverette708aa9d2015-11-20 15:21:30 -0500121 /**
122 * Sets the alignment of the popup window relative to the anchor view.
123 * <p>
124 * Changes take effect on the next call to show().
125 *
126 * @param gravity alignment of the popup relative to the anchor
127 */
Mathew Inwoodaf972c82018-08-20 14:13:20 +0100128 @UnsupportedAppUsage
Adam Powell54c94de2013-09-26 15:36:34 -0700129 public void setGravity(int gravity) {
130 mDropDownGravity = gravity;
131 }
132
Alan Viverette708aa9d2015-11-20 15:21:30 -0500133 /**
134 * @return alignment of the popup relative to the anchor
135 */
Alan Viverette75d83792015-01-07 15:51:54 -0800136 public int getGravity() {
137 return mDropDownGravity;
138 }
139
Mathew Inwoodaf972c82018-08-20 14:13:20 +0100140 @UnsupportedAppUsage
Adam Powell42675342010-07-09 18:02:59 -0700141 public void show() {
Adam Powell5e3f2842011-01-07 17:16:56 -0800142 if (!tryShow()) {
143 throw new IllegalStateException("MenuPopupHelper cannot be used without an anchor");
144 }
145 }
146
Oren Blasberged391262015-09-01 12:12:51 -0700147 public void show(int x, int y) {
148 if (!tryShow(x, y)) {
149 throw new IllegalStateException("MenuPopupHelper cannot be used without an anchor");
150 }
151 }
152
Alan Viverette708aa9d2015-11-20 15:21:30 -0500153 @NonNull
Mathew Inwoodaf972c82018-08-20 14:13:20 +0100154 @UnsupportedAppUsage
Alan Viverette708aa9d2015-11-20 15:21:30 -0500155 public MenuPopup getPopup() {
156 if (mPopup == null) {
157 mPopup = createPopup();
158 }
Alan Viveretteca6a36112013-08-16 14:41:06 -0700159 return mPopup;
160 }
161
Alan Viverette8fd949e2015-03-11 12:21:30 -0700162 /**
Oren Blasbergb23976e2015-09-01 14:55:42 -0700163 * Attempts to show the popup anchored to the view specified by {@link #setAnchorView(View)}.
Alan Viverette8fd949e2015-03-11 12:21:30 -0700164 *
Oren Blasbergb23976e2015-09-01 14:55:42 -0700165 * @return {@code true} if the popup was shown or was already showing prior to calling this
166 * method, {@code false} otherwise
Alan Viverette8fd949e2015-03-11 12:21:30 -0700167 */
Mathew Inwoodaf972c82018-08-20 14:13:20 +0100168 @UnsupportedAppUsage
Adam Powell5e3f2842011-01-07 17:16:56 -0800169 public boolean tryShow() {
Alan Viverette8fd949e2015-03-11 12:21:30 -0700170 if (isShowing()) {
171 return true;
172 }
173
Oren Blasberg99162822015-09-10 14:37:26 -0700174 if (mAnchorView == null) {
Adam Powell5e3f2842011-01-07 17:16:56 -0800175 return false;
Adam Powell8028dd32010-07-15 10:16:33 -0700176 }
Adam Powell42675342010-07-09 18:02:59 -0700177
Alan Viverette708aa9d2015-11-20 15:21:30 -0500178 showPopup(0, 0, false, false);
Oren Blasberged391262015-09-01 12:12:51 -0700179 return true;
180 }
181
Alan Viveretted6443f62015-11-20 13:57:15 -0500182 /**
183 * Shows the popup menu and makes a best-effort to anchor it to the
184 * specified (x,y) coordinate relative to the anchor view.
185 * <p>
Alan Viverette91098572016-01-19 14:07:31 -0500186 * Additionally, the popup's transition epicenter (see
187 * {@link android.widget.PopupWindow#setEpicenterBounds(Rect)} will be
188 * centered on the specified coordinate, rather than using the bounds of
189 * the anchor view.
190 * <p>
Alan Viveretted6443f62015-11-20 13:57:15 -0500191 * If the popup's resolved gravity is {@link Gravity#LEFT}, this will
192 * display the popup with its top-left corner at (x,y) relative to the
193 * anchor view. If the resolved gravity is {@link Gravity#RIGHT}, the
194 * popup's top-right corner will be at (x,y).
195 * <p>
196 * If the popup cannot be displayed fully on-screen, this method will
197 * attempt to scroll the anchor view's ancestors and/or offset the popup
198 * such that it may be displayed fully on-screen.
199 *
200 * @param x x coordinate relative to the anchor view
201 * @param y y coordinate relative to the anchor view
202 * @return {@code true} if the popup was shown or was already showing prior
203 * to calling this method, {@code false} otherwise
204 */
Oren Blasberged391262015-09-01 12:12:51 -0700205 public boolean tryShow(int x, int y) {
206 if (isShowing()) {
207 return true;
208 }
209
210 if (mAnchorView == null) {
211 return false;
212 }
213
Alan Viverette708aa9d2015-11-20 15:21:30 -0500214 showPopup(x, y, true, true);
Oren Blasberged391262015-09-01 12:12:51 -0700215 return true;
216 }
217
Alan Viverette708aa9d2015-11-20 15:21:30 -0500218 /**
219 * Creates the popup and assigns cached properties.
220 *
221 * @return an initialized popup
222 */
223 @NonNull
224 private MenuPopup createPopup() {
Oren Blasberg93c19582016-04-14 14:08:17 -0700225 final WindowManager windowManager = (WindowManager) mContext.getSystemService(
226 Context.WINDOW_SERVICE);
227 final Display display = windowManager.getDefaultDisplay();
228 final Point displaySize = new Point();
229 display.getRealSize(displaySize);
230
231 final int smallestWidth = Math.min(displaySize.x, displaySize.y);
232 final int minSmallestWidthCascading = mContext.getResources().getDimensionPixelSize(
233 com.android.internal.R.dimen.cascading_menus_min_smallest_width);
234 final boolean enableCascadingSubmenus = smallestWidth >= minSmallestWidthCascading;
Alan Viveretted6443f62015-11-20 13:57:15 -0500235
Alan Viverette708aa9d2015-11-20 15:21:30 -0500236 final MenuPopup popup;
237 if (enableCascadingSubmenus) {
238 popup = new CascadingMenuPopup(mContext, mAnchorView, mPopupStyleAttr,
239 mPopupStyleRes, mOverflowOnly);
240 } else {
241 popup = new StandardMenuPopup(mContext, mMenu, mAnchorView, mPopupStyleAttr,
242 mPopupStyleRes, mOverflowOnly);
243 }
244
245 // Assign immutable properties.
246 popup.addMenu(mMenu);
247 popup.setOnDismissListener(mInternalOnDismissListener);
248
249 // Assign mutable properties. These may be reassigned later.
250 popup.setAnchorView(mAnchorView);
251 popup.setCallback(mPresenterCallback);
252 popup.setForceShowIcon(mForceShowIcon);
253 popup.setGravity(mDropDownGravity);
254
255 return popup;
256 }
257
Alan Viverette91098572016-01-19 14:07:31 -0500258 private void showPopup(int xOffset, int yOffset, boolean useOffsets, boolean showTitle) {
259 final MenuPopup popup = getPopup();
260 popup.setShowTitle(showTitle);
261
262 if (useOffsets) {
Alan Viveretted6443f62015-11-20 13:57:15 -0500263 // If the resolved drop-down gravity is RIGHT, the popup's right
264 // edge will be aligned with the anchor view. Adjust by the anchor
265 // width such that the top-right corner is at the X offset.
266 final int hgrav = Gravity.getAbsoluteGravity(mDropDownGravity,
267 mAnchorView.getLayoutDirection()) & Gravity.HORIZONTAL_GRAVITY_MASK;
Alan Viveretted6443f62015-11-20 13:57:15 -0500268 if (hgrav == Gravity.RIGHT) {
Vladislav Kaznacheevd959c9d2018-01-23 14:03:36 -0800269 xOffset -= mAnchorView.getWidth();
Alan Viveretted6443f62015-11-20 13:57:15 -0500270 }
Alan Viverette91098572016-01-19 14:07:31 -0500271
272 popup.setHorizontalOffset(xOffset);
273 popup.setVerticalOffset(yOffset);
274
275 // Set the transition epicenter to be roughly finger (or mouse
276 // cursor) sized and centered around the offset position. This
277 // will give the appearance that the window is emerging from
278 // the touch point.
279 final float density = mContext.getResources().getDisplayMetrics().density;
280 final int halfSize = (int) (TOUCH_EPICENTER_SIZE_DP * density / 2);
281 final Rect epicenter = new Rect(xOffset - halfSize, yOffset - halfSize,
282 xOffset + halfSize, yOffset + halfSize);
283 popup.setEpicenterBounds(epicenter);
Alan Viveretted6443f62015-11-20 13:57:15 -0500284 }
Oren Blasberg99162822015-09-10 14:37:26 -0700285
Alan Viverette708aa9d2015-11-20 15:21:30 -0500286 popup.show();
Adam Powell42675342010-07-09 18:02:59 -0700287 }
288
Alan Viverette708aa9d2015-11-20 15:21:30 -0500289 /**
290 * Dismisses the popup, if showing.
291 */
Alan Viverette021627e2015-11-25 14:22:00 -0500292 @Override
Mathew Inwoodaf972c82018-08-20 14:13:20 +0100293 @UnsupportedAppUsage
Adam Powell42675342010-07-09 18:02:59 -0700294 public void dismiss() {
Adam Powell3d3da272010-08-11 18:06:17 -0700295 if (isShowing()) {
296 mPopup.dismiss();
297 }
Adam Powell8515ee82010-11-30 14:09:55 -0800298 }
299
Alan Viverette708aa9d2015-11-20 15:21:30 -0500300 /**
301 * Called after the popup has been dismissed.
302 * <p>
303 * <strong>Note:</strong> Subclasses should call the super implementation
304 * last to ensure that any necessary tear down has occurred before the
305 * listener specified by {@link #setOnDismissListener(OnDismissListener)}
306 * is called.
307 */
308 protected void onDismiss() {
Adam Powell8515ee82010-11-30 14:09:55 -0800309 mPopup = null;
Alan Viverette708aa9d2015-11-20 15:21:30 -0500310
311 if (mOnDismissListener != null) {
312 mOnDismissListener.onDismiss();
313 }
Adam Powell42675342010-07-09 18:02:59 -0700314 }
315
Adam Powell8028dd32010-07-15 10:16:33 -0700316 public boolean isShowing() {
317 return mPopup != null && mPopup.isShowing();
318 }
319
Alan Viverette021627e2015-11-25 14:22:00 -0500320 @Override
321 public void setPresenterCallback(@Nullable MenuPresenter.Callback cb) {
Oren Blasberg99162822015-09-10 14:37:26 -0700322 mPresenterCallback = cb;
Alan Viverette708aa9d2015-11-20 15:21:30 -0500323 if (mPopup != null) {
324 mPopup.setCallback(cb);
325 }
Adam Powell696cba52011-03-29 10:38:16 -0700326 }
Alan Viverette708aa9d2015-11-20 15:21:30 -0500327
328 /**
329 * Listener used to proxy dismiss callbacks to the helper's owner.
330 */
331 private final OnDismissListener mInternalOnDismissListener = new OnDismissListener() {
332 @Override
333 public void onDismiss() {
334 MenuPopupHelper.this.onDismiss();
335 }
336 };
Adam Powell42675342010-07-09 18:02:59 -0700337}