1 /* 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 17 package com.android.internal.view.menu; 18 19 import android.annotation.AttrRes; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.annotation.StyleRes; 23 import android.compat.annotation.UnsupportedAppUsage; 24 import android.content.Context; 25 import android.graphics.Rect; 26 import android.os.Build; 27 import android.view.Gravity; 28 import android.view.View; 29 import android.view.WindowManager; 30 import android.widget.PopupWindow.OnDismissListener; 31 32 import com.android.internal.view.menu.MenuPresenter.Callback; 33 34 /** 35 * Presents a menu as a small, simple popup anchored to another view. 36 */ 37 public class MenuPopupHelper implements MenuHelper { 38 private static final int TOUCH_EPICENTER_SIZE_DP = 48; 39 40 private final Context mContext; 41 42 // Immutable cached popup menu properties. 43 private final MenuBuilder mMenu; 44 private final boolean mOverflowOnly; 45 private final int mPopupStyleAttr; 46 private final int mPopupStyleRes; 47 48 // Mutable cached popup menu properties. 49 private View mAnchorView; 50 private int mDropDownGravity = Gravity.START; 51 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 52 private boolean mForceShowIcon; 53 private Callback mPresenterCallback; 54 55 private MenuPopup mPopup; 56 private OnDismissListener mOnDismissListener; 57 58 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) MenuPopupHelper(@onNull Context context, @NonNull MenuBuilder menu)59 public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu) { 60 this(context, menu, null, false, com.android.internal.R.attr.popupMenuStyle, 0); 61 } 62 63 @UnsupportedAppUsage MenuPopupHelper(@onNull Context context, @NonNull MenuBuilder menu, @NonNull View anchorView)64 public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu, 65 @NonNull View anchorView) { 66 this(context, menu, anchorView, false, com.android.internal.R.attr.popupMenuStyle, 0); 67 } 68 MenuPopupHelper(@onNull Context context, @NonNull MenuBuilder menu, @NonNull View anchorView, boolean overflowOnly, @AttrRes int popupStyleAttr)69 public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu, 70 @NonNull View anchorView, 71 boolean overflowOnly, @AttrRes int popupStyleAttr) { 72 this(context, menu, anchorView, overflowOnly, popupStyleAttr, 0); 73 } 74 MenuPopupHelper(@onNull Context context, @NonNull MenuBuilder menu, @NonNull View anchorView, boolean overflowOnly, @AttrRes int popupStyleAttr, @StyleRes int popupStyleRes)75 public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu, 76 @NonNull View anchorView, boolean overflowOnly, @AttrRes int popupStyleAttr, 77 @StyleRes int popupStyleRes) { 78 mContext = context; 79 mMenu = menu; 80 mAnchorView = anchorView; 81 mOverflowOnly = overflowOnly; 82 mPopupStyleAttr = popupStyleAttr; 83 mPopupStyleRes = popupStyleRes; 84 } 85 setOnDismissListener(@ullable OnDismissListener listener)86 public void setOnDismissListener(@Nullable OnDismissListener listener) { 87 mOnDismissListener = listener; 88 } 89 90 /** 91 * Sets the view to which the popup window is anchored. 92 * <p> 93 * Changes take effect on the next call to show(). 94 * 95 * @param anchor the view to which the popup window should be anchored 96 */ 97 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) setAnchorView(@onNull View anchor)98 public void setAnchorView(@NonNull View anchor) { 99 mAnchorView = anchor; 100 } 101 102 /** 103 * Sets whether the popup menu's adapter is forced to show icons in the 104 * menu item views. 105 * <p> 106 * Changes take effect on the next call to show(). 107 * 108 * This method should not be accessed directly outside the framework, please use 109 * {@link android.widget.PopupMenu#setForceShowIcon(boolean)} instead. 110 * 111 * @param forceShowIcon {@code true} to force icons to be shown, or 112 * {@code false} for icons to be optionally shown 113 */ 114 @UnsupportedAppUsage setForceShowIcon(boolean forceShowIcon)115 public void setForceShowIcon(boolean forceShowIcon) { 116 mForceShowIcon = forceShowIcon; 117 if (mPopup != null) { 118 mPopup.setForceShowIcon(forceShowIcon); 119 } 120 } 121 122 /** 123 * Sets the alignment of the popup window relative to the anchor view. 124 * <p> 125 * Changes take effect on the next call to show(). 126 * 127 * @param gravity alignment of the popup relative to the anchor 128 */ 129 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) setGravity(int gravity)130 public void setGravity(int gravity) { 131 mDropDownGravity = gravity; 132 } 133 134 /** 135 * @return alignment of the popup relative to the anchor 136 */ getGravity()137 public int getGravity() { 138 return mDropDownGravity; 139 } 140 141 @UnsupportedAppUsage show()142 public void show() { 143 if (!tryShow()) { 144 throw new IllegalStateException("MenuPopupHelper cannot be used without an anchor"); 145 } 146 } 147 show(int x, int y)148 public void show(int x, int y) { 149 if (!tryShow(x, y)) { 150 throw new IllegalStateException("MenuPopupHelper cannot be used without an anchor"); 151 } 152 } 153 154 @NonNull 155 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) getPopup()156 public MenuPopup getPopup() { 157 if (mPopup == null) { 158 mPopup = createPopup(); 159 } 160 return mPopup; 161 } 162 163 /** 164 * Attempts to show the popup anchored to the view specified by {@link #setAnchorView(View)}. 165 * 166 * @return {@code true} if the popup was shown or was already showing prior to calling this 167 * method, {@code false} otherwise 168 */ 169 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) tryShow()170 public boolean tryShow() { 171 if (isShowing()) { 172 return true; 173 } 174 175 if (mAnchorView == null) { 176 return false; 177 } 178 179 showPopup(0, 0, false, false); 180 return true; 181 } 182 183 /** 184 * Shows the popup menu and makes a best-effort to anchor it to the 185 * specified (x,y) coordinate relative to the anchor view. 186 * <p> 187 * Additionally, the popup's transition epicenter (see 188 * {@link android.widget.PopupWindow#setEpicenterBounds(Rect)} will be 189 * centered on the specified coordinate, rather than using the bounds of 190 * the anchor view. 191 * <p> 192 * If the popup's resolved gravity is {@link Gravity#LEFT}, this will 193 * display the popup with its top-left corner at (x,y) relative to the 194 * anchor view. If the resolved gravity is {@link Gravity#RIGHT}, the 195 * popup's top-right corner will be at (x,y). 196 * <p> 197 * If the popup cannot be displayed fully on-screen, this method will 198 * attempt to scroll the anchor view's ancestors and/or offset the popup 199 * such that it may be displayed fully on-screen. 200 * 201 * @param x x coordinate relative to the anchor view 202 * @param y y coordinate relative to the anchor view 203 * @return {@code true} if the popup was shown or was already showing prior 204 * to calling this method, {@code false} otherwise 205 */ tryShow(int x, int y)206 public boolean tryShow(int x, int y) { 207 if (isShowing()) { 208 return true; 209 } 210 211 if (mAnchorView == null) { 212 return false; 213 } 214 215 showPopup(x, y, true, true); 216 return true; 217 } 218 219 /** 220 * Creates the popup and assigns cached properties. 221 * 222 * @return an initialized popup 223 */ 224 @NonNull createPopup()225 private MenuPopup createPopup() { 226 final WindowManager windowManager = mContext.getSystemService(WindowManager.class); 227 final Rect maxWindowBounds = windowManager.getMaximumWindowMetrics().getBounds(); 228 229 final int smallestWidth = Math.min(maxWindowBounds.width(), maxWindowBounds.height()); 230 final int minSmallestWidthCascading = mContext.getResources().getDimensionPixelSize( 231 com.android.internal.R.dimen.cascading_menus_min_smallest_width); 232 final boolean enableCascadingSubmenus = smallestWidth >= minSmallestWidthCascading; 233 234 final MenuPopup popup; 235 if (enableCascadingSubmenus) { 236 popup = new CascadingMenuPopup(mContext, mAnchorView, mPopupStyleAttr, 237 mPopupStyleRes, mOverflowOnly); 238 } else { 239 popup = new StandardMenuPopup(mContext, mMenu, mAnchorView, mPopupStyleAttr, 240 mPopupStyleRes, mOverflowOnly); 241 } 242 243 // Assign immutable properties. 244 popup.addMenu(mMenu); 245 popup.setOnDismissListener(mInternalOnDismissListener); 246 247 // Assign mutable properties. These may be reassigned later. 248 popup.setAnchorView(mAnchorView); 249 popup.setCallback(mPresenterCallback); 250 popup.setForceShowIcon(mForceShowIcon); 251 popup.setGravity(mDropDownGravity); 252 253 return popup; 254 } 255 showPopup(int xOffset, int yOffset, boolean useOffsets, boolean showTitle)256 private void showPopup(int xOffset, int yOffset, boolean useOffsets, boolean showTitle) { 257 final MenuPopup popup = getPopup(); 258 popup.setShowTitle(showTitle); 259 260 if (useOffsets) { 261 // If the resolved drop-down gravity is RIGHT, the popup's right 262 // edge will be aligned with the anchor view. Adjust by the anchor 263 // width such that the top-right corner is at the X offset. 264 final int hgrav = Gravity.getAbsoluteGravity(mDropDownGravity, 265 mAnchorView.getLayoutDirection()) & Gravity.HORIZONTAL_GRAVITY_MASK; 266 if (hgrav == Gravity.RIGHT) { 267 xOffset -= mAnchorView.getWidth(); 268 } 269 270 popup.setHorizontalOffset(xOffset); 271 popup.setVerticalOffset(yOffset); 272 273 // Set the transition epicenter to be roughly finger (or mouse 274 // cursor) sized and centered around the offset position. This 275 // will give the appearance that the window is emerging from 276 // the touch point. 277 final float density = mContext.getResources().getDisplayMetrics().density; 278 final int halfSize = (int) (TOUCH_EPICENTER_SIZE_DP * density / 2); 279 final Rect epicenter = new Rect(xOffset - halfSize, yOffset - halfSize, 280 xOffset + halfSize, yOffset + halfSize); 281 popup.setEpicenterBounds(epicenter); 282 } 283 284 popup.show(); 285 } 286 287 /** 288 * Dismisses the popup, if showing. 289 */ 290 @Override 291 @UnsupportedAppUsage dismiss()292 public void dismiss() { 293 if (isShowing()) { 294 mPopup.dismiss(); 295 } 296 } 297 298 /** 299 * Called after the popup has been dismissed. 300 * <p> 301 * <strong>Note:</strong> Subclasses should call the super implementation 302 * last to ensure that any necessary tear down has occurred before the 303 * listener specified by {@link #setOnDismissListener(OnDismissListener)} 304 * is called. 305 */ onDismiss()306 protected void onDismiss() { 307 mPopup = null; 308 309 if (mOnDismissListener != null) { 310 mOnDismissListener.onDismiss(); 311 } 312 } 313 isShowing()314 public boolean isShowing() { 315 return mPopup != null && mPopup.isShowing(); 316 } 317 318 @Override setPresenterCallback(@ullable MenuPresenter.Callback cb)319 public void setPresenterCallback(@Nullable MenuPresenter.Callback cb) { 320 mPresenterCallback = cb; 321 if (mPopup != null) { 322 mPopup.setCallback(cb); 323 } 324 } 325 326 /** 327 * Listener used to proxy dismiss callbacks to the helper's owner. 328 */ 329 private final OnDismissListener mInternalOnDismissListener = new OnDismissListener() { 330 @Override 331 public void onDismiss() { 332 MenuPopupHelper.this.onDismiss(); 333 } 334 }; 335 } 336