blob: 1f1e594e755b39d5555270a52b3b279ce31a8394 [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;
Alan Viverette91098572016-01-19 14:07:31 -050026import android.graphics.Rect;
27import android.util.DisplayMetrics;
Adam Powell54c94de2013-09-26 15:36:34 -070028import android.view.Gravity;
Adam Powell42675342010-07-09 18:02:59 -070029import android.view.View;
Alan Viverette708aa9d2015-11-20 15:21:30 -050030import android.widget.PopupWindow.OnDismissListener;
Adam Powell42675342010-07-09 18:02:59 -070031
32/**
Adam Powell696cba52011-03-29 10:38:16 -070033 * Presents a menu as a small, simple popup anchored to another view.
Adam Powell42675342010-07-09 18:02:59 -070034 */
Alan Viverette021627e2015-11-25 14:22:00 -050035public class MenuPopupHelper implements MenuHelper {
Alan Viverette91098572016-01-19 14:07:31 -050036 private static final int TOUCH_EPICENTER_SIZE_DP = 48;
37
Alan Viverette0bce6ab2013-06-26 17:46:16 -070038 private final Context mContext;
Alan Viverette708aa9d2015-11-20 15:21:30 -050039
40 // Immutable cached popup menu properties.
Alan Viverette0bce6ab2013-06-26 17:46:16 -070041 private final MenuBuilder mMenu;
Alan Viverette0bce6ab2013-06-26 17:46:16 -070042 private final boolean mOverflowOnly;
Alan Viverette560f1702014-05-05 14:40:07 -070043 private final int mPopupStyleAttr;
Alan Viverette29632522014-10-15 17:19:30 -070044 private final int mPopupStyleRes;
Alan Viverette0bce6ab2013-06-26 17:46:16 -070045
Alan Viverette708aa9d2015-11-20 15:21:30 -050046 // Mutable cached popup menu properties.
Adam Powell4afd62b2011-02-18 15:02:18 -080047 private View mAnchorView;
Alan Viveretted6443f62015-11-20 13:57:15 -050048 private int mDropDownGravity = Gravity.START;
Oren Blasberged391262015-09-01 12:12:51 -070049 private boolean mForceShowIcon;
Oren Blasberg99162822015-09-10 14:37:26 -070050 private Callback mPresenterCallback;
Alan Viveretted6443f62015-11-20 13:57:15 -050051
Alan Viverette708aa9d2015-11-20 15:21:30 -050052 private MenuPopup mPopup;
53 private OnDismissListener mOnDismissListener;
Adam Powell54c94de2013-09-26 15:36:34 -070054
Alan Viverette708aa9d2015-11-20 15:21:30 -050055 public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu) {
Alan Viverette29632522014-10-15 17:19:30 -070056 this(context, menu, null, false, com.android.internal.R.attr.popupMenuStyle, 0);
Adam Powell8028dd32010-07-15 10:16:33 -070057 }
58
Alan Viverette708aa9d2015-11-20 15:21:30 -050059 public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu,
60 @NonNull View anchorView) {
Alan Viverette29632522014-10-15 17:19:30 -070061 this(context, menu, anchorView, false, com.android.internal.R.attr.popupMenuStyle, 0);
Adam Powell8028dd32010-07-15 10:16:33 -070062 }
63
Alan Viverette708aa9d2015-11-20 15:21:30 -050064 public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu,
65 @NonNull View anchorView,
66 boolean overflowOnly, @AttrRes int popupStyleAttr) {
Alan Viverette29632522014-10-15 17:19:30 -070067 this(context, menu, anchorView, overflowOnly, popupStyleAttr, 0);
68 }
69
Alan Viverette708aa9d2015-11-20 15:21:30 -050070 public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu,
71 @NonNull View anchorView, boolean overflowOnly, @AttrRes int popupStyleAttr,
72 @StyleRes int popupStyleRes) {
Adam Powell42675342010-07-09 18:02:59 -070073 mContext = context;
Adam Powell8028dd32010-07-15 10:16:33 -070074 mMenu = menu;
Alan Viverette708aa9d2015-11-20 15:21:30 -050075 mAnchorView = anchorView;
Adam Powell8028dd32010-07-15 10:16:33 -070076 mOverflowOnly = overflowOnly;
Alan Viverette560f1702014-05-05 14:40:07 -070077 mPopupStyleAttr = popupStyleAttr;
Alan Viverette29632522014-10-15 17:19:30 -070078 mPopupStyleRes = popupStyleRes;
Oren Blasbergb23976e2015-09-01 14:55:42 -070079 }
Adam Powell696cba52011-03-29 10:38:16 -070080
Alan Viverette708aa9d2015-11-20 15:21:30 -050081 public void setOnDismissListener(@Nullable OnDismissListener listener) {
82 mOnDismissListener = listener;
Adam Powell42675342010-07-09 18:02:59 -070083 }
84
Alan Viverette708aa9d2015-11-20 15:21:30 -050085 /**
86 * Sets the view to which the popup window is anchored.
87 * <p>
88 * Changes take effect on the next call to show().
89 *
90 * @param anchor the view to which the popup window should be anchored
91 */
92 public void setAnchorView(@NonNull View anchor) {
Adam Powell4afd62b2011-02-18 15:02:18 -080093 mAnchorView = anchor;
Adam Powellf0ad6e62011-01-10 17:14:06 -080094 }
95
Alan Viverette708aa9d2015-11-20 15:21:30 -050096 /**
97 * Sets whether the popup menu's adapter is forced to show icons in the
98 * menu item views.
99 * <p>
100 * Changes take effect on the next call to show().
101 *
102 * @param forceShowIcon {@code true} to force icons to be shown, or
103 * {@code false} for icons to be optionally shown
104 */
105 public void setForceShowIcon(boolean forceShowIcon) {
106 mForceShowIcon = forceShowIcon;
Adam Powell91511032011-07-13 10:24:06 -0700107 }
108
Alan Viverette708aa9d2015-11-20 15:21:30 -0500109 /**
110 * Sets the alignment of the popup window relative to the anchor view.
111 * <p>
112 * Changes take effect on the next call to show().
113 *
114 * @param gravity alignment of the popup relative to the anchor
115 */
Adam Powell54c94de2013-09-26 15:36:34 -0700116 public void setGravity(int gravity) {
117 mDropDownGravity = gravity;
118 }
119
Alan Viverette708aa9d2015-11-20 15:21:30 -0500120 /**
121 * @return alignment of the popup relative to the anchor
122 */
Alan Viverette75d83792015-01-07 15:51:54 -0800123 public int getGravity() {
124 return mDropDownGravity;
125 }
126
Adam Powell42675342010-07-09 18:02:59 -0700127 public void show() {
Adam Powell5e3f2842011-01-07 17:16:56 -0800128 if (!tryShow()) {
129 throw new IllegalStateException("MenuPopupHelper cannot be used without an anchor");
130 }
131 }
132
Oren Blasberged391262015-09-01 12:12:51 -0700133 public void show(int x, int y) {
134 if (!tryShow(x, y)) {
135 throw new IllegalStateException("MenuPopupHelper cannot be used without an anchor");
136 }
137 }
138
Alan Viverette708aa9d2015-11-20 15:21:30 -0500139 @NonNull
140 public MenuPopup getPopup() {
141 if (mPopup == null) {
142 mPopup = createPopup();
143 }
Alan Viveretteca6a36112013-08-16 14:41:06 -0700144 return mPopup;
145 }
146
Alan Viverette8fd949e2015-03-11 12:21:30 -0700147 /**
Oren Blasbergb23976e2015-09-01 14:55:42 -0700148 * Attempts to show the popup anchored to the view specified by {@link #setAnchorView(View)}.
Alan Viverette8fd949e2015-03-11 12:21:30 -0700149 *
Oren Blasbergb23976e2015-09-01 14:55:42 -0700150 * @return {@code true} if the popup was shown or was already showing prior to calling this
151 * method, {@code false} otherwise
Alan Viverette8fd949e2015-03-11 12:21:30 -0700152 */
Adam Powell5e3f2842011-01-07 17:16:56 -0800153 public boolean tryShow() {
Alan Viverette8fd949e2015-03-11 12:21:30 -0700154 if (isShowing()) {
155 return true;
156 }
157
Oren Blasberg99162822015-09-10 14:37:26 -0700158 if (mAnchorView == null) {
Adam Powell5e3f2842011-01-07 17:16:56 -0800159 return false;
Adam Powell8028dd32010-07-15 10:16:33 -0700160 }
Adam Powell42675342010-07-09 18:02:59 -0700161
Alan Viverette708aa9d2015-11-20 15:21:30 -0500162 showPopup(0, 0, false, false);
Oren Blasberged391262015-09-01 12:12:51 -0700163 return true;
164 }
165
Alan Viveretted6443f62015-11-20 13:57:15 -0500166 /**
167 * Shows the popup menu and makes a best-effort to anchor it to the
168 * specified (x,y) coordinate relative to the anchor view.
169 * <p>
Alan Viverette91098572016-01-19 14:07:31 -0500170 * Additionally, the popup's transition epicenter (see
171 * {@link android.widget.PopupWindow#setEpicenterBounds(Rect)} will be
172 * centered on the specified coordinate, rather than using the bounds of
173 * the anchor view.
174 * <p>
Alan Viveretted6443f62015-11-20 13:57:15 -0500175 * If the popup's resolved gravity is {@link Gravity#LEFT}, this will
176 * display the popup with its top-left corner at (x,y) relative to the
177 * anchor view. If the resolved gravity is {@link Gravity#RIGHT}, the
178 * popup's top-right corner will be at (x,y).
179 * <p>
180 * If the popup cannot be displayed fully on-screen, this method will
181 * attempt to scroll the anchor view's ancestors and/or offset the popup
182 * such that it may be displayed fully on-screen.
183 *
184 * @param x x coordinate relative to the anchor view
185 * @param y y coordinate relative to the anchor view
186 * @return {@code true} if the popup was shown or was already showing prior
187 * to calling this method, {@code false} otherwise
188 */
Oren Blasberged391262015-09-01 12:12:51 -0700189 public boolean tryShow(int x, int y) {
190 if (isShowing()) {
191 return true;
192 }
193
194 if (mAnchorView == null) {
195 return false;
196 }
197
Alan Viverette708aa9d2015-11-20 15:21:30 -0500198 showPopup(x, y, true, true);
Oren Blasberged391262015-09-01 12:12:51 -0700199 return true;
200 }
201
Alan Viverette708aa9d2015-11-20 15:21:30 -0500202 /**
203 * Creates the popup and assigns cached properties.
204 *
205 * @return an initialized popup
206 */
207 @NonNull
208 private MenuPopup createPopup() {
209 final boolean enableCascadingSubmenus = mContext.getResources().getBoolean(
210 com.android.internal.R.bool.config_enableCascadingSubmenus);
Alan Viveretted6443f62015-11-20 13:57:15 -0500211
Alan Viverette708aa9d2015-11-20 15:21:30 -0500212 final MenuPopup popup;
213 if (enableCascadingSubmenus) {
214 popup = new CascadingMenuPopup(mContext, mAnchorView, mPopupStyleAttr,
215 mPopupStyleRes, mOverflowOnly);
216 } else {
217 popup = new StandardMenuPopup(mContext, mMenu, mAnchorView, mPopupStyleAttr,
218 mPopupStyleRes, mOverflowOnly);
219 }
220
221 // Assign immutable properties.
222 popup.addMenu(mMenu);
223 popup.setOnDismissListener(mInternalOnDismissListener);
224
225 // Assign mutable properties. These may be reassigned later.
226 popup.setAnchorView(mAnchorView);
227 popup.setCallback(mPresenterCallback);
228 popup.setForceShowIcon(mForceShowIcon);
229 popup.setGravity(mDropDownGravity);
230
231 return popup;
232 }
233
Alan Viverette91098572016-01-19 14:07:31 -0500234 private void showPopup(int xOffset, int yOffset, boolean useOffsets, boolean showTitle) {
235 final MenuPopup popup = getPopup();
236 popup.setShowTitle(showTitle);
237
238 if (useOffsets) {
Alan Viveretted6443f62015-11-20 13:57:15 -0500239 // If the resolved drop-down gravity is RIGHT, the popup's right
240 // edge will be aligned with the anchor view. Adjust by the anchor
241 // width such that the top-right corner is at the X offset.
242 final int hgrav = Gravity.getAbsoluteGravity(mDropDownGravity,
243 mAnchorView.getLayoutDirection()) & Gravity.HORIZONTAL_GRAVITY_MASK;
Alan Viveretted6443f62015-11-20 13:57:15 -0500244 if (hgrav == Gravity.RIGHT) {
Alan Viverette708aa9d2015-11-20 15:21:30 -0500245 xOffset -= mAnchorView.getWidth();
Alan Viveretted6443f62015-11-20 13:57:15 -0500246 }
Alan Viverette91098572016-01-19 14:07:31 -0500247
248 popup.setHorizontalOffset(xOffset);
249 popup.setVerticalOffset(yOffset);
250
251 // Set the transition epicenter to be roughly finger (or mouse
252 // cursor) sized and centered around the offset position. This
253 // will give the appearance that the window is emerging from
254 // the touch point.
255 final float density = mContext.getResources().getDisplayMetrics().density;
256 final int halfSize = (int) (TOUCH_EPICENTER_SIZE_DP * density / 2);
257 final Rect epicenter = new Rect(xOffset - halfSize, yOffset - halfSize,
258 xOffset + halfSize, yOffset + halfSize);
259 popup.setEpicenterBounds(epicenter);
Alan Viveretted6443f62015-11-20 13:57:15 -0500260 }
Oren Blasberg99162822015-09-10 14:37:26 -0700261
Alan Viverette708aa9d2015-11-20 15:21:30 -0500262 popup.show();
Adam Powell42675342010-07-09 18:02:59 -0700263 }
264
Alan Viverette708aa9d2015-11-20 15:21:30 -0500265 /**
266 * Dismisses the popup, if showing.
267 */
Alan Viverette021627e2015-11-25 14:22:00 -0500268 @Override
Adam Powell42675342010-07-09 18:02:59 -0700269 public void dismiss() {
Adam Powell3d3da272010-08-11 18:06:17 -0700270 if (isShowing()) {
271 mPopup.dismiss();
272 }
Adam Powell8515ee82010-11-30 14:09:55 -0800273 }
274
Alan Viverette708aa9d2015-11-20 15:21:30 -0500275 /**
276 * Called after the popup has been dismissed.
277 * <p>
278 * <strong>Note:</strong> Subclasses should call the super implementation
279 * last to ensure that any necessary tear down has occurred before the
280 * listener specified by {@link #setOnDismissListener(OnDismissListener)}
281 * is called.
282 */
283 protected void onDismiss() {
Adam Powell8515ee82010-11-30 14:09:55 -0800284 mPopup = null;
Alan Viverette708aa9d2015-11-20 15:21:30 -0500285
286 if (mOnDismissListener != null) {
287 mOnDismissListener.onDismiss();
288 }
Adam Powell42675342010-07-09 18:02:59 -0700289 }
290
Adam Powell8028dd32010-07-15 10:16:33 -0700291 public boolean isShowing() {
292 return mPopup != null && mPopup.isShowing();
293 }
294
Alan Viverette021627e2015-11-25 14:22:00 -0500295 @Override
296 public void setPresenterCallback(@Nullable MenuPresenter.Callback cb) {
Oren Blasberg99162822015-09-10 14:37:26 -0700297 mPresenterCallback = cb;
Alan Viverette708aa9d2015-11-20 15:21:30 -0500298 if (mPopup != null) {
299 mPopup.setCallback(cb);
300 }
Adam Powell696cba52011-03-29 10:38:16 -0700301 }
Alan Viverette708aa9d2015-11-20 15:21:30 -0500302
303 /**
304 * Listener used to proxy dismiss callbacks to the helper's owner.
305 */
306 private final OnDismissListener mInternalOnDismissListener = new OnDismissListener() {
307 @Override
308 public void onDismiss() {
309 MenuPopupHelper.this.onDismiss();
310 }
311 };
Adam Powell42675342010-07-09 18:02:59 -0700312}