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