blob: 9f029ad2e50740aabc87db8c07d6dc217c67d075 [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;
Oren Blasberg7b457452016-04-19 15:44:46 -0700107 if (mPopup != null) {
108 mPopup.setForceShowIcon(forceShowIcon);
109 }
Adam Powell91511032011-07-13 10:24:06 -0700110 }
111
Alan Viverette708aa9d2015-11-20 15:21:30 -0500112 /**
113 * Sets the alignment of the popup window relative to the anchor view.
114 * <p>
115 * Changes take effect on the next call to show().
116 *
117 * @param gravity alignment of the popup relative to the anchor
118 */
Adam Powell54c94de2013-09-26 15:36:34 -0700119 public void setGravity(int gravity) {
120 mDropDownGravity = gravity;
121 }
122
Alan Viverette708aa9d2015-11-20 15:21:30 -0500123 /**
124 * @return alignment of the popup relative to the anchor
125 */
Alan Viverette75d83792015-01-07 15:51:54 -0800126 public int getGravity() {
127 return mDropDownGravity;
128 }
129
Adam Powell42675342010-07-09 18:02:59 -0700130 public void show() {
Adam Powell5e3f2842011-01-07 17:16:56 -0800131 if (!tryShow()) {
132 throw new IllegalStateException("MenuPopupHelper cannot be used without an anchor");
133 }
134 }
135
Oren Blasberged391262015-09-01 12:12:51 -0700136 public void show(int x, int y) {
137 if (!tryShow(x, y)) {
138 throw new IllegalStateException("MenuPopupHelper cannot be used without an anchor");
139 }
140 }
141
Alan Viverette708aa9d2015-11-20 15:21:30 -0500142 @NonNull
143 public MenuPopup getPopup() {
144 if (mPopup == null) {
145 mPopup = createPopup();
146 }
Alan Viveretteca6a36112013-08-16 14:41:06 -0700147 return mPopup;
148 }
149
Alan Viverette8fd949e2015-03-11 12:21:30 -0700150 /**
Oren Blasbergb23976e2015-09-01 14:55:42 -0700151 * Attempts to show the popup anchored to the view specified by {@link #setAnchorView(View)}.
Alan Viverette8fd949e2015-03-11 12:21:30 -0700152 *
Oren Blasbergb23976e2015-09-01 14:55:42 -0700153 * @return {@code true} if the popup was shown or was already showing prior to calling this
154 * method, {@code false} otherwise
Alan Viverette8fd949e2015-03-11 12:21:30 -0700155 */
Adam Powell5e3f2842011-01-07 17:16:56 -0800156 public boolean tryShow() {
Alan Viverette8fd949e2015-03-11 12:21:30 -0700157 if (isShowing()) {
158 return true;
159 }
160
Oren Blasberg99162822015-09-10 14:37:26 -0700161 if (mAnchorView == null) {
Adam Powell5e3f2842011-01-07 17:16:56 -0800162 return false;
Adam Powell8028dd32010-07-15 10:16:33 -0700163 }
Adam Powell42675342010-07-09 18:02:59 -0700164
Alan Viverette708aa9d2015-11-20 15:21:30 -0500165 showPopup(0, 0, false, false);
Oren Blasberged391262015-09-01 12:12:51 -0700166 return true;
167 }
168
Alan Viveretted6443f62015-11-20 13:57:15 -0500169 /**
170 * Shows the popup menu and makes a best-effort to anchor it to the
171 * specified (x,y) coordinate relative to the anchor view.
172 * <p>
Alan Viverette91098572016-01-19 14:07:31 -0500173 * Additionally, the popup's transition epicenter (see
174 * {@link android.widget.PopupWindow#setEpicenterBounds(Rect)} will be
175 * centered on the specified coordinate, rather than using the bounds of
176 * the anchor view.
177 * <p>
Alan Viveretted6443f62015-11-20 13:57:15 -0500178 * If the popup's resolved gravity is {@link Gravity#LEFT}, this will
179 * display the popup with its top-left corner at (x,y) relative to the
180 * anchor view. If the resolved gravity is {@link Gravity#RIGHT}, the
181 * popup's top-right corner will be at (x,y).
182 * <p>
183 * If the popup cannot be displayed fully on-screen, this method will
184 * attempt to scroll the anchor view's ancestors and/or offset the popup
185 * such that it may be displayed fully on-screen.
186 *
187 * @param x x coordinate relative to the anchor view
188 * @param y y coordinate relative to the anchor view
189 * @return {@code true} if the popup was shown or was already showing prior
190 * to calling this method, {@code false} otherwise
191 */
Oren Blasberged391262015-09-01 12:12:51 -0700192 public boolean tryShow(int x, int y) {
193 if (isShowing()) {
194 return true;
195 }
196
197 if (mAnchorView == null) {
198 return false;
199 }
200
Alan Viverette708aa9d2015-11-20 15:21:30 -0500201 showPopup(x, y, true, true);
Oren Blasberged391262015-09-01 12:12:51 -0700202 return true;
203 }
204
Alan Viverette708aa9d2015-11-20 15:21:30 -0500205 /**
206 * Creates the popup and assigns cached properties.
207 *
208 * @return an initialized popup
209 */
210 @NonNull
211 private MenuPopup createPopup() {
212 final boolean enableCascadingSubmenus = mContext.getResources().getBoolean(
213 com.android.internal.R.bool.config_enableCascadingSubmenus);
Alan Viveretted6443f62015-11-20 13:57:15 -0500214
Alan Viverette708aa9d2015-11-20 15:21:30 -0500215 final MenuPopup popup;
216 if (enableCascadingSubmenus) {
217 popup = new CascadingMenuPopup(mContext, mAnchorView, mPopupStyleAttr,
218 mPopupStyleRes, mOverflowOnly);
219 } else {
220 popup = new StandardMenuPopup(mContext, mMenu, mAnchorView, mPopupStyleAttr,
221 mPopupStyleRes, mOverflowOnly);
222 }
223
224 // Assign immutable properties.
225 popup.addMenu(mMenu);
226 popup.setOnDismissListener(mInternalOnDismissListener);
227
228 // Assign mutable properties. These may be reassigned later.
229 popup.setAnchorView(mAnchorView);
230 popup.setCallback(mPresenterCallback);
231 popup.setForceShowIcon(mForceShowIcon);
232 popup.setGravity(mDropDownGravity);
233
234 return popup;
235 }
236
Alan Viverette91098572016-01-19 14:07:31 -0500237 private void showPopup(int xOffset, int yOffset, boolean useOffsets, boolean showTitle) {
238 final MenuPopup popup = getPopup();
239 popup.setShowTitle(showTitle);
240
241 if (useOffsets) {
Alan Viveretted6443f62015-11-20 13:57:15 -0500242 // If the resolved drop-down gravity is RIGHT, the popup's right
243 // edge will be aligned with the anchor view. Adjust by the anchor
244 // width such that the top-right corner is at the X offset.
245 final int hgrav = Gravity.getAbsoluteGravity(mDropDownGravity,
246 mAnchorView.getLayoutDirection()) & Gravity.HORIZONTAL_GRAVITY_MASK;
Alan Viveretted6443f62015-11-20 13:57:15 -0500247 if (hgrav == Gravity.RIGHT) {
Alan Viverette708aa9d2015-11-20 15:21:30 -0500248 xOffset -= mAnchorView.getWidth();
Alan Viveretted6443f62015-11-20 13:57:15 -0500249 }
Alan Viverette91098572016-01-19 14:07:31 -0500250
251 popup.setHorizontalOffset(xOffset);
252 popup.setVerticalOffset(yOffset);
253
254 // Set the transition epicenter to be roughly finger (or mouse
255 // cursor) sized and centered around the offset position. This
256 // will give the appearance that the window is emerging from
257 // the touch point.
258 final float density = mContext.getResources().getDisplayMetrics().density;
259 final int halfSize = (int) (TOUCH_EPICENTER_SIZE_DP * density / 2);
260 final Rect epicenter = new Rect(xOffset - halfSize, yOffset - halfSize,
261 xOffset + halfSize, yOffset + halfSize);
262 popup.setEpicenterBounds(epicenter);
Alan Viveretted6443f62015-11-20 13:57:15 -0500263 }
Oren Blasberg99162822015-09-10 14:37:26 -0700264
Alan Viverette708aa9d2015-11-20 15:21:30 -0500265 popup.show();
Adam Powell42675342010-07-09 18:02:59 -0700266 }
267
Alan Viverette708aa9d2015-11-20 15:21:30 -0500268 /**
269 * Dismisses the popup, if showing.
270 */
Alan Viverette021627e2015-11-25 14:22:00 -0500271 @Override
Adam Powell42675342010-07-09 18:02:59 -0700272 public void dismiss() {
Adam Powell3d3da272010-08-11 18:06:17 -0700273 if (isShowing()) {
274 mPopup.dismiss();
275 }
Adam Powell8515ee82010-11-30 14:09:55 -0800276 }
277
Alan Viverette708aa9d2015-11-20 15:21:30 -0500278 /**
279 * Called after the popup has been dismissed.
280 * <p>
281 * <strong>Note:</strong> Subclasses should call the super implementation
282 * last to ensure that any necessary tear down has occurred before the
283 * listener specified by {@link #setOnDismissListener(OnDismissListener)}
284 * is called.
285 */
286 protected void onDismiss() {
Adam Powell8515ee82010-11-30 14:09:55 -0800287 mPopup = null;
Alan Viverette708aa9d2015-11-20 15:21:30 -0500288
289 if (mOnDismissListener != null) {
290 mOnDismissListener.onDismiss();
291 }
Adam Powell42675342010-07-09 18:02:59 -0700292 }
293
Adam Powell8028dd32010-07-15 10:16:33 -0700294 public boolean isShowing() {
295 return mPopup != null && mPopup.isShowing();
296 }
297
Alan Viverette021627e2015-11-25 14:22:00 -0500298 @Override
299 public void setPresenterCallback(@Nullable MenuPresenter.Callback cb) {
Oren Blasberg99162822015-09-10 14:37:26 -0700300 mPresenterCallback = cb;
Alan Viverette708aa9d2015-11-20 15:21:30 -0500301 if (mPopup != null) {
302 mPopup.setCallback(cb);
303 }
Adam Powell696cba52011-03-29 10:38:16 -0700304 }
Alan Viverette708aa9d2015-11-20 15:21:30 -0500305
306 /**
307 * Listener used to proxy dismiss callbacks to the helper's owner.
308 */
309 private final OnDismissListener mInternalOnDismissListener = new OnDismissListener() {
310 @Override
311 public void onDismiss() {
312 MenuPopupHelper.this.onDismiss();
313 }
314 };
Adam Powell42675342010-07-09 18:02:59 -0700315}