blob: bac602509148bfcab9fa2db36bb05edd4ed5c3d0 [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
Alan Viverette708aa9d2015-11-20 15:21:30 -050019import android.annotation.AttrRes;
20import android.annotation.NonNull;
21import android.annotation.Nullable;
22import android.annotation.StyleRes;
Artur Satayev2a9f3b82019-12-10 17:47:55 +000023import android.compat.annotation.UnsupportedAppUsage;
Adam Powell42675342010-07-09 18:02:59 -070024import android.content.Context;
Oren Blasberg93c19582016-04-14 14:08:17 -070025import android.graphics.Point;
Alan Viverette91098572016-01-19 14:07:31 -050026import android.graphics.Rect;
Oren Blasberg93c19582016-04-14 14:08:17 -070027import android.view.Display;
Adam Powell54c94de2013-09-26 15:36:34 -070028import android.view.Gravity;
Adam Powell42675342010-07-09 18:02:59 -070029import android.view.View;
Oren Blasberg93c19582016-04-14 14:08:17 -070030import android.view.WindowManager;
Alan Viverette708aa9d2015-11-20 15:21:30 -050031import android.widget.PopupWindow.OnDismissListener;
Adam Powell42675342010-07-09 18:02:59 -070032
Mathew Inwood3a75f262019-06-27 12:47:38 +010033import com.android.internal.view.menu.MenuPresenter.Callback;
34
Adam Powell42675342010-07-09 18:02:59 -070035/**
Adam Powell696cba52011-03-29 10:38:16 -070036 * Presents a menu as a small, simple popup anchored to another view.
Adam Powell42675342010-07-09 18:02:59 -070037 */
Alan Viverette021627e2015-11-25 14:22:00 -050038public class MenuPopupHelper implements MenuHelper {
Alan Viverette91098572016-01-19 14:07:31 -050039 private static final int TOUCH_EPICENTER_SIZE_DP = 48;
40
Alan Viverette0bce6ab2013-06-26 17:46:16 -070041 private final Context mContext;
Alan Viverette708aa9d2015-11-20 15:21:30 -050042
43 // Immutable cached popup menu properties.
Alan Viverette0bce6ab2013-06-26 17:46:16 -070044 private final MenuBuilder mMenu;
Alan Viverette0bce6ab2013-06-26 17:46:16 -070045 private final boolean mOverflowOnly;
Alan Viverette560f1702014-05-05 14:40:07 -070046 private final int mPopupStyleAttr;
Alan Viverette29632522014-10-15 17:19:30 -070047 private final int mPopupStyleRes;
Alan Viverette0bce6ab2013-06-26 17:46:16 -070048
Alan Viverette708aa9d2015-11-20 15:21:30 -050049 // Mutable cached popup menu properties.
Adam Powell4afd62b2011-02-18 15:02:18 -080050 private View mAnchorView;
Alan Viveretted6443f62015-11-20 13:57:15 -050051 private int mDropDownGravity = Gravity.START;
Mathew Inwoodaf972c82018-08-20 14:13:20 +010052 @UnsupportedAppUsage
Oren Blasberged391262015-09-01 12:12:51 -070053 private boolean mForceShowIcon;
Oren Blasberg99162822015-09-10 14:37:26 -070054 private Callback mPresenterCallback;
Alan Viveretted6443f62015-11-20 13:57:15 -050055
Alan Viverette708aa9d2015-11-20 15:21:30 -050056 private MenuPopup mPopup;
57 private OnDismissListener mOnDismissListener;
Adam Powell54c94de2013-09-26 15:36:34 -070058
Mathew Inwoodaf972c82018-08-20 14:13:20 +010059 @UnsupportedAppUsage
Alan Viverette708aa9d2015-11-20 15:21:30 -050060 public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu) {
Alan Viverette29632522014-10-15 17:19:30 -070061 this(context, menu, null, false, com.android.internal.R.attr.popupMenuStyle, 0);
Adam Powell8028dd32010-07-15 10:16:33 -070062 }
63
Mathew Inwoodaf972c82018-08-20 14:13:20 +010064 @UnsupportedAppUsage
Alan Viverette708aa9d2015-11-20 15:21:30 -050065 public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu,
66 @NonNull View anchorView) {
Alan Viverette29632522014-10-15 17:19:30 -070067 this(context, menu, anchorView, false, com.android.internal.R.attr.popupMenuStyle, 0);
Adam Powell8028dd32010-07-15 10:16:33 -070068 }
69
Alan Viverette708aa9d2015-11-20 15:21:30 -050070 public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu,
71 @NonNull View anchorView,
72 boolean overflowOnly, @AttrRes int popupStyleAttr) {
Alan Viverette29632522014-10-15 17:19:30 -070073 this(context, menu, anchorView, overflowOnly, popupStyleAttr, 0);
74 }
75
Alan Viverette708aa9d2015-11-20 15:21:30 -050076 public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu,
77 @NonNull View anchorView, boolean overflowOnly, @AttrRes int popupStyleAttr,
78 @StyleRes int popupStyleRes) {
Adam Powell42675342010-07-09 18:02:59 -070079 mContext = context;
Adam Powell8028dd32010-07-15 10:16:33 -070080 mMenu = menu;
Alan Viverette708aa9d2015-11-20 15:21:30 -050081 mAnchorView = anchorView;
Adam Powell8028dd32010-07-15 10:16:33 -070082 mOverflowOnly = overflowOnly;
Alan Viverette560f1702014-05-05 14:40:07 -070083 mPopupStyleAttr = popupStyleAttr;
Alan Viverette29632522014-10-15 17:19:30 -070084 mPopupStyleRes = popupStyleRes;
Oren Blasbergb23976e2015-09-01 14:55:42 -070085 }
Adam Powell696cba52011-03-29 10:38:16 -070086
Alan Viverette708aa9d2015-11-20 15:21:30 -050087 public void setOnDismissListener(@Nullable OnDismissListener listener) {
88 mOnDismissListener = listener;
Adam Powell42675342010-07-09 18:02:59 -070089 }
90
Alan Viverette708aa9d2015-11-20 15:21:30 -050091 /**
92 * Sets the view to which the popup window is anchored.
93 * <p>
94 * Changes take effect on the next call to show().
95 *
96 * @param anchor the view to which the popup window should be anchored
97 */
Mathew Inwoodaf972c82018-08-20 14:13:20 +010098 @UnsupportedAppUsage
Alan Viverette708aa9d2015-11-20 15:21:30 -050099 public void setAnchorView(@NonNull View anchor) {
Adam Powell4afd62b2011-02-18 15:02:18 -0800100 mAnchorView = anchor;
Adam Powellf0ad6e62011-01-10 17:14:06 -0800101 }
102
Alan Viverette708aa9d2015-11-20 15:21:30 -0500103 /**
104 * Sets whether the popup menu's adapter is forced to show icons in the
105 * menu item views.
106 * <p>
107 * Changes take effect on the next call to show().
108 *
Mihai Popada59e1d2019-02-05 16:02:21 +0000109 * This method should not be accessed directly outside the framework, please use
110 * {@link android.widget.PopupMenu#setForceShowIcon(boolean)} instead.
111 *
Alan Viverette708aa9d2015-11-20 15:21:30 -0500112 * @param forceShowIcon {@code true} to force icons to be shown, or
113 * {@code false} for icons to be optionally shown
114 */
Mathew Inwood3a75f262019-06-27 12:47:38 +0100115 @UnsupportedAppUsage
Alan Viverette708aa9d2015-11-20 15:21:30 -0500116 public void setForceShowIcon(boolean forceShowIcon) {
117 mForceShowIcon = forceShowIcon;
Oren Blasberg7b457452016-04-19 15:44:46 -0700118 if (mPopup != null) {
119 mPopup.setForceShowIcon(forceShowIcon);
120 }
Adam Powell91511032011-07-13 10:24:06 -0700121 }
122
Alan Viverette708aa9d2015-11-20 15:21:30 -0500123 /**
124 * Sets the alignment of the popup window relative to the anchor view.
125 * <p>
126 * Changes take effect on the next call to show().
127 *
128 * @param gravity alignment of the popup relative to the anchor
129 */
Mathew Inwoodaf972c82018-08-20 14:13:20 +0100130 @UnsupportedAppUsage
Adam Powell54c94de2013-09-26 15:36:34 -0700131 public void setGravity(int gravity) {
132 mDropDownGravity = gravity;
133 }
134
Alan Viverette708aa9d2015-11-20 15:21:30 -0500135 /**
136 * @return alignment of the popup relative to the anchor
137 */
Alan Viverette75d83792015-01-07 15:51:54 -0800138 public int getGravity() {
139 return mDropDownGravity;
140 }
141
Mathew Inwoodaf972c82018-08-20 14:13:20 +0100142 @UnsupportedAppUsage
Adam Powell42675342010-07-09 18:02:59 -0700143 public void show() {
Adam Powell5e3f2842011-01-07 17:16:56 -0800144 if (!tryShow()) {
145 throw new IllegalStateException("MenuPopupHelper cannot be used without an anchor");
146 }
147 }
148
Oren Blasberged391262015-09-01 12:12:51 -0700149 public void show(int x, int y) {
150 if (!tryShow(x, y)) {
151 throw new IllegalStateException("MenuPopupHelper cannot be used without an anchor");
152 }
153 }
154
Alan Viverette708aa9d2015-11-20 15:21:30 -0500155 @NonNull
Mathew Inwoodaf972c82018-08-20 14:13:20 +0100156 @UnsupportedAppUsage
Alan Viverette708aa9d2015-11-20 15:21:30 -0500157 public MenuPopup getPopup() {
158 if (mPopup == null) {
159 mPopup = createPopup();
160 }
Alan Viveretteca6a36112013-08-16 14:41:06 -0700161 return mPopup;
162 }
163
Alan Viverette8fd949e2015-03-11 12:21:30 -0700164 /**
Oren Blasbergb23976e2015-09-01 14:55:42 -0700165 * Attempts to show the popup anchored to the view specified by {@link #setAnchorView(View)}.
Alan Viverette8fd949e2015-03-11 12:21:30 -0700166 *
Oren Blasbergb23976e2015-09-01 14:55:42 -0700167 * @return {@code true} if the popup was shown or was already showing prior to calling this
168 * method, {@code false} otherwise
Alan Viverette8fd949e2015-03-11 12:21:30 -0700169 */
Mathew Inwoodaf972c82018-08-20 14:13:20 +0100170 @UnsupportedAppUsage
Adam Powell5e3f2842011-01-07 17:16:56 -0800171 public boolean tryShow() {
Alan Viverette8fd949e2015-03-11 12:21:30 -0700172 if (isShowing()) {
173 return true;
174 }
175
Oren Blasberg99162822015-09-10 14:37:26 -0700176 if (mAnchorView == null) {
Adam Powell5e3f2842011-01-07 17:16:56 -0800177 return false;
Adam Powell8028dd32010-07-15 10:16:33 -0700178 }
Adam Powell42675342010-07-09 18:02:59 -0700179
Alan Viverette708aa9d2015-11-20 15:21:30 -0500180 showPopup(0, 0, false, false);
Oren Blasberged391262015-09-01 12:12:51 -0700181 return true;
182 }
183
Alan Viveretted6443f62015-11-20 13:57:15 -0500184 /**
185 * Shows the popup menu and makes a best-effort to anchor it to the
186 * specified (x,y) coordinate relative to the anchor view.
187 * <p>
Alan Viverette91098572016-01-19 14:07:31 -0500188 * Additionally, the popup's transition epicenter (see
189 * {@link android.widget.PopupWindow#setEpicenterBounds(Rect)} will be
190 * centered on the specified coordinate, rather than using the bounds of
191 * the anchor view.
192 * <p>
Alan Viveretted6443f62015-11-20 13:57:15 -0500193 * If the popup's resolved gravity is {@link Gravity#LEFT}, this will
194 * display the popup with its top-left corner at (x,y) relative to the
195 * anchor view. If the resolved gravity is {@link Gravity#RIGHT}, the
196 * popup's top-right corner will be at (x,y).
197 * <p>
198 * If the popup cannot be displayed fully on-screen, this method will
199 * attempt to scroll the anchor view's ancestors and/or offset the popup
200 * such that it may be displayed fully on-screen.
201 *
202 * @param x x coordinate relative to the anchor view
203 * @param y y coordinate relative to the anchor view
204 * @return {@code true} if the popup was shown or was already showing prior
205 * to calling this method, {@code false} otherwise
206 */
Oren Blasberged391262015-09-01 12:12:51 -0700207 public boolean tryShow(int x, int y) {
208 if (isShowing()) {
209 return true;
210 }
211
212 if (mAnchorView == null) {
213 return false;
214 }
215
Alan Viverette708aa9d2015-11-20 15:21:30 -0500216 showPopup(x, y, true, true);
Oren Blasberged391262015-09-01 12:12:51 -0700217 return true;
218 }
219
Alan Viverette708aa9d2015-11-20 15:21:30 -0500220 /**
221 * Creates the popup and assigns cached properties.
222 *
223 * @return an initialized popup
224 */
225 @NonNull
226 private MenuPopup createPopup() {
Oren Blasberg93c19582016-04-14 14:08:17 -0700227 final WindowManager windowManager = (WindowManager) mContext.getSystemService(
228 Context.WINDOW_SERVICE);
229 final Display display = windowManager.getDefaultDisplay();
230 final Point displaySize = new Point();
231 display.getRealSize(displaySize);
232
233 final int smallestWidth = Math.min(displaySize.x, displaySize.y);
234 final int minSmallestWidthCascading = mContext.getResources().getDimensionPixelSize(
235 com.android.internal.R.dimen.cascading_menus_min_smallest_width);
236 final boolean enableCascadingSubmenus = smallestWidth >= minSmallestWidthCascading;
Alan Viveretted6443f62015-11-20 13:57:15 -0500237
Alan Viverette708aa9d2015-11-20 15:21:30 -0500238 final MenuPopup popup;
239 if (enableCascadingSubmenus) {
240 popup = new CascadingMenuPopup(mContext, mAnchorView, mPopupStyleAttr,
241 mPopupStyleRes, mOverflowOnly);
242 } else {
243 popup = new StandardMenuPopup(mContext, mMenu, mAnchorView, mPopupStyleAttr,
244 mPopupStyleRes, mOverflowOnly);
245 }
246
247 // Assign immutable properties.
248 popup.addMenu(mMenu);
249 popup.setOnDismissListener(mInternalOnDismissListener);
250
251 // Assign mutable properties. These may be reassigned later.
252 popup.setAnchorView(mAnchorView);
253 popup.setCallback(mPresenterCallback);
254 popup.setForceShowIcon(mForceShowIcon);
255 popup.setGravity(mDropDownGravity);
256
257 return popup;
258 }
259
Alan Viverette91098572016-01-19 14:07:31 -0500260 private void showPopup(int xOffset, int yOffset, boolean useOffsets, boolean showTitle) {
261 final MenuPopup popup = getPopup();
262 popup.setShowTitle(showTitle);
263
264 if (useOffsets) {
Alan Viveretted6443f62015-11-20 13:57:15 -0500265 // If the resolved drop-down gravity is RIGHT, the popup's right
266 // edge will be aligned with the anchor view. Adjust by the anchor
267 // width such that the top-right corner is at the X offset.
268 final int hgrav = Gravity.getAbsoluteGravity(mDropDownGravity,
269 mAnchorView.getLayoutDirection()) & Gravity.HORIZONTAL_GRAVITY_MASK;
Alan Viveretted6443f62015-11-20 13:57:15 -0500270 if (hgrav == Gravity.RIGHT) {
Vladislav Kaznacheevd959c9d2018-01-23 14:03:36 -0800271 xOffset -= mAnchorView.getWidth();
Alan Viveretted6443f62015-11-20 13:57:15 -0500272 }
Alan Viverette91098572016-01-19 14:07:31 -0500273
274 popup.setHorizontalOffset(xOffset);
275 popup.setVerticalOffset(yOffset);
276
277 // Set the transition epicenter to be roughly finger (or mouse
278 // cursor) sized and centered around the offset position. This
279 // will give the appearance that the window is emerging from
280 // the touch point.
281 final float density = mContext.getResources().getDisplayMetrics().density;
282 final int halfSize = (int) (TOUCH_EPICENTER_SIZE_DP * density / 2);
283 final Rect epicenter = new Rect(xOffset - halfSize, yOffset - halfSize,
284 xOffset + halfSize, yOffset + halfSize);
285 popup.setEpicenterBounds(epicenter);
Alan Viveretted6443f62015-11-20 13:57:15 -0500286 }
Oren Blasberg99162822015-09-10 14:37:26 -0700287
Alan Viverette708aa9d2015-11-20 15:21:30 -0500288 popup.show();
Adam Powell42675342010-07-09 18:02:59 -0700289 }
290
Alan Viverette708aa9d2015-11-20 15:21:30 -0500291 /**
292 * Dismisses the popup, if showing.
293 */
Alan Viverette021627e2015-11-25 14:22:00 -0500294 @Override
Mathew Inwoodaf972c82018-08-20 14:13:20 +0100295 @UnsupportedAppUsage
Adam Powell42675342010-07-09 18:02:59 -0700296 public void dismiss() {
Adam Powell3d3da272010-08-11 18:06:17 -0700297 if (isShowing()) {
298 mPopup.dismiss();
299 }
Adam Powell8515ee82010-11-30 14:09:55 -0800300 }
301
Alan Viverette708aa9d2015-11-20 15:21:30 -0500302 /**
303 * Called after the popup has been dismissed.
304 * <p>
305 * <strong>Note:</strong> Subclasses should call the super implementation
306 * last to ensure that any necessary tear down has occurred before the
307 * listener specified by {@link #setOnDismissListener(OnDismissListener)}
308 * is called.
309 */
310 protected void onDismiss() {
Adam Powell8515ee82010-11-30 14:09:55 -0800311 mPopup = null;
Alan Viverette708aa9d2015-11-20 15:21:30 -0500312
313 if (mOnDismissListener != null) {
314 mOnDismissListener.onDismiss();
315 }
Adam Powell42675342010-07-09 18:02:59 -0700316 }
317
Adam Powell8028dd32010-07-15 10:16:33 -0700318 public boolean isShowing() {
319 return mPopup != null && mPopup.isShowing();
320 }
321
Alan Viverette021627e2015-11-25 14:22:00 -0500322 @Override
323 public void setPresenterCallback(@Nullable MenuPresenter.Callback cb) {
Oren Blasberg99162822015-09-10 14:37:26 -0700324 mPresenterCallback = cb;
Alan Viverette708aa9d2015-11-20 15:21:30 -0500325 if (mPopup != null) {
326 mPopup.setCallback(cb);
327 }
Adam Powell696cba52011-03-29 10:38:16 -0700328 }
Alan Viverette708aa9d2015-11-20 15:21:30 -0500329
330 /**
331 * Listener used to proxy dismiss callbacks to the helper's owner.
332 */
333 private final OnDismissListener mInternalOnDismissListener = new OnDismissListener() {
334 @Override
335 public void onDismiss() {
336 MenuPopupHelper.this.onDismiss();
337 }
338 };
Adam Powell42675342010-07-09 18:02:59 -0700339}