ListPopupWindow.java revision 62e2bdecc21819a71c04204f20fc051886fdabd6
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    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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
330    public void setListSelector(Drawable selector) {
331        mDropDownListHighlight = selector;
332    }
333
334    /**
335     * @return The background drawable for the popup window.
336     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
624    public void setOnDismissListener(PopupWindow.OnDismissListener listener) {
625        mPopup.setOnDismissListener(listener);
626    }
627
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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.setVerticalFadingEdgeEnabled(true);
992            mDropDownList.setOnItemClickListener(mItemClickListener);
993            mDropDownList.setFocusable(true);
994            mDropDownList.setFocusableInTouchMode(true);
995            mDropDownList.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
996                public void onItemSelected(AdapterView<?> parent, View view,
997                        int position, long id) {
998
999                    if (position != -1) {
1000                        DropDownListView dropDownList = mDropDownList;
1001
1002                        if (dropDownList != null) {
1003                            dropDownList.mListSelectionHidden = false;
1004                        }
1005                    }
1006                }
1007
1008                public void onNothingSelected(AdapterView<?> parent) {
1009                }
1010            });
1011            mDropDownList.setOnScrollListener(mScrollListener);
1012
1013            if (mItemSelectedListener != null) {
1014                mDropDownList.setOnItemSelectedListener(mItemSelectedListener);
1015            }
1016
1017            dropDownView = mDropDownList;
1018
1019            View hintView = mPromptView;
1020            if (hintView != null) {
1021                // if an hint has been specified, we accomodate more space for it and
1022                // add a text view in the drop down menu, at the bottom of the list
1023                LinearLayout hintContainer = new LinearLayout(context);
1024                hintContainer.setOrientation(LinearLayout.VERTICAL);
1025
1026                LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams(
1027                        ViewGroup.LayoutParams.MATCH_PARENT, 0, 1.0f
1028                );
1029
1030                switch (mPromptPosition) {
1031                case POSITION_PROMPT_BELOW:
1032                    hintContainer.addView(dropDownView, hintParams);
1033                    hintContainer.addView(hintView);
1034                    break;
1035
1036                case POSITION_PROMPT_ABOVE:
1037                    hintContainer.addView(hintView);
1038                    hintContainer.addView(dropDownView, hintParams);
1039                    break;
1040
1041                default:
1042                    Log.e(TAG, "Invalid hint position " + mPromptPosition);
1043                    break;
1044                }
1045
1046                // measure the hint's height to find how much more vertical space
1047                // we need to add to the drop down's height
1048                int widthSpec = MeasureSpec.makeMeasureSpec(mDropDownWidth, MeasureSpec.AT_MOST);
1049                int heightSpec = MeasureSpec.UNSPECIFIED;
1050                hintView.measure(widthSpec, heightSpec);
1051
1052                hintParams = (LinearLayout.LayoutParams) hintView.getLayoutParams();
1053                otherHeights = hintView.getMeasuredHeight() + hintParams.topMargin
1054                        + hintParams.bottomMargin;
1055
1056                dropDownView = hintContainer;
1057            }
1058
1059            mPopup.setContentView(dropDownView);
1060        } else {
1061            dropDownView = (ViewGroup) mPopup.getContentView();
1062            final View view = mPromptView;
1063            if (view != null) {
1064                LinearLayout.LayoutParams hintParams =
1065                        (LinearLayout.LayoutParams) view.getLayoutParams();
1066                otherHeights = view.getMeasuredHeight() + hintParams.topMargin
1067                        + hintParams.bottomMargin;
1068            }
1069        }
1070
1071        // getMaxAvailableHeight() subtracts the padding, so we put it back
1072        // to get the available height for the whole window
1073        int padding = 0;
1074        Drawable background = mPopup.getBackground();
1075        if (background != null) {
1076            background.getPadding(mTempRect);
1077            padding = mTempRect.top + mTempRect.bottom;
1078
1079            // If we don't have an explicit vertical offset, determine one from the window
1080            // background so that content will line up.
1081            if (!mDropDownVerticalOffsetSet) {
1082                mDropDownVerticalOffset = -mTempRect.top;
1083            }
1084        }
1085
1086        // Max height available on the screen for a popup.
1087        boolean ignoreBottomDecorations =
1088                mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED;
1089        final int maxHeight = mPopup.getMaxAvailableHeight(
1090                getAnchorView(), mDropDownVerticalOffset, ignoreBottomDecorations);
1091
1092        if (mDropDownAlwaysVisible || mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
1093            return maxHeight + padding;
1094        }
1095
1096        final int listContent = mDropDownList.measureHeightOfChildren(MeasureSpec.UNSPECIFIED,
1097                0, ListView.NO_POSITION, maxHeight - otherHeights, -1);
1098        // add padding only if the list has items in it, that way we don't show
1099        // the popup if it is not needed
1100        if (listContent > 0) otherHeights += padding;
1101
1102        return listContent + otherHeights;
1103    }
1104
1105    /**
1106     * <p>Wrapper class for a ListView. This wrapper can hijack the focus to
1107     * make sure the list uses the appropriate drawables and states when
1108     * displayed on screen within a drop down. The focus is never actually
1109     * passed to the drop down in this mode; the list only looks focused.</p>
1110     */
1111    private static class DropDownListView extends ListView {
1112        private static final String TAG = ListPopupWindow.TAG + ".DropDownListView";
1113        /*
1114         * WARNING: This is a workaround for a touch mode issue.
1115         *
1116         * Touch mode is propagated lazily to windows. This causes problems in
1117         * the following scenario:
1118         * - Type something in the AutoCompleteTextView and get some results
1119         * - Move down with the d-pad to select an item in the list
1120         * - Move up with the d-pad until the selection disappears
1121         * - Type more text in the AutoCompleteTextView *using the soft keyboard*
1122         *   and get new results; you are now in touch mode
1123         * - The selection comes back on the first item in the list, even though
1124         *   the list is supposed to be in touch mode
1125         *
1126         * Using the soft keyboard triggers the touch mode change but that change
1127         * is propagated to our window only after the first list layout, therefore
1128         * after the list attempts to resurrect the selection.
1129         *
1130         * The trick to work around this issue is to pretend the list is in touch
1131         * mode when we know that the selection should not appear, that is when
1132         * we know the user moved the selection away from the list.
1133         *
1134         * This boolean is set to true whenever we explicitly hide the list's
1135         * selection and reset to false whenever we know the user moved the
1136         * selection back to the list.
1137         *
1138         * When this boolean is true, isInTouchMode() returns true, otherwise it
1139         * returns super.isInTouchMode().
1140         */
1141        private boolean mListSelectionHidden;
1142
1143        /**
1144         * True if this wrapper should fake focus.
1145         */
1146        private boolean mHijackFocus;
1147
1148        /**
1149         * <p>Creates a new list view wrapper.</p>
1150         *
1151         * @param context this view's context
1152         */
1153        public DropDownListView(Context context, boolean hijackFocus) {
1154            super(context, null, com.android.internal.R.attr.dropDownListViewStyle);
1155            mHijackFocus = hijackFocus;
1156            // TODO: Add an API to control this
1157            setCacheColorHint(0); // Transparent, since the background drawable could be anything.
1158        }
1159
1160        /**
1161         * <p>Avoids jarring scrolling effect by ensuring that list elements
1162         * made of a text view fit on a single line.</p>
1163         *
1164         * @param position the item index in the list to get a view for
1165         * @return the view for the specified item
1166         */
1167        @Override
1168        View obtainView(int position, boolean[] isScrap) {
1169            View view = super.obtainView(position, isScrap);
1170
1171            if (view instanceof TextView) {
1172                ((TextView) view).setHorizontallyScrolling(true);
1173            }
1174
1175            return view;
1176        }
1177
1178        @Override
1179        public boolean isInTouchMode() {
1180            // WARNING: Please read the comment where mListSelectionHidden is declared
1181            return (mHijackFocus && mListSelectionHidden) || super.isInTouchMode();
1182        }
1183
1184        /**
1185         * <p>Returns the focus state in the drop down.</p>
1186         *
1187         * @return true always if hijacking focus
1188         */
1189        @Override
1190        public boolean hasWindowFocus() {
1191            return mHijackFocus || super.hasWindowFocus();
1192        }
1193
1194        /**
1195         * <p>Returns the focus state in the drop down.</p>
1196         *
1197         * @return true always if hijacking focus
1198         */
1199        @Override
1200        public boolean isFocused() {
1201            return mHijackFocus || super.isFocused();
1202        }
1203
1204        /**
1205         * <p>Returns the focus state in the drop down.</p>
1206         *
1207         * @return true always if hijacking focus
1208         */
1209        @Override
1210        public boolean hasFocus() {
1211            return mHijackFocus || super.hasFocus();
1212        }
1213    }
1214
1215    private class PopupDataSetObserver extends DataSetObserver {
1216        @Override
1217        public void onChanged() {
1218            if (isShowing()) {
1219                // Resize the popup to fit new content
1220                show();
1221            }
1222        }
1223
1224        @Override
1225        public void onInvalidated() {
1226            dismiss();
1227        }
1228    }
1229
1230    private class ListSelectorHider implements Runnable {
1231        public void run() {
1232            clearListSelection();
1233        }
1234    }
1235
1236    private class ResizePopupRunnable implements Runnable {
1237        public void run() {
1238            if (mDropDownList != null && mDropDownList.getCount() > mDropDownList.getChildCount() &&
1239                    mDropDownList.getChildCount() <= mListItemExpandMaximum) {
1240                mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
1241                show();
1242            }
1243        }
1244    }
1245
1246    private class PopupTouchInterceptor implements OnTouchListener {
1247        public boolean onTouch(View v, MotionEvent event) {
1248            final int action = event.getAction();
1249            final int x = (int) event.getX();
1250            final int y = (int) event.getY();
1251
1252            if (action == MotionEvent.ACTION_DOWN &&
1253                    mPopup != null && mPopup.isShowing() &&
1254                    (x >= 0 && x < mPopup.getWidth() && y >= 0 && y < mPopup.getHeight())) {
1255                mHandler.postDelayed(mResizePopupRunnable, EXPAND_LIST_TIMEOUT);
1256            } else if (action == MotionEvent.ACTION_UP) {
1257                mHandler.removeCallbacks(mResizePopupRunnable);
1258            }
1259            return false;
1260        }
1261    }
1262
1263    private class PopupScrollListener implements ListView.OnScrollListener {
1264        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
1265                int totalItemCount) {
1266
1267        }
1268
1269        public void onScrollStateChanged(AbsListView view, int scrollState) {
1270            if (scrollState == SCROLL_STATE_TOUCH_SCROLL &&
1271                    !isInputMethodNotNeeded() && mPopup.getContentView() != null) {
1272                mHandler.removeCallbacks(mResizePopupRunnable);
1273                mResizePopupRunnable.run();
1274            }
1275        }
1276    }
1277}
1278