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