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