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