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 com.android.internal.view.menu.MenuPresenter.Callback; 20 21 import android.annotation.AttrRes; 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.annotation.StyleRes; 25 import android.content.Context; 26 import android.graphics.Point; 27 import android.graphics.Rect; 28 import android.util.DisplayMetrics; 29 import android.view.Display; 30 import android.view.Gravity; 31 import android.view.View; 32 import android.view.WindowManager; 33 import android.widget.PopupWindow.OnDismissListener; 34 35 /** 36 * Presents a menu as a small, simple popup anchored to another view. 37 */ 38 public class MenuPopupHelper implements MenuHelper { 39 private static final int TOUCH_EPICENTER_SIZE_DP = 48; 40 41 private final Context mContext; 42 43 // Immutable cached popup menu properties. 44 private final MenuBuilder mMenu; 45 private final boolean mOverflowOnly; 46 private final int mPopupStyleAttr; 47 private final int mPopupStyleRes; 48 49 // Mutable cached popup menu properties. 50 private View mAnchorView; 51 private int mDropDownGravity = Gravity.START; 52 private boolean mForceShowIcon; 53 private Callback mPresenterCallback; 54 55 private MenuPopup mPopup; 56 private OnDismissListener mOnDismissListener; 57 MenuPopupHelper(@onNull Context context, @NonNull MenuBuilder menu)58 public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu) { 59 this(context, menu, null, false, com.android.internal.R.attr.popupMenuStyle, 0); 60 } 61 MenuPopupHelper(@onNull Context context, @NonNull MenuBuilder menu, @NonNull View anchorView)62 public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu, 63 @NonNull View anchorView) { 64 this(context, menu, anchorView, false, com.android.internal.R.attr.popupMenuStyle, 0); 65 } 66 MenuPopupHelper(@onNull Context context, @NonNull MenuBuilder menu, @NonNull View anchorView, boolean overflowOnly, @AttrRes int popupStyleAttr)67 public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu, 68 @NonNull View anchorView, 69 boolean overflowOnly, @AttrRes int popupStyleAttr) { 70 this(context, menu, anchorView, overflowOnly, popupStyleAttr, 0); 71 } 72 MenuPopupHelper(@onNull Context context, @NonNull MenuBuilder menu, @NonNull View anchorView, boolean overflowOnly, @AttrRes int popupStyleAttr, @StyleRes int popupStyleRes)73 public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu, 74 @NonNull View anchorView, boolean overflowOnly, @AttrRes int popupStyleAttr, 75 @StyleRes int popupStyleRes) { 76 mContext = context; 77 mMenu = menu; 78 mAnchorView = anchorView; 79 mOverflowOnly = overflowOnly; 80 mPopupStyleAttr = popupStyleAttr; 81 mPopupStyleRes = popupStyleRes; 82 } 83 setOnDismissListener(@ullable OnDismissListener listener)84 public void setOnDismissListener(@Nullable OnDismissListener listener) { 85 mOnDismissListener = listener; 86 } 87 88 /** 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 */ setAnchorView(@onNull View anchor)95 public void setAnchorView(@NonNull View anchor) { 96 mAnchorView = anchor; 97 } 98 99 /** 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 */ setForceShowIcon(boolean forceShowIcon)108 public void setForceShowIcon(boolean forceShowIcon) { 109 mForceShowIcon = forceShowIcon; 110 if (mPopup != null) { 111 mPopup.setForceShowIcon(forceShowIcon); 112 } 113 } 114 115 /** 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 */ setGravity(int gravity)122 public void setGravity(int gravity) { 123 mDropDownGravity = gravity; 124 } 125 126 /** 127 * @return alignment of the popup relative to the anchor 128 */ getGravity()129 public int getGravity() { 130 return mDropDownGravity; 131 } 132 show()133 public void show() { 134 if (!tryShow()) { 135 throw new IllegalStateException("MenuPopupHelper cannot be used without an anchor"); 136 } 137 } 138 show(int x, int y)139 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 145 @NonNull getPopup()146 public MenuPopup getPopup() { 147 if (mPopup == null) { 148 mPopup = createPopup(); 149 } 150 return mPopup; 151 } 152 153 /** 154 * Attempts to show the popup anchored to the view specified by {@link #setAnchorView(View)}. 155 * 156 * @return {@code true} if the popup was shown or was already showing prior to calling this 157 * method, {@code false} otherwise 158 */ tryShow()159 public boolean tryShow() { 160 if (isShowing()) { 161 return true; 162 } 163 164 if (mAnchorView == null) { 165 return false; 166 } 167 168 showPopup(0, 0, false, false); 169 return true; 170 } 171 172 /** 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> 176 * 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> 181 * 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 */ tryShow(int x, int y)195 public boolean tryShow(int x, int y) { 196 if (isShowing()) { 197 return true; 198 } 199 200 if (mAnchorView == null) { 201 return false; 202 } 203 204 showPopup(x, y, true, true); 205 return true; 206 } 207 208 /** 209 * Creates the popup and assigns cached properties. 210 * 211 * @return an initialized popup 212 */ 213 @NonNull createPopup()214 private MenuPopup createPopup() { 215 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; 225 226 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 showPopup(int xOffset, int yOffset, boolean useOffsets, boolean showTitle)248 private void showPopup(int xOffset, int yOffset, boolean useOffsets, boolean showTitle) { 249 final MenuPopup popup = getPopup(); 250 popup.setShowTitle(showTitle); 251 252 if (useOffsets) { 253 // 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; 258 if (hgrav == Gravity.RIGHT) { 259 xOffset -= mAnchorView.getWidth(); 260 } 261 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); 274 } 275 276 popup.show(); 277 } 278 279 /** 280 * Dismisses the popup, if showing. 281 */ 282 @Override dismiss()283 public void dismiss() { 284 if (isShowing()) { 285 mPopup.dismiss(); 286 } 287 } 288 289 /** 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 */ onDismiss()297 protected void onDismiss() { 298 mPopup = null; 299 300 if (mOnDismissListener != null) { 301 mOnDismissListener.onDismiss(); 302 } 303 } 304 isShowing()305 public boolean isShowing() { 306 return mPopup != null && mPopup.isShowing(); 307 } 308 309 @Override setPresenterCallback(@ullable MenuPresenter.Callback cb)310 public void setPresenterCallback(@Nullable MenuPresenter.Callback cb) { 311 mPresenterCallback = cb; 312 if (mPopup != null) { 313 mPopup.setCallback(cb); 314 } 315 } 316 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 }; 326 } 327