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