1 /* 2 * Copyright (C) 2014 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 androidx.appcompat.widget; 18 19 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX; 20 21 import android.content.Context; 22 import android.view.Gravity; 23 import android.view.Menu; 24 import android.view.MenuInflater; 25 import android.view.MenuItem; 26 import android.view.View; 27 import android.widget.ListView; 28 import android.widget.PopupWindow; 29 30 import androidx.annotation.AttrRes; 31 import androidx.annotation.MenuRes; 32 import androidx.annotation.RestrictTo; 33 import androidx.annotation.StyleRes; 34 import androidx.appcompat.R; 35 import androidx.appcompat.view.SupportMenuInflater; 36 import androidx.appcompat.view.menu.MenuBuilder; 37 import androidx.appcompat.view.menu.MenuPopupHelper; 38 import androidx.appcompat.view.menu.ShowableListMenu; 39 40 import org.jspecify.annotations.NonNull; 41 import org.jspecify.annotations.Nullable; 42 43 /** 44 * Static library support version of the framework's {@link android.widget.PopupMenu}. 45 * Used to write apps that run on platforms prior to Android 3.0. When running 46 * on Android 3.0 or above, this implementation is still used; it does not try 47 * to switch to the framework's implementation. See the framework SDK 48 * documentation for a class overview. 49 */ 50 public class PopupMenu { 51 private final Context mContext; 52 private final MenuBuilder mMenu; 53 private final View mAnchor; 54 final MenuPopupHelper mPopup; 55 56 OnMenuItemClickListener mMenuItemClickListener; 57 OnDismissListener mOnDismissListener; 58 private View.OnTouchListener mDragListener; 59 60 /** 61 * Constructor to create a new popup menu with an anchor view. 62 * 63 * @param context Context the popup menu is running in, through which it 64 * can access the current theme, resources, etc. 65 * @param anchor Anchor view for this popup. The popup will appear below 66 * the anchor if there is room, or above it if there is not. 67 */ PopupMenu(@onNull Context context, @NonNull View anchor)68 public PopupMenu(@NonNull Context context, @NonNull View anchor) { 69 this(context, anchor, Gravity.NO_GRAVITY); 70 } 71 72 /** 73 * Constructor to create a new popup menu with an anchor view and alignment 74 * gravity. 75 * 76 * @param context Context the popup menu is running in, through which it 77 * can access the current theme, resources, etc. 78 * @param anchor Anchor view for this popup. The popup will appear below 79 * the anchor if there is room, or above it if there is not. 80 * @param gravity The {@link Gravity} value for aligning the popup with its 81 * anchor. 82 */ PopupMenu(@onNull Context context, @NonNull View anchor, int gravity)83 public PopupMenu(@NonNull Context context, @NonNull View anchor, int gravity) { 84 this(context, anchor, gravity, R.attr.popupMenuStyle, 0); 85 } 86 87 /** 88 * Constructor a create a new popup menu with a specific style. 89 * 90 * @param context Context the popup menu is running in, through which it 91 * can access the current theme, resources, etc. 92 * @param anchor Anchor view for this popup. The popup will appear below 93 * the anchor if there is room, or above it if there is not. 94 * @param gravity The {@link Gravity} value for aligning the popup with its 95 * anchor. 96 * @param popupStyleAttr An attribute in the current theme that contains a 97 * reference to a style resource that supplies default values for 98 * the popup window. Can be 0 to not look for defaults. 99 * @param popupStyleRes A resource identifier of a style resource that 100 * supplies default values for the popup window, used only if 101 * popupStyleAttr is 0 or can not be found in the theme. Can be 0 102 * to not look for defaults. 103 */ PopupMenu(@onNull Context context, @NonNull View anchor, int gravity, @AttrRes int popupStyleAttr, @StyleRes int popupStyleRes)104 public PopupMenu(@NonNull Context context, @NonNull View anchor, int gravity, 105 @AttrRes int popupStyleAttr, @StyleRes int popupStyleRes) { 106 mContext = context; 107 mAnchor = anchor; 108 109 mMenu = new MenuBuilder(context); 110 mMenu.setCallback(new MenuBuilder.Callback() { 111 @Override 112 public boolean onMenuItemSelected(@NonNull MenuBuilder menu, @NonNull MenuItem item) { 113 if (mMenuItemClickListener != null) { 114 return mMenuItemClickListener.onMenuItemClick(item); 115 } 116 return false; 117 } 118 119 @Override 120 public void onMenuModeChange(@NonNull MenuBuilder menu) { 121 } 122 }); 123 124 mPopup = new MenuPopupHelper(context, mMenu, anchor, false, popupStyleAttr, popupStyleRes); 125 mPopup.setGravity(gravity); 126 mPopup.setOnDismissListener(new PopupWindow.OnDismissListener() { 127 @Override 128 public void onDismiss() { 129 if (mOnDismissListener != null) { 130 mOnDismissListener.onDismiss(PopupMenu.this); 131 } 132 } 133 }); 134 } 135 136 /** 137 * Sets the gravity used to align the popup window to its anchor view. 138 * <p> 139 * If the popup is showing, calling this method will take effect only 140 * the next time the popup is shown. 141 * 142 * @param gravity the gravity used to align the popup window 143 * @see #getGravity() 144 */ setGravity(int gravity)145 public void setGravity(int gravity) { 146 mPopup.setGravity(gravity); 147 } 148 149 /** 150 * @return the gravity used to align the popup window to its anchor view 151 * @see #setGravity(int) 152 */ getGravity()153 public int getGravity() { 154 return mPopup.getGravity(); 155 } 156 157 /** 158 * Returns an {@link View.OnTouchListener} that can be added to the anchor view 159 * to implement drag-to-open behavior. 160 * <p> 161 * When the listener is set on a view, touching that view and dragging 162 * outside of its bounds will open the popup window. Lifting will select 163 * the currently touched list item. 164 * <p> 165 * Example usage: 166 * <pre> 167 * PopupMenu myPopup = new PopupMenu(context, myAnchor); 168 * myAnchor.setOnTouchListener(myPopup.getDragToOpenListener()); 169 * </pre> 170 * 171 * @return a touch listener that controls drag-to-open behavior 172 */ getDragToOpenListener()173 public View.@NonNull OnTouchListener getDragToOpenListener() { 174 if (mDragListener == null) { 175 mDragListener = new ForwardingListener(mAnchor) { 176 @Override 177 protected boolean onForwardingStarted() { 178 show(); 179 return true; 180 } 181 182 @Override 183 protected boolean onForwardingStopped() { 184 dismiss(); 185 return true; 186 } 187 188 @Override 189 public ShowableListMenu getPopup() { 190 // This will be null until show() is called. 191 return mPopup.getPopup(); 192 } 193 }; 194 } 195 196 return mDragListener; 197 } 198 199 /** 200 * Returns the {@link Menu} associated with this popup. Populate the 201 * returned Menu with items before calling {@link #show()}. 202 * 203 * @return the {@link Menu} associated with this popup 204 * @see #show() 205 * @see #getMenuInflater() 206 */ getMenu()207 public @NonNull Menu getMenu() { 208 return mMenu; 209 } 210 211 /** 212 * @return a {@link MenuInflater} that can be used to inflate menu items 213 * from XML into the menu returned by {@link #getMenu()} 214 * @see #getMenu() 215 */ getMenuInflater()216 public @NonNull MenuInflater getMenuInflater() { 217 return new SupportMenuInflater(mContext); 218 } 219 220 /** 221 * Inflate a menu resource into this PopupMenu. This is equivalent to 222 * calling {@code popupMenu.getMenuInflater().inflate(menuRes, popupMenu.getMenu())}. 223 * 224 * @param menuRes Menu resource to inflate 225 */ inflate(@enuRes int menuRes)226 public void inflate(@MenuRes int menuRes) { 227 getMenuInflater().inflate(menuRes, mMenu); 228 } 229 230 /** 231 * Show the menu popup anchored to the view specified during construction. 232 * 233 * @see #dismiss() 234 */ show()235 public void show() { 236 mPopup.show(); 237 } 238 239 /** 240 * Dismiss the menu popup. 241 * 242 * @see #show() 243 */ dismiss()244 public void dismiss() { 245 mPopup.dismiss(); 246 } 247 248 /** 249 * Sets a listener that will be notified when the user selects an item from 250 * the menu. 251 * 252 * @param listener the listener to notify 253 */ setOnMenuItemClickListener(@ullable OnMenuItemClickListener listener)254 public void setOnMenuItemClickListener(@Nullable OnMenuItemClickListener listener) { 255 mMenuItemClickListener = listener; 256 } 257 258 /** 259 * Sets a listener that will be notified when this menu is dismissed. 260 * 261 * @param listener the listener to notify 262 */ setOnDismissListener(@ullable OnDismissListener listener)263 public void setOnDismissListener(@Nullable OnDismissListener listener) { 264 mOnDismissListener = listener; 265 } 266 267 /** 268 * Sets whether the popup menu's adapter is forced to show icons in the 269 * menu item views. 270 * <p> 271 * Changes take effect on the next call to show(). 272 * 273 * @param forceShowIcon {@code true} to force icons to be shown, or 274 * {@code false} for icons to be optionally shown 275 */ setForceShowIcon(boolean forceShowIcon)276 public void setForceShowIcon(boolean forceShowIcon) { 277 mPopup.setForceShowIcon(forceShowIcon); 278 } 279 280 /** 281 * Interface responsible for receiving menu item click events if the items 282 * themselves do not have individual item click listeners. 283 */ 284 public interface OnMenuItemClickListener { 285 /** 286 * This method will be invoked when a menu item is clicked if the item 287 * itself did not already handle the event. 288 * 289 * @param item the menu item that was clicked 290 * @return {@code true} if the event was handled, {@code false} 291 * otherwise 292 */ onMenuItemClick(MenuItem item)293 boolean onMenuItemClick(MenuItem item); 294 } 295 296 /** 297 * Callback interface used to notify the application that the menu has closed. 298 */ 299 public interface OnDismissListener { 300 /** 301 * Called when the associated menu has been dismissed. 302 * 303 * @param menu the popup menu that was dismissed 304 */ onDismiss(PopupMenu menu)305 void onDismiss(PopupMenu menu); 306 } 307 308 /** 309 * Returns the {@link ListView} representing the list of menu items in the currently showing 310 * menu. 311 * 312 * @return The view representing the list of menu items. 313 */ 314 @RestrictTo(LIBRARY_GROUP_PREFIX) getMenuListView()315 ListView getMenuListView() { 316 if (!mPopup.isShowing()) { 317 return null; 318 } 319 return mPopup.getListView(); 320 } 321 }