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