• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.content.Context;
20 import android.database.DataSetObserver;
21 import android.graphics.Rect;
22 import android.graphics.drawable.Drawable;
23 import android.os.Handler;
24 import android.text.TextUtils;
25 import android.util.AttributeSet;
26 import android.util.Log;
27 import android.view.KeyEvent;
28 import android.view.MotionEvent;
29 import android.view.View;
30 import android.view.View.MeasureSpec;
31 import android.view.View.OnTouchListener;
32 import android.view.ViewGroup;
33 import android.view.ViewParent;
34 
35 import java.util.Locale;
36 
37 /**
38  * A ListPopupWindow anchors itself to a host view and displays a
39  * list of choices.
40  *
41  * <p>ListPopupWindow contains a number of tricky behaviors surrounding
42  * positioning, scrolling parents to fit the dropdown, interacting
43  * sanely with the IME if present, and others.
44  *
45  * @see android.widget.AutoCompleteTextView
46  * @see android.widget.Spinner
47  */
48 public class ListPopupWindow {
49     private static final String TAG = "ListPopupWindow";
50     private static final boolean DEBUG = false;
51 
52     /**
53      * This value controls the length of time that the user
54      * must leave a pointer down without scrolling to expand
55      * the autocomplete dropdown list to cover the IME.
56      */
57     private static final int EXPAND_LIST_TIMEOUT = 250;
58 
59     private Context mContext;
60     private PopupWindow mPopup;
61     private ListAdapter mAdapter;
62     private DropDownListView mDropDownList;
63 
64     private int mDropDownHeight = ViewGroup.LayoutParams.WRAP_CONTENT;
65     private int mDropDownWidth = ViewGroup.LayoutParams.WRAP_CONTENT;
66     private int mDropDownHorizontalOffset;
67     private int mDropDownVerticalOffset;
68     private boolean mDropDownVerticalOffsetSet;
69 
70     private boolean mDropDownAlwaysVisible = false;
71     private boolean mForceIgnoreOutsideTouch = false;
72     int mListItemExpandMaximum = Integer.MAX_VALUE;
73 
74     private View mPromptView;
75     private int mPromptPosition = POSITION_PROMPT_ABOVE;
76 
77     private DataSetObserver mObserver;
78 
79     private View mDropDownAnchorView;
80 
81     private Drawable mDropDownListHighlight;
82 
83     private AdapterView.OnItemClickListener mItemClickListener;
84     private AdapterView.OnItemSelectedListener mItemSelectedListener;
85 
86     private final ResizePopupRunnable mResizePopupRunnable = new ResizePopupRunnable();
87     private final PopupTouchInterceptor mTouchInterceptor = new PopupTouchInterceptor();
88     private final PopupScrollListener mScrollListener = new PopupScrollListener();
89     private final ListSelectorHider mHideSelector = new ListSelectorHider();
90     private Runnable mShowDropDownRunnable;
91 
92     private Handler mHandler = new Handler();
93 
94     private Rect mTempRect = new Rect();
95 
96     private boolean mModal;
97 
98     private int mLayoutDirection;
99 
100     /**
101      * The provided prompt view should appear above list content.
102      *
103      * @see #setPromptPosition(int)
104      * @see #getPromptPosition()
105      * @see #setPromptView(View)
106      */
107     public static final int POSITION_PROMPT_ABOVE = 0;
108 
109     /**
110      * The provided prompt view should appear below list content.
111      *
112      * @see #setPromptPosition(int)
113      * @see #getPromptPosition()
114      * @see #setPromptView(View)
115      */
116     public static final int POSITION_PROMPT_BELOW = 1;
117 
118     /**
119      * Alias for {@link ViewGroup.LayoutParams#MATCH_PARENT}.
120      * If used to specify a popup width, the popup will match the width of the anchor view.
121      * If used to specify a popup height, the popup will fill available space.
122      */
123     public static final int MATCH_PARENT = ViewGroup.LayoutParams.MATCH_PARENT;
124 
125     /**
126      * Alias for {@link ViewGroup.LayoutParams#WRAP_CONTENT}.
127      * If used to specify a popup width, the popup will use the width of its content.
128      */
129     public static final int WRAP_CONTENT = ViewGroup.LayoutParams.WRAP_CONTENT;
130 
131     /**
132      * Mode for {@link #setInputMethodMode(int)}: the requirements for the
133      * input method should be based on the focusability of the popup.  That is
134      * if it is focusable than it needs to work with the input method, else
135      * it doesn't.
136      */
137     public static final int INPUT_METHOD_FROM_FOCUSABLE = PopupWindow.INPUT_METHOD_FROM_FOCUSABLE;
138 
139     /**
140      * Mode for {@link #setInputMethodMode(int)}: this popup always needs to
141      * work with an input method, regardless of whether it is focusable.  This
142      * means that it will always be displayed so that the user can also operate
143      * the input method while it is shown.
144      */
145     public static final int INPUT_METHOD_NEEDED = PopupWindow.INPUT_METHOD_NEEDED;
146 
147     /**
148      * Mode for {@link #setInputMethodMode(int)}: this popup never needs to
149      * work with an input method, regardless of whether it is focusable.  This
150      * means that it will always be displayed to use as much space on the
151      * screen as needed, regardless of whether this covers the input method.
152      */
153     public static final int INPUT_METHOD_NOT_NEEDED = PopupWindow.INPUT_METHOD_NOT_NEEDED;
154 
155     /**
156      * Create a new, empty popup window capable of displaying items from a ListAdapter.
157      * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}.
158      *
159      * @param context Context used for contained views.
160      */
ListPopupWindow(Context context)161     public ListPopupWindow(Context context) {
162         this(context, null, com.android.internal.R.attr.listPopupWindowStyle, 0);
163     }
164 
165     /**
166      * Create a new, empty popup window capable of displaying items from a ListAdapter.
167      * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}.
168      *
169      * @param context Context used for contained views.
170      * @param attrs Attributes from inflating parent views used to style the popup.
171      */
ListPopupWindow(Context context, AttributeSet attrs)172     public ListPopupWindow(Context context, AttributeSet attrs) {
173         this(context, attrs, com.android.internal.R.attr.listPopupWindowStyle, 0);
174     }
175 
176     /**
177      * Create a new, empty popup window capable of displaying items from a ListAdapter.
178      * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}.
179      *
180      * @param context Context used for contained views.
181      * @param attrs Attributes from inflating parent views used to style the popup.
182      * @param defStyleAttr Default style attribute to use for popup content.
183      */
ListPopupWindow(Context context, AttributeSet attrs, int defStyleAttr)184     public ListPopupWindow(Context context, AttributeSet attrs, int defStyleAttr) {
185         this(context, attrs, defStyleAttr, 0);
186     }
187 
188     /**
189      * Create a new, empty popup window capable of displaying items from a ListAdapter.
190      * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}.
191      *
192      * @param context Context used for contained views.
193      * @param attrs Attributes from inflating parent views used to style the popup.
194      * @param defStyleAttr Style attribute to read for default styling of popup content.
195      * @param defStyleRes Style resource ID to use for default styling of popup content.
196      */
ListPopupWindow(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)197     public ListPopupWindow(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
198         mContext = context;
199         mPopup = new PopupWindow(context, attrs, defStyleAttr, defStyleRes);
200         mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
201         // Set the default layout direction to match the default locale one
202         final Locale locale = mContext.getResources().getConfiguration().locale;
203         mLayoutDirection = TextUtils.getLayoutDirectionFromLocale(locale);
204     }
205 
206     /**
207      * Sets the adapter that provides the data and the views to represent the data
208      * in this popup window.
209      *
210      * @param adapter The adapter to use to create this window's content.
211      */
setAdapter(ListAdapter adapter)212     public void setAdapter(ListAdapter adapter) {
213         if (mObserver == null) {
214             mObserver = new PopupDataSetObserver();
215         } else if (mAdapter != null) {
216             mAdapter.unregisterDataSetObserver(mObserver);
217         }
218         mAdapter = adapter;
219         if (mAdapter != null) {
220             adapter.registerDataSetObserver(mObserver);
221         }
222 
223         if (mDropDownList != null) {
224             mDropDownList.setAdapter(mAdapter);
225         }
226     }
227 
228     /**
229      * Set where the optional prompt view should appear. The default is
230      * {@link #POSITION_PROMPT_ABOVE}.
231      *
232      * @param position A position constant declaring where the prompt should be displayed.
233      *
234      * @see #POSITION_PROMPT_ABOVE
235      * @see #POSITION_PROMPT_BELOW
236      */
setPromptPosition(int position)237     public void setPromptPosition(int position) {
238         mPromptPosition = position;
239     }
240 
241     /**
242      * @return Where the optional prompt view should appear.
243      *
244      * @see #POSITION_PROMPT_ABOVE
245      * @see #POSITION_PROMPT_BELOW
246      */
getPromptPosition()247     public int getPromptPosition() {
248         return mPromptPosition;
249     }
250 
251     /**
252      * Set whether this window should be modal when shown.
253      *
254      * <p>If a popup window is modal, it will receive all touch and key input.
255      * If the user touches outside the popup window's content area the popup window
256      * will be dismissed.
257      *
258      * @param modal {@code true} if the popup window should be modal, {@code false} otherwise.
259      */
setModal(boolean modal)260     public void setModal(boolean modal) {
261         mModal = true;
262         mPopup.setFocusable(modal);
263     }
264 
265     /**
266      * Returns whether the popup window will be modal when shown.
267      *
268      * @return {@code true} if the popup window will be modal, {@code false} otherwise.
269      */
isModal()270     public boolean isModal() {
271         return mModal;
272     }
273 
274     /**
275      * Forces outside touches to be ignored. Normally if {@link #isDropDownAlwaysVisible()} is
276      * false, we allow outside touch to dismiss the dropdown. If this is set to true, then we
277      * ignore outside touch even when the drop down is not set to always visible.
278      *
279      * @hide Used only by AutoCompleteTextView to handle some internal special cases.
280      */
setForceIgnoreOutsideTouch(boolean forceIgnoreOutsideTouch)281     public void setForceIgnoreOutsideTouch(boolean forceIgnoreOutsideTouch) {
282         mForceIgnoreOutsideTouch = forceIgnoreOutsideTouch;
283     }
284 
285     /**
286      * Sets whether the drop-down should remain visible under certain conditions.
287      *
288      * The drop-down will occupy the entire screen below {@link #getAnchorView} regardless
289      * of the size or content of the list.  {@link #getBackground()} will fill any space
290      * that is not used by the list.
291      *
292      * @param dropDownAlwaysVisible Whether to keep the drop-down visible.
293      *
294      * @hide Only used by AutoCompleteTextView under special conditions.
295      */
setDropDownAlwaysVisible(boolean dropDownAlwaysVisible)296     public void setDropDownAlwaysVisible(boolean dropDownAlwaysVisible) {
297         mDropDownAlwaysVisible = dropDownAlwaysVisible;
298     }
299 
300     /**
301      * @return Whether the drop-down is visible under special conditions.
302      *
303      * @hide Only used by AutoCompleteTextView under special conditions.
304      */
isDropDownAlwaysVisible()305     public boolean isDropDownAlwaysVisible() {
306         return mDropDownAlwaysVisible;
307     }
308 
309     /**
310      * Sets the operating mode for the soft input area.
311      *
312      * @param mode The desired mode, see
313      *        {@link android.view.WindowManager.LayoutParams#softInputMode}
314      *        for the full list
315      *
316      * @see android.view.WindowManager.LayoutParams#softInputMode
317      * @see #getSoftInputMode()
318      */
setSoftInputMode(int mode)319     public void setSoftInputMode(int mode) {
320         mPopup.setSoftInputMode(mode);
321     }
322 
323     /**
324      * Returns the current value in {@link #setSoftInputMode(int)}.
325      *
326      * @see #setSoftInputMode(int)
327      * @see android.view.WindowManager.LayoutParams#softInputMode
328      */
getSoftInputMode()329     public int getSoftInputMode() {
330         return mPopup.getSoftInputMode();
331     }
332 
333     /**
334      * Sets a drawable to use as the list item selector.
335      *
336      * @param selector List selector drawable to use in the popup.
337      */
setListSelector(Drawable selector)338     public void setListSelector(Drawable selector) {
339         mDropDownListHighlight = selector;
340     }
341 
342     /**
343      * @return The background drawable for the popup window.
344      */
getBackground()345     public Drawable getBackground() {
346         return mPopup.getBackground();
347     }
348 
349     /**
350      * Sets a drawable to be the background for the popup window.
351      *
352      * @param d A drawable to set as the background.
353      */
setBackgroundDrawable(Drawable d)354     public void setBackgroundDrawable(Drawable d) {
355         mPopup.setBackgroundDrawable(d);
356     }
357 
358     /**
359      * Set an animation style to use when the popup window is shown or dismissed.
360      *
361      * @param animationStyle Animation style to use.
362      */
setAnimationStyle(int animationStyle)363     public void setAnimationStyle(int animationStyle) {
364         mPopup.setAnimationStyle(animationStyle);
365     }
366 
367     /**
368      * Returns the animation style that will be used when the popup window is
369      * shown or dismissed.
370      *
371      * @return Animation style that will be used.
372      */
getAnimationStyle()373     public int getAnimationStyle() {
374         return mPopup.getAnimationStyle();
375     }
376 
377     /**
378      * Returns the view that will be used to anchor this popup.
379      *
380      * @return The popup's anchor view
381      */
getAnchorView()382     public View getAnchorView() {
383         return mDropDownAnchorView;
384     }
385 
386     /**
387      * Sets the popup's anchor view. This popup will always be positioned relative to
388      * the anchor view when shown.
389      *
390      * @param anchor The view to use as an anchor.
391      */
setAnchorView(View anchor)392     public void setAnchorView(View anchor) {
393         mDropDownAnchorView = anchor;
394     }
395 
396     /**
397      * @return The horizontal offset of the popup from its anchor in pixels.
398      */
getHorizontalOffset()399     public int getHorizontalOffset() {
400         return mDropDownHorizontalOffset;
401     }
402 
403     /**
404      * Set the horizontal offset of this popup from its anchor view in pixels.
405      *
406      * @param offset The horizontal offset of the popup from its anchor.
407      */
setHorizontalOffset(int offset)408     public void setHorizontalOffset(int offset) {
409         mDropDownHorizontalOffset = offset;
410     }
411 
412     /**
413      * @return The vertical offset of the popup from its anchor in pixels.
414      */
getVerticalOffset()415     public int getVerticalOffset() {
416         if (!mDropDownVerticalOffsetSet) {
417             return 0;
418         }
419         return mDropDownVerticalOffset;
420     }
421 
422     /**
423      * Set the vertical offset of this popup from its anchor view in pixels.
424      *
425      * @param offset The vertical offset of the popup from its anchor.
426      */
setVerticalOffset(int offset)427     public void setVerticalOffset(int offset) {
428         mDropDownVerticalOffset = offset;
429         mDropDownVerticalOffsetSet = true;
430     }
431 
432     /**
433      * @return The width of the popup window in pixels.
434      */
getWidth()435     public int getWidth() {
436         return mDropDownWidth;
437     }
438 
439     /**
440      * Sets the width of the popup window in pixels. Can also be {@link #MATCH_PARENT}
441      * or {@link #WRAP_CONTENT}.
442      *
443      * @param width Width of the popup window.
444      */
setWidth(int width)445     public void setWidth(int width) {
446         mDropDownWidth = width;
447     }
448 
449     /**
450      * Sets the width of the popup window by the size of its content. The final width may be
451      * larger to accommodate styled window dressing.
452      *
453      * @param width Desired width of content in pixels.
454      */
setContentWidth(int width)455     public void setContentWidth(int width) {
456         Drawable popupBackground = mPopup.getBackground();
457         if (popupBackground != null) {
458             popupBackground.getPadding(mTempRect);
459             mDropDownWidth = mTempRect.left + mTempRect.right + width;
460         } else {
461             setWidth(width);
462         }
463     }
464 
465     /**
466      * @return The height of the popup window in pixels.
467      */
getHeight()468     public int getHeight() {
469         return mDropDownHeight;
470     }
471 
472     /**
473      * Sets the height of the popup window in pixels. Can also be {@link #MATCH_PARENT}.
474      *
475      * @param height Height of the popup window.
476      */
setHeight(int height)477     public void setHeight(int height) {
478         mDropDownHeight = height;
479     }
480 
481     /**
482      * Sets a listener to receive events when a list item is clicked.
483      *
484      * @param clickListener Listener to register
485      *
486      * @see ListView#setOnItemClickListener(android.widget.AdapterView.OnItemClickListener)
487      */
setOnItemClickListener(AdapterView.OnItemClickListener clickListener)488     public void setOnItemClickListener(AdapterView.OnItemClickListener clickListener) {
489         mItemClickListener = clickListener;
490     }
491 
492     /**
493      * Sets a listener to receive events when a list item is selected.
494      *
495      * @param selectedListener Listener to register.
496      *
497      * @see ListView#setOnItemSelectedListener(android.widget.AdapterView.OnItemSelectedListener)
498      */
setOnItemSelectedListener(AdapterView.OnItemSelectedListener selectedListener)499     public void setOnItemSelectedListener(AdapterView.OnItemSelectedListener selectedListener) {
500         mItemSelectedListener = selectedListener;
501     }
502 
503     /**
504      * Set a view to act as a user prompt for this popup window. Where the prompt view will appear
505      * is controlled by {@link #setPromptPosition(int)}.
506      *
507      * @param prompt View to use as an informational prompt.
508      */
setPromptView(View prompt)509     public void setPromptView(View prompt) {
510         boolean showing = isShowing();
511         if (showing) {
512             removePromptView();
513         }
514         mPromptView = prompt;
515         if (showing) {
516             show();
517         }
518     }
519 
520     /**
521      * Post a {@link #show()} call to the UI thread.
522      */
postShow()523     public void postShow() {
524         mHandler.post(mShowDropDownRunnable);
525     }
526 
527     /**
528      * Show the popup list. If the list is already showing, this method
529      * will recalculate the popup's size and position.
530      */
show()531     public void show() {
532         int height = buildDropDown();
533 
534         int widthSpec = 0;
535         int heightSpec = 0;
536 
537         boolean noInputMethod = isInputMethodNotNeeded();
538         mPopup.setAllowScrollingAnchorParent(!noInputMethod);
539 
540         if (mPopup.isShowing()) {
541             if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) {
542                 // The call to PopupWindow's update method below can accept -1 for any
543                 // value you do not want to update.
544                 widthSpec = -1;
545             } else if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) {
546                 widthSpec = getAnchorView().getWidth();
547             } else {
548                 widthSpec = mDropDownWidth;
549             }
550 
551             if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
552                 // The call to PopupWindow's update method below can accept -1 for any
553                 // value you do not want to update.
554                 heightSpec = noInputMethod ? height : ViewGroup.LayoutParams.MATCH_PARENT;
555                 if (noInputMethod) {
556                     mPopup.setWindowLayoutMode(
557                             mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ?
558                                     ViewGroup.LayoutParams.MATCH_PARENT : 0, 0);
559                 } else {
560                     mPopup.setWindowLayoutMode(
561                             mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ?
562                                     ViewGroup.LayoutParams.MATCH_PARENT : 0,
563                             ViewGroup.LayoutParams.MATCH_PARENT);
564                 }
565             } else if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
566                 heightSpec = height;
567             } else {
568                 heightSpec = mDropDownHeight;
569             }
570 
571             mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible);
572 
573             mPopup.update(getAnchorView(), mDropDownHorizontalOffset,
574                     mDropDownVerticalOffset, widthSpec, heightSpec);
575         } else {
576             if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) {
577                 widthSpec = ViewGroup.LayoutParams.MATCH_PARENT;
578             } else {
579                 if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) {
580                     mPopup.setWidth(getAnchorView().getWidth());
581                 } else {
582                     mPopup.setWidth(mDropDownWidth);
583                 }
584             }
585 
586             if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
587                 heightSpec = ViewGroup.LayoutParams.MATCH_PARENT;
588             } else {
589                 if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
590                     mPopup.setHeight(height);
591                 } else {
592                     mPopup.setHeight(mDropDownHeight);
593                 }
594             }
595 
596             mPopup.setWindowLayoutMode(widthSpec, heightSpec);
597             mPopup.setClipToScreenEnabled(true);
598 
599             // use outside touchable to dismiss drop down when touching outside of it, so
600             // only set this if the dropdown is not always visible
601             mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible);
602             mPopup.setTouchInterceptor(mTouchInterceptor);
603             mPopup.showAsDropDown(getAnchorView(),
604                     mDropDownHorizontalOffset, mDropDownVerticalOffset);
605             mDropDownList.setSelection(ListView.INVALID_POSITION);
606 
607             if (!mModal || mDropDownList.isInTouchMode()) {
608                 clearListSelection();
609             }
610             if (!mModal) {
611                 mHandler.post(mHideSelector);
612             }
613         }
614     }
615 
616     /**
617      * Dismiss the popup window.
618      */
dismiss()619     public void dismiss() {
620         mPopup.dismiss();
621         removePromptView();
622         mPopup.setContentView(null);
623         mDropDownList = null;
624         mHandler.removeCallbacks(mResizePopupRunnable);
625     }
626 
627     /**
628      * Set a listener to receive a callback when the popup is dismissed.
629      *
630      * @param listener Listener that will be notified when the popup is dismissed.
631      */
setOnDismissListener(PopupWindow.OnDismissListener listener)632     public void setOnDismissListener(PopupWindow.OnDismissListener listener) {
633         mPopup.setOnDismissListener(listener);
634     }
635 
removePromptView()636     private void removePromptView() {
637         if (mPromptView != null) {
638             final ViewParent parent = mPromptView.getParent();
639             if (parent instanceof ViewGroup) {
640                 final ViewGroup group = (ViewGroup) parent;
641                 group.removeView(mPromptView);
642             }
643         }
644     }
645 
646     /**
647      * Control how the popup operates with an input method: one of
648      * {@link #INPUT_METHOD_FROM_FOCUSABLE}, {@link #INPUT_METHOD_NEEDED},
649      * or {@link #INPUT_METHOD_NOT_NEEDED}.
650      *
651      * <p>If the popup is showing, calling this method will take effect only
652      * the next time the popup is shown or through a manual call to the {@link #show()}
653      * method.</p>
654      *
655      * @see #getInputMethodMode()
656      * @see #show()
657      */
setInputMethodMode(int mode)658     public void setInputMethodMode(int mode) {
659         mPopup.setInputMethodMode(mode);
660     }
661 
662     /**
663      * Return the current value in {@link #setInputMethodMode(int)}.
664      *
665      * @see #setInputMethodMode(int)
666      */
getInputMethodMode()667     public int getInputMethodMode() {
668         return mPopup.getInputMethodMode();
669     }
670 
671     /**
672      * Set the selected position of the list.
673      * Only valid when {@link #isShowing()} == {@code true}.
674      *
675      * @param position List position to set as selected.
676      */
setSelection(int position)677     public void setSelection(int position) {
678         DropDownListView list = mDropDownList;
679         if (isShowing() && list != null) {
680             list.mListSelectionHidden = false;
681             list.setSelection(position);
682             if (list.getChoiceMode() != ListView.CHOICE_MODE_NONE) {
683                 list.setItemChecked(position, true);
684             }
685         }
686     }
687 
688     /**
689      * Clear any current list selection.
690      * Only valid when {@link #isShowing()} == {@code true}.
691      */
clearListSelection()692     public void clearListSelection() {
693         final DropDownListView list = mDropDownList;
694         if (list != null) {
695             // WARNING: Please read the comment where mListSelectionHidden is declared
696             list.mListSelectionHidden = true;
697             list.hideSelector();
698             list.requestLayout();
699         }
700     }
701 
702     /**
703      * @return {@code true} if the popup is currently showing, {@code false} otherwise.
704      */
isShowing()705     public boolean isShowing() {
706         return mPopup.isShowing();
707     }
708 
709     /**
710      * @return {@code true} if this popup is configured to assume the user does not need
711      * to interact with the IME while it is showing, {@code false} otherwise.
712      */
isInputMethodNotNeeded()713     public boolean isInputMethodNotNeeded() {
714         return mPopup.getInputMethodMode() == INPUT_METHOD_NOT_NEEDED;
715     }
716 
717     /**
718      * Perform an item click operation on the specified list adapter position.
719      *
720      * @param position Adapter position for performing the click
721      * @return true if the click action could be performed, false if not.
722      *         (e.g. if the popup was not showing, this method would return false.)
723      */
performItemClick(int position)724     public boolean performItemClick(int position) {
725         if (isShowing()) {
726             if (mItemClickListener != null) {
727                 final DropDownListView list = mDropDownList;
728                 final View child = list.getChildAt(position - list.getFirstVisiblePosition());
729                 final ListAdapter adapter = list.getAdapter();
730                 mItemClickListener.onItemClick(list, child, position, adapter.getItemId(position));
731             }
732             return true;
733         }
734         return false;
735     }
736 
737     /**
738      * @return The currently selected item or null if the popup is not showing.
739      */
getSelectedItem()740     public Object getSelectedItem() {
741         if (!isShowing()) {
742             return null;
743         }
744         return mDropDownList.getSelectedItem();
745     }
746 
747     /**
748      * @return The position of the currently selected item or {@link ListView#INVALID_POSITION}
749      * if {@link #isShowing()} == {@code false}.
750      *
751      * @see ListView#getSelectedItemPosition()
752      */
getSelectedItemPosition()753     public int getSelectedItemPosition() {
754         if (!isShowing()) {
755             return ListView.INVALID_POSITION;
756         }
757         return mDropDownList.getSelectedItemPosition();
758     }
759 
760     /**
761      * @return The ID of the currently selected item or {@link ListView#INVALID_ROW_ID}
762      * if {@link #isShowing()} == {@code false}.
763      *
764      * @see ListView#getSelectedItemId()
765      */
getSelectedItemId()766     public long getSelectedItemId() {
767         if (!isShowing()) {
768             return ListView.INVALID_ROW_ID;
769         }
770         return mDropDownList.getSelectedItemId();
771     }
772 
773     /**
774      * @return The View for the currently selected item or null if
775      * {@link #isShowing()} == {@code false}.
776      *
777      * @see ListView#getSelectedView()
778      */
getSelectedView()779     public View getSelectedView() {
780         if (!isShowing()) {
781             return null;
782         }
783         return mDropDownList.getSelectedView();
784     }
785 
786     /**
787      * @return The {@link ListView} displayed within the popup window.
788      * Only valid when {@link #isShowing()} == {@code true}.
789      */
getListView()790     public ListView getListView() {
791         return mDropDownList;
792     }
793 
794     /**
795      * The maximum number of list items that can be visible and still have
796      * the list expand when touched.
797      *
798      * @param max Max number of items that can be visible and still allow the list to expand.
799      */
setListItemExpandMax(int max)800     void setListItemExpandMax(int max) {
801         mListItemExpandMaximum = max;
802     }
803 
804     /**
805      * Filter key down events. By forwarding key down events to this function,
806      * views using non-modal ListPopupWindow can have it handle key selection of items.
807      *
808      * @param keyCode keyCode param passed to the host view's onKeyDown
809      * @param event event param passed to the host view's onKeyDown
810      * @return true if the event was handled, false if it was ignored.
811      *
812      * @see #setModal(boolean)
813      */
onKeyDown(int keyCode, KeyEvent event)814     public boolean onKeyDown(int keyCode, KeyEvent event) {
815         // when the drop down is shown, we drive it directly
816         if (isShowing()) {
817             // the key events are forwarded to the list in the drop down view
818             // note that ListView handles space but we don't want that to happen
819             // also if selection is not currently in the drop down, then don't
820             // let center or enter presses go there since that would cause it
821             // to select one of its items
822             if (keyCode != KeyEvent.KEYCODE_SPACE
823                     && (mDropDownList.getSelectedItemPosition() >= 0
824                             || (keyCode != KeyEvent.KEYCODE_ENTER
825                                     && keyCode != KeyEvent.KEYCODE_DPAD_CENTER))) {
826                 int curIndex = mDropDownList.getSelectedItemPosition();
827                 boolean consumed;
828 
829                 final boolean below = !mPopup.isAboveAnchor();
830 
831                 final ListAdapter adapter = mAdapter;
832 
833                 boolean allEnabled;
834                 int firstItem = Integer.MAX_VALUE;
835                 int lastItem = Integer.MIN_VALUE;
836 
837                 if (adapter != null) {
838                     allEnabled = adapter.areAllItemsEnabled();
839                     firstItem = allEnabled ? 0 :
840                             mDropDownList.lookForSelectablePosition(0, true);
841                     lastItem = allEnabled ? adapter.getCount() - 1 :
842                             mDropDownList.lookForSelectablePosition(adapter.getCount() - 1, false);
843                 }
844 
845                 if ((below && keyCode == KeyEvent.KEYCODE_DPAD_UP && curIndex <= firstItem) ||
846                         (!below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN && curIndex >= lastItem)) {
847                     // When the selection is at the top, we block the key
848                     // event to prevent focus from moving.
849                     clearListSelection();
850                     mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
851                     show();
852                     return true;
853                 } else {
854                     // WARNING: Please read the comment where mListSelectionHidden
855                     //          is declared
856                     mDropDownList.mListSelectionHidden = false;
857                 }
858 
859                 consumed = mDropDownList.onKeyDown(keyCode, event);
860                 if (DEBUG) Log.v(TAG, "Key down: code=" + keyCode + " list consumed=" + consumed);
861 
862                 if (consumed) {
863                     // If it handled the key event, then the user is
864                     // navigating in the list, so we should put it in front.
865                     mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
866                     // Here's a little trick we need to do to make sure that
867                     // the list view is actually showing its focus indicator,
868                     // by ensuring it has focus and getting its window out
869                     // of touch mode.
870                     mDropDownList.requestFocusFromTouch();
871                     show();
872 
873                     switch (keyCode) {
874                         // avoid passing the focus from the text view to the
875                         // next component
876                         case KeyEvent.KEYCODE_ENTER:
877                         case KeyEvent.KEYCODE_DPAD_CENTER:
878                         case KeyEvent.KEYCODE_DPAD_DOWN:
879                         case KeyEvent.KEYCODE_DPAD_UP:
880                             return true;
881                     }
882                 } else {
883                     if (below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
884                         // when the selection is at the bottom, we block the
885                         // event to avoid going to the next focusable widget
886                         if (curIndex == lastItem) {
887                             return true;
888                         }
889                     } else if (!below && keyCode == KeyEvent.KEYCODE_DPAD_UP &&
890                             curIndex == firstItem) {
891                         return true;
892                     }
893                 }
894             }
895         }
896 
897         return false;
898     }
899 
900     /**
901      * Filter key down events. By forwarding key up events to this function,
902      * views using non-modal ListPopupWindow can have it handle key selection of items.
903      *
904      * @param keyCode keyCode param passed to the host view's onKeyUp
905      * @param event event param passed to the host view's onKeyUp
906      * @return true if the event was handled, false if it was ignored.
907      *
908      * @see #setModal(boolean)
909      */
onKeyUp(int keyCode, KeyEvent event)910     public boolean onKeyUp(int keyCode, KeyEvent event) {
911         if (isShowing() && mDropDownList.getSelectedItemPosition() >= 0) {
912             boolean consumed = mDropDownList.onKeyUp(keyCode, event);
913             if (consumed) {
914                 switch (keyCode) {
915                     // if the list accepts the key events and the key event
916                     // was a click, the text view gets the selected item
917                     // from the drop down as its content
918                     case KeyEvent.KEYCODE_ENTER:
919                     case KeyEvent.KEYCODE_DPAD_CENTER:
920                         dismiss();
921                         break;
922                 }
923             }
924             return consumed;
925         }
926         return false;
927     }
928 
929     /**
930      * Filter pre-IME key events. By forwarding {@link View#onKeyPreIme(int, KeyEvent)}
931      * events to this function, views using ListPopupWindow can have it dismiss the popup
932      * when the back key is pressed.
933      *
934      * @param keyCode keyCode param passed to the host view's onKeyPreIme
935      * @param event event param passed to the host view's onKeyPreIme
936      * @return true if the event was handled, false if it was ignored.
937      *
938      * @see #setModal(boolean)
939      */
onKeyPreIme(int keyCode, KeyEvent event)940     public boolean onKeyPreIme(int keyCode, KeyEvent event) {
941         if (keyCode == KeyEvent.KEYCODE_BACK && isShowing()) {
942             // special case for the back key, we do not even try to send it
943             // to the drop down list but instead, consume it immediately
944             final View anchorView = mDropDownAnchorView;
945             if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
946                 KeyEvent.DispatcherState state = anchorView.getKeyDispatcherState();
947                 if (state != null) {
948                     state.startTracking(event, this);
949                 }
950                 return true;
951             } else if (event.getAction() == KeyEvent.ACTION_UP) {
952                 KeyEvent.DispatcherState state = anchorView.getKeyDispatcherState();
953                 if (state != null) {
954                     state.handleUpEvent(event);
955                 }
956                 if (event.isTracking() && !event.isCanceled()) {
957                     dismiss();
958                     return true;
959                 }
960             }
961         }
962         return false;
963     }
964 
965     /**
966      * <p>Builds the popup window's content and returns the height the popup
967      * should have. Returns -1 when the content already exists.</p>
968      *
969      * @return the content's height or -1 if content already exists
970      */
buildDropDown()971     private int buildDropDown() {
972         ViewGroup dropDownView;
973         int otherHeights = 0;
974 
975         if (mDropDownList == null) {
976             Context context = mContext;
977 
978             /**
979              * This Runnable exists for the sole purpose of checking if the view layout has got
980              * completed and if so call showDropDown to display the drop down. This is used to show
981              * the drop down as soon as possible after user opens up the search dialog, without
982              * waiting for the normal UI pipeline to do it's job which is slower than this method.
983              */
984             mShowDropDownRunnable = new Runnable() {
985                 public void run() {
986                     // View layout should be all done before displaying the drop down.
987                     View view = getAnchorView();
988                     if (view != null && view.getWindowToken() != null) {
989                         show();
990                     }
991                 }
992             };
993 
994             mDropDownList = new DropDownListView(context, !mModal);
995             if (mDropDownListHighlight != null) {
996                 mDropDownList.setSelector(mDropDownListHighlight);
997             }
998             mDropDownList.setAdapter(mAdapter);
999             mDropDownList.setOnItemClickListener(mItemClickListener);
1000             mDropDownList.setFocusable(true);
1001             mDropDownList.setFocusableInTouchMode(true);
1002             mDropDownList.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
1003                 public void onItemSelected(AdapterView<?> parent, View view,
1004                         int position, long id) {
1005 
1006                     if (position != -1) {
1007                         DropDownListView dropDownList = mDropDownList;
1008 
1009                         if (dropDownList != null) {
1010                             dropDownList.mListSelectionHidden = false;
1011                         }
1012                     }
1013                 }
1014 
1015                 public void onNothingSelected(AdapterView<?> parent) {
1016                 }
1017             });
1018             mDropDownList.setOnScrollListener(mScrollListener);
1019 
1020             if (mItemSelectedListener != null) {
1021                 mDropDownList.setOnItemSelectedListener(mItemSelectedListener);
1022             }
1023 
1024             dropDownView = mDropDownList;
1025 
1026             View hintView = mPromptView;
1027             if (hintView != null) {
1028                 // if a hint has been specified, we accomodate more space for it and
1029                 // add a text view in the drop down menu, at the bottom of the list
1030                 LinearLayout hintContainer = new LinearLayout(context);
1031                 hintContainer.setOrientation(LinearLayout.VERTICAL);
1032 
1033                 LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams(
1034                         ViewGroup.LayoutParams.MATCH_PARENT, 0, 1.0f
1035                 );
1036 
1037                 switch (mPromptPosition) {
1038                 case POSITION_PROMPT_BELOW:
1039                     hintContainer.addView(dropDownView, hintParams);
1040                     hintContainer.addView(hintView);
1041                     break;
1042 
1043                 case POSITION_PROMPT_ABOVE:
1044                     hintContainer.addView(hintView);
1045                     hintContainer.addView(dropDownView, hintParams);
1046                     break;
1047 
1048                 default:
1049                     Log.e(TAG, "Invalid hint position " + mPromptPosition);
1050                     break;
1051                 }
1052 
1053                 // measure the hint's height to find how much more vertical space
1054                 // we need to add to the drop down's height
1055                 int widthSpec = MeasureSpec.makeMeasureSpec(mDropDownWidth, MeasureSpec.AT_MOST);
1056                 int heightSpec = MeasureSpec.UNSPECIFIED;
1057                 hintView.measure(widthSpec, heightSpec);
1058 
1059                 hintParams = (LinearLayout.LayoutParams) hintView.getLayoutParams();
1060                 otherHeights = hintView.getMeasuredHeight() + hintParams.topMargin
1061                         + hintParams.bottomMargin;
1062 
1063                 dropDownView = hintContainer;
1064             }
1065 
1066             mPopup.setContentView(dropDownView);
1067         } else {
1068             dropDownView = (ViewGroup) mPopup.getContentView();
1069             final View view = mPromptView;
1070             if (view != null) {
1071                 LinearLayout.LayoutParams hintParams =
1072                         (LinearLayout.LayoutParams) view.getLayoutParams();
1073                 otherHeights = view.getMeasuredHeight() + hintParams.topMargin
1074                         + hintParams.bottomMargin;
1075             }
1076         }
1077 
1078         // getMaxAvailableHeight() subtracts the padding, so we put it back
1079         // to get the available height for the whole window
1080         int padding = 0;
1081         Drawable background = mPopup.getBackground();
1082         if (background != null) {
1083             background.getPadding(mTempRect);
1084             padding = mTempRect.top + mTempRect.bottom;
1085 
1086             // If we don't have an explicit vertical offset, determine one from the window
1087             // background so that content will line up.
1088             if (!mDropDownVerticalOffsetSet) {
1089                 mDropDownVerticalOffset = -mTempRect.top;
1090             }
1091         } else {
1092             mTempRect.setEmpty();
1093         }
1094 
1095         // Max height available on the screen for a popup.
1096         boolean ignoreBottomDecorations =
1097                 mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED;
1098         final int maxHeight = mPopup.getMaxAvailableHeight(
1099                 getAnchorView(), mDropDownVerticalOffset, ignoreBottomDecorations);
1100 
1101         if (mDropDownAlwaysVisible || mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
1102             return maxHeight + padding;
1103         }
1104 
1105         final int childWidthSpec;
1106         switch (mDropDownWidth) {
1107             case ViewGroup.LayoutParams.WRAP_CONTENT:
1108                 childWidthSpec = MeasureSpec.makeMeasureSpec(
1109                         mContext.getResources().getDisplayMetrics().widthPixels -
1110                         (mTempRect.left + mTempRect.right),
1111                         MeasureSpec.AT_MOST);
1112                 break;
1113             case ViewGroup.LayoutParams.MATCH_PARENT:
1114                 childWidthSpec = MeasureSpec.makeMeasureSpec(
1115                         mContext.getResources().getDisplayMetrics().widthPixels -
1116                         (mTempRect.left + mTempRect.right),
1117                         MeasureSpec.EXACTLY);
1118                 break;
1119             default:
1120                 childWidthSpec = MeasureSpec.makeMeasureSpec(mDropDownWidth, MeasureSpec.EXACTLY);
1121                 break;
1122         }
1123         final int listContent = mDropDownList.measureHeightOfChildren(childWidthSpec,
1124                 0, ListView.NO_POSITION, maxHeight - otherHeights, -1);
1125         // add padding only if the list has items in it, that way we don't show
1126         // the popup if it is not needed
1127         if (listContent > 0) otherHeights += padding;
1128 
1129         return listContent + otherHeights;
1130     }
1131 
1132     /**
1133      * <p>Wrapper class for a ListView. This wrapper can hijack the focus to
1134      * make sure the list uses the appropriate drawables and states when
1135      * displayed on screen within a drop down. The focus is never actually
1136      * passed to the drop down in this mode; the list only looks focused.</p>
1137      */
1138     private static class DropDownListView extends ListView {
1139         private static final String TAG = ListPopupWindow.TAG + ".DropDownListView";
1140         /*
1141          * WARNING: This is a workaround for a touch mode issue.
1142          *
1143          * Touch mode is propagated lazily to windows. This causes problems in
1144          * the following scenario:
1145          * - Type something in the AutoCompleteTextView and get some results
1146          * - Move down with the d-pad to select an item in the list
1147          * - Move up with the d-pad until the selection disappears
1148          * - Type more text in the AutoCompleteTextView *using the soft keyboard*
1149          *   and get new results; you are now in touch mode
1150          * - The selection comes back on the first item in the list, even though
1151          *   the list is supposed to be in touch mode
1152          *
1153          * Using the soft keyboard triggers the touch mode change but that change
1154          * is propagated to our window only after the first list layout, therefore
1155          * after the list attempts to resurrect the selection.
1156          *
1157          * The trick to work around this issue is to pretend the list is in touch
1158          * mode when we know that the selection should not appear, that is when
1159          * we know the user moved the selection away from the list.
1160          *
1161          * This boolean is set to true whenever we explicitly hide the list's
1162          * selection and reset to false whenever we know the user moved the
1163          * selection back to the list.
1164          *
1165          * When this boolean is true, isInTouchMode() returns true, otherwise it
1166          * returns super.isInTouchMode().
1167          */
1168         private boolean mListSelectionHidden;
1169 
1170         /**
1171          * True if this wrapper should fake focus.
1172          */
1173         private boolean mHijackFocus;
1174 
1175         /**
1176          * <p>Creates a new list view wrapper.</p>
1177          *
1178          * @param context this view's context
1179          */
DropDownListView(Context context, boolean hijackFocus)1180         public DropDownListView(Context context, boolean hijackFocus) {
1181             super(context, null, com.android.internal.R.attr.dropDownListViewStyle);
1182             mHijackFocus = hijackFocus;
1183             // TODO: Add an API to control this
1184             setCacheColorHint(0); // Transparent, since the background drawable could be anything.
1185         }
1186 
1187         /**
1188          * <p>Avoids jarring scrolling effect by ensuring that list elements
1189          * made of a text view fit on a single line.</p>
1190          *
1191          * @param position the item index in the list to get a view for
1192          * @return the view for the specified item
1193          */
1194         @Override
obtainView(int position, boolean[] isScrap)1195         View obtainView(int position, boolean[] isScrap) {
1196             View view = super.obtainView(position, isScrap);
1197 
1198             if (view instanceof TextView) {
1199                 ((TextView) view).setHorizontallyScrolling(true);
1200             }
1201 
1202             return view;
1203         }
1204 
1205         @Override
isInTouchMode()1206         public boolean isInTouchMode() {
1207             // WARNING: Please read the comment where mListSelectionHidden is declared
1208             return (mHijackFocus && mListSelectionHidden) || super.isInTouchMode();
1209         }
1210 
1211         /**
1212          * <p>Returns the focus state in the drop down.</p>
1213          *
1214          * @return true always if hijacking focus
1215          */
1216         @Override
hasWindowFocus()1217         public boolean hasWindowFocus() {
1218             return mHijackFocus || super.hasWindowFocus();
1219         }
1220 
1221         /**
1222          * <p>Returns the focus state in the drop down.</p>
1223          *
1224          * @return true always if hijacking focus
1225          */
1226         @Override
isFocused()1227         public boolean isFocused() {
1228             return mHijackFocus || super.isFocused();
1229         }
1230 
1231         /**
1232          * <p>Returns the focus state in the drop down.</p>
1233          *
1234          * @return true always if hijacking focus
1235          */
1236         @Override
hasFocus()1237         public boolean hasFocus() {
1238             return mHijackFocus || super.hasFocus();
1239         }
1240     }
1241 
1242     private class PopupDataSetObserver extends DataSetObserver {
1243         @Override
onChanged()1244         public void onChanged() {
1245             if (isShowing()) {
1246                 // Resize the popup to fit new content
1247                 show();
1248             }
1249         }
1250 
1251         @Override
onInvalidated()1252         public void onInvalidated() {
1253             dismiss();
1254         }
1255     }
1256 
1257     private class ListSelectorHider implements Runnable {
run()1258         public void run() {
1259             clearListSelection();
1260         }
1261     }
1262 
1263     private class ResizePopupRunnable implements Runnable {
run()1264         public void run() {
1265             if (mDropDownList != null && mDropDownList.getCount() > mDropDownList.getChildCount() &&
1266                     mDropDownList.getChildCount() <= mListItemExpandMaximum) {
1267                 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
1268                 show();
1269             }
1270         }
1271     }
1272 
1273     private class PopupTouchInterceptor implements OnTouchListener {
onTouch(View v, MotionEvent event)1274         public boolean onTouch(View v, MotionEvent event) {
1275             final int action = event.getAction();
1276             final int x = (int) event.getX();
1277             final int y = (int) event.getY();
1278 
1279             if (action == MotionEvent.ACTION_DOWN &&
1280                     mPopup != null && mPopup.isShowing() &&
1281                     (x >= 0 && x < mPopup.getWidth() && y >= 0 && y < mPopup.getHeight())) {
1282                 mHandler.postDelayed(mResizePopupRunnable, EXPAND_LIST_TIMEOUT);
1283             } else if (action == MotionEvent.ACTION_UP) {
1284                 mHandler.removeCallbacks(mResizePopupRunnable);
1285             }
1286             return false;
1287         }
1288     }
1289 
1290     private class PopupScrollListener implements ListView.OnScrollListener {
onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)1291         public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
1292                 int totalItemCount) {
1293 
1294         }
1295 
onScrollStateChanged(AbsListView view, int scrollState)1296         public void onScrollStateChanged(AbsListView view, int scrollState) {
1297             if (scrollState == SCROLL_STATE_TOUCH_SCROLL &&
1298                     !isInputMethodNotNeeded() && mPopup.getContentView() != null) {
1299                 mHandler.removeCallbacks(mResizePopupRunnable);
1300                 mResizePopupRunnable.run();
1301             }
1302         }
1303     }
1304 }
1305