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