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