blob: 6af41a51f0ddbbaacaa1a93ea5ad65c95f4c7a85 [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;
Adam Powell42675342010-07-09 18:02:59 -070025import android.content.Context;
Oren Blasberg93c19582016-04-14 14:08:17 -070026import android.graphics.Point;
Alan Viverette91098572016-01-19 14:07:31 -050027import android.graphics.Rect;
28import android.util.DisplayMetrics;
Oren Blasberg93c19582016-04-14 14:08:17 -070029import android.view.Display;
Adam Powell54c94de2013-09-26 15:36:34 -070030import android.view.Gravity;
Adam Powell42675342010-07-09 18:02:59 -070031import android.view.View;
Oren Blasberg93c19582016-04-14 14:08:17 -070032import android.view.WindowManager;
Alan Viverette708aa9d2015-11-20 15:21:30 -050033import android.widget.PopupWindow.OnDismissListener;
Adam Powell42675342010-07-09 18:02:59 -070034
35/**
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;
Oren Blasberged391262015-09-01 12:12:51 -070052 private boolean mForceShowIcon;
Oren Blasberg99162822015-09-10 14:37:26 -070053 private Callback mPresenterCallback;
Alan Viveretted6443f62015-11-20 13:57:15 -050054
Alan Viverette708aa9d2015-11-20 15:21:30 -050055 private MenuPopup mPopup;
56 private OnDismissListener mOnDismissListener;
Adam Powell54c94de2013-09-26 15:36:34 -070057
Alan Viverette708aa9d2015-11-20 15:21:30 -050058 public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu) {
Alan Viverette29632522014-10-15 17:19:30 -070059 this(context, menu, null, false, com.android.internal.R.attr.popupMenuStyle, 0);
Adam Powell8028dd32010-07-15 10:16:33 -070060 }
61
Alan Viverette708aa9d2015-11-20 15:21:30 -050062 public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu,
63 @NonNull View anchorView) {
Alan Viverette29632522014-10-15 17:19:30 -070064 this(context, menu, anchorView, false, com.android.internal.R.attr.popupMenuStyle, 0);
Adam Powell8028dd32010-07-15 10:16:33 -070065 }
66
Alan Viverette708aa9d2015-11-20 15:21:30 -050067 public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu,
68 @NonNull View anchorView,
69 boolean overflowOnly, @AttrRes int popupStyleAttr) {
Alan Viverette29632522014-10-15 17:19:30 -070070 this(context, menu, anchorView, overflowOnly, popupStyleAttr, 0);
71 }
72
Alan Viverette708aa9d2015-11-20 15:21:30 -050073 public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu,
74 @NonNull View anchorView, boolean overflowOnly, @AttrRes int popupStyleAttr,
75 @StyleRes int popupStyleRes) {
Adam Powell42675342010-07-09 18:02:59 -070076 mContext = context;
Adam Powell8028dd32010-07-15 10:16:33 -070077 mMenu = menu;
Alan Viverette708aa9d2015-11-20 15:21:30 -050078 mAnchorView = anchorView;
Adam Powell8028dd32010-07-15 10:16:33 -070079 mOverflowOnly = overflowOnly;
Alan Viverette560f1702014-05-05 14:40:07 -070080 mPopupStyleAttr = popupStyleAttr;
Alan Viverette29632522014-10-15 17:19:30 -070081 mPopupStyleRes = popupStyleRes;
Oren Blasbergb23976e2015-09-01 14:55:42 -070082 }
Adam Powell696cba52011-03-29 10:38:16 -070083
Alan Viverette708aa9d2015-11-20 15:21:30 -050084 public void setOnDismissListener(@Nullable OnDismissListener listener) {
85 mOnDismissListener = listener;
Adam Powell42675342010-07-09 18:02:59 -070086 }
87
Alan Viverette708aa9d2015-11-20 15:21:30 -050088 /**
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 */
95 public void setAnchorView(@NonNull View anchor) {
Adam Powell4afd62b2011-02-18 15:02:18 -080096 mAnchorView = anchor;
Adam Powellf0ad6e62011-01-10 17:14:06 -080097 }
98
Alan Viverette708aa9d2015-11-20 15:21:30 -050099 /**
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 */
108 public void setForceShowIcon(boolean forceShowIcon) {
109 mForceShowIcon = forceShowIcon;
Oren Blasberg7b457452016-04-19 15:44:46 -0700110 if (mPopup != null) {
111 mPopup.setForceShowIcon(forceShowIcon);
112 }
Adam Powell91511032011-07-13 10:24:06 -0700113 }
114
Alan Viverette708aa9d2015-11-20 15:21:30 -0500115 /**
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 */
Adam Powell54c94de2013-09-26 15:36:34 -0700122 public void setGravity(int gravity) {
123 mDropDownGravity = gravity;
124 }
125
Alan Viverette708aa9d2015-11-20 15:21:30 -0500126 /**
127 * @return alignment of the popup relative to the anchor
128 */
Alan Viverette75d83792015-01-07 15:51:54 -0800129 public int getGravity() {
130 return mDropDownGravity;
131 }
132
Adam Powell42675342010-07-09 18:02:59 -0700133 public void show() {
Adam Powell5e3f2842011-01-07 17:16:56 -0800134 if (!tryShow()) {
135 throw new IllegalStateException("MenuPopupHelper cannot be used without an anchor");
136 }
137 }
138
Oren Blasberged391262015-09-01 12:12:51 -0700139 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
Alan Viverette708aa9d2015-11-20 15:21:30 -0500145 @NonNull
146 public MenuPopup getPopup() {
147 if (mPopup == null) {
148 mPopup = createPopup();
149 }
Alan Viveretteca6a36112013-08-16 14:41:06 -0700150 return mPopup;
151 }
152
Alan Viverette8fd949e2015-03-11 12:21:30 -0700153 /**
Oren Blasbergb23976e2015-09-01 14:55:42 -0700154 * Attempts to show the popup anchored to the view specified by {@link #setAnchorView(View)}.
Alan Viverette8fd949e2015-03-11 12:21:30 -0700155 *
Oren Blasbergb23976e2015-09-01 14:55:42 -0700156 * @return {@code true} if the popup was shown or was already showing prior to calling this
157 * method, {@code false} otherwise
Alan Viverette8fd949e2015-03-11 12:21:30 -0700158 */
Adam Powell5e3f2842011-01-07 17:16:56 -0800159 public boolean tryShow() {
Alan Viverette8fd949e2015-03-11 12:21:30 -0700160 if (isShowing()) {
161 return true;
162 }
163
Oren Blasberg99162822015-09-10 14:37:26 -0700164 if (mAnchorView == null) {
Adam Powell5e3f2842011-01-07 17:16:56 -0800165 return false;
Adam Powell8028dd32010-07-15 10:16:33 -0700166 }
Adam Powell42675342010-07-09 18:02:59 -0700167
Alan Viverette708aa9d2015-11-20 15:21:30 -0500168 showPopup(0, 0, false, false);
Oren Blasberged391262015-09-01 12:12:51 -0700169 return true;
170 }
171
Alan Viveretted6443f62015-11-20 13:57:15 -0500172 /**
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>
Alan Viverette91098572016-01-19 14:07:31 -0500176 * 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>
Alan Viveretted6443f62015-11-20 13:57:15 -0500181 * 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 */
Oren Blasberged391262015-09-01 12:12:51 -0700195 public boolean tryShow(int x, int y) {
196 if (isShowing()) {
197 return true;
198 }
199
200 if (mAnchorView == null) {
201 return false;
202 }
203
Alan Viverette708aa9d2015-11-20 15:21:30 -0500204 showPopup(x, y, true, true);
Oren Blasberged391262015-09-01 12:12:51 -0700205 return true;
206 }
207
Alan Viverette708aa9d2015-11-20 15:21:30 -0500208 /**
209 * Creates the popup and assigns cached properties.
210 *
211 * @return an initialized popup
212 */
213 @NonNull
214 private MenuPopup createPopup() {
Oren Blasberg93c19582016-04-14 14:08:17 -0700215 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;
Alan Viveretted6443f62015-11-20 13:57:15 -0500225
Alan Viverette708aa9d2015-11-20 15:21:30 -0500226 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
Alan Viverette91098572016-01-19 14:07:31 -0500248 private void showPopup(int xOffset, int yOffset, boolean useOffsets, boolean showTitle) {
249 final MenuPopup popup = getPopup();
250 popup.setShowTitle(showTitle);
251
252 if (useOffsets) {
Alan Viveretted6443f62015-11-20 13:57:15 -0500253 // 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;
Alan Viveretted6443f62015-11-20 13:57:15 -0500258 if (hgrav == Gravity.RIGHT) {
Vladislav Kaznacheev618cfc12017-04-28 17:03:30 -0700259 xOffset += mAnchorView.getWidth();
Alan Viveretted6443f62015-11-20 13:57:15 -0500260 }
Alan Viverette91098572016-01-19 14:07:31 -0500261
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);
Alan Viveretted6443f62015-11-20 13:57:15 -0500274 }
Oren Blasberg99162822015-09-10 14:37:26 -0700275
Alan Viverette708aa9d2015-11-20 15:21:30 -0500276 popup.show();
Adam Powell42675342010-07-09 18:02:59 -0700277 }
278
Alan Viverette708aa9d2015-11-20 15:21:30 -0500279 /**
280 * Dismisses the popup, if showing.
281 */
Alan Viverette021627e2015-11-25 14:22:00 -0500282 @Override
Adam Powell42675342010-07-09 18:02:59 -0700283 public void dismiss() {
Adam Powell3d3da272010-08-11 18:06:17 -0700284 if (isShowing()) {
285 mPopup.dismiss();
286 }
Adam Powell8515ee82010-11-30 14:09:55 -0800287 }
288
Alan Viverette708aa9d2015-11-20 15:21:30 -0500289 /**
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 */
297 protected void onDismiss() {
Adam Powell8515ee82010-11-30 14:09:55 -0800298 mPopup = null;
Alan Viverette708aa9d2015-11-20 15:21:30 -0500299
300 if (mOnDismissListener != null) {
301 mOnDismissListener.onDismiss();
302 }
Adam Powell42675342010-07-09 18:02:59 -0700303 }
304
Adam Powell8028dd32010-07-15 10:16:33 -0700305 public boolean isShowing() {
306 return mPopup != null && mPopup.isShowing();
307 }
308
Alan Viverette021627e2015-11-25 14:22:00 -0500309 @Override
310 public void setPresenterCallback(@Nullable MenuPresenter.Callback cb) {
Oren Blasberg99162822015-09-10 14:37:26 -0700311 mPresenterCallback = cb;
Alan Viverette708aa9d2015-11-20 15:21:30 -0500312 if (mPopup != null) {
313 mPopup.setCallback(cb);
314 }
Adam Powell696cba52011-03-29 10:38:16 -0700315 }
Alan Viverette708aa9d2015-11-20 15:21:30 -0500316
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 };
Adam Powell42675342010-07-09 18:02:59 -0700326}