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 }