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