ListPopupWindow.java revision 6090995951c6e2e4dcf38102f01793f8a94166e1
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.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ObjectAnimator;
22import android.content.Context;
23import android.database.DataSetObserver;
24import android.graphics.Rect;
25import android.graphics.drawable.Drawable;
26import android.os.Handler;
27import android.text.TextUtils;
28import android.util.AttributeSet;
29import android.util.IntProperty;
30import android.util.Log;
31import android.view.Gravity;
32import android.view.KeyEvent;
33import android.view.MotionEvent;
34import android.view.View;
35import android.view.View.MeasureSpec;
36import android.view.View.OnTouchListener;
37import android.view.ViewConfiguration;
38import android.view.ViewGroup;
39import android.view.ViewParent;
40import android.view.animation.AccelerateDecelerateInterpolator;
41
42import com.android.internal.widget.AutoScrollHelper.AbsListViewAutoScroller;
43
44import java.util.Locale;
45
46/**
47 * A ListPopupWindow anchors itself to a host view and displays a
48 * list of choices.
49 *
50 * <p>ListPopupWindow contains a number of tricky behaviors surrounding
51 * positioning, scrolling parents to fit the dropdown, interacting
52 * sanely with the IME if present, and others.
53 *
54 * @see android.widget.AutoCompleteTextView
55 * @see android.widget.Spinner
56 */
57public class ListPopupWindow {
58    private static final String TAG = "ListPopupWindow";
59    private static final boolean DEBUG = false;
60
61    /**
62     * This value controls the length of time that the user
63     * must leave a pointer down without scrolling to expand
64     * the autocomplete dropdown list to cover the IME.
65     */
66    private static final int EXPAND_LIST_TIMEOUT = 250;
67
68    private Context mContext;
69    private PopupWindow mPopup;
70    private ListAdapter mAdapter;
71    private DropDownListView mDropDownList;
72
73    private int mDropDownHeight = ViewGroup.LayoutParams.WRAP_CONTENT;
74    private int mDropDownWidth = ViewGroup.LayoutParams.WRAP_CONTENT;
75    private int mDropDownHorizontalOffset;
76    private int mDropDownVerticalOffset;
77    private boolean mDropDownVerticalOffsetSet;
78
79    private int mDropDownGravity = Gravity.NO_GRAVITY;
80
81    private boolean mDropDownAlwaysVisible = false;
82    private boolean mForceIgnoreOutsideTouch = false;
83    int mListItemExpandMaximum = Integer.MAX_VALUE;
84
85    private View mPromptView;
86    private int mPromptPosition = POSITION_PROMPT_ABOVE;
87
88    private DataSetObserver mObserver;
89
90    private View mDropDownAnchorView;
91
92    private Drawable mDropDownListHighlight;
93
94    private AdapterView.OnItemClickListener mItemClickListener;
95    private AdapterView.OnItemSelectedListener mItemSelectedListener;
96
97    private final ResizePopupRunnable mResizePopupRunnable = new ResizePopupRunnable();
98    private final PopupTouchInterceptor mTouchInterceptor = new PopupTouchInterceptor();
99    private final PopupScrollListener mScrollListener = new PopupScrollListener();
100    private final ListSelectorHider mHideSelector = new ListSelectorHider();
101    private Runnable mShowDropDownRunnable;
102
103    private Handler mHandler = new Handler();
104
105    private Rect mTempRect = new Rect();
106
107    private boolean mModal;
108
109    private int mLayoutDirection;
110
111    /**
112     * The provided prompt view should appear above list content.
113     *
114     * @see #setPromptPosition(int)
115     * @see #getPromptPosition()
116     * @see #setPromptView(View)
117     */
118    public static final int POSITION_PROMPT_ABOVE = 0;
119
120    /**
121     * The provided prompt view should appear below list content.
122     *
123     * @see #setPromptPosition(int)
124     * @see #getPromptPosition()
125     * @see #setPromptView(View)
126     */
127    public static final int POSITION_PROMPT_BELOW = 1;
128
129    /**
130     * Alias for {@link ViewGroup.LayoutParams#MATCH_PARENT}.
131     * If used to specify a popup width, the popup will match the width of the anchor view.
132     * If used to specify a popup height, the popup will fill available space.
133     */
134    public static final int MATCH_PARENT = ViewGroup.LayoutParams.MATCH_PARENT;
135
136    /**
137     * Alias for {@link ViewGroup.LayoutParams#WRAP_CONTENT}.
138     * If used to specify a popup width, the popup will use the width of its content.
139     */
140    public static final int WRAP_CONTENT = ViewGroup.LayoutParams.WRAP_CONTENT;
141
142    /**
143     * Mode for {@link #setInputMethodMode(int)}: the requirements for the
144     * input method should be based on the focusability of the popup.  That is
145     * if it is focusable than it needs to work with the input method, else
146     * it doesn't.
147     */
148    public static final int INPUT_METHOD_FROM_FOCUSABLE = PopupWindow.INPUT_METHOD_FROM_FOCUSABLE;
149
150    /**
151     * Mode for {@link #setInputMethodMode(int)}: this popup always needs to
152     * work with an input method, regardless of whether it is focusable.  This
153     * means that it will always be displayed so that the user can also operate
154     * the input method while it is shown.
155     */
156    public static final int INPUT_METHOD_NEEDED = PopupWindow.INPUT_METHOD_NEEDED;
157
158    /**
159     * Mode for {@link #setInputMethodMode(int)}: this popup never needs to
160     * work with an input method, regardless of whether it is focusable.  This
161     * means that it will always be displayed to use as much space on the
162     * screen as needed, regardless of whether this covers the input method.
163     */
164    public static final int INPUT_METHOD_NOT_NEEDED = PopupWindow.INPUT_METHOD_NOT_NEEDED;
165
166    /**
167     * Create a new, empty popup window capable of displaying items from a ListAdapter.
168     * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}.
169     *
170     * @param context Context used for contained views.
171     */
172    public ListPopupWindow(Context context) {
173        this(context, null, 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     */
183    public ListPopupWindow(Context context, AttributeSet attrs) {
184        this(context, attrs, com.android.internal.R.attr.listPopupWindowStyle, 0);
185    }
186
187    /**
188     * Create a new, empty popup window capable of displaying items from a ListAdapter.
189     * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}.
190     *
191     * @param context Context used for contained views.
192     * @param attrs Attributes from inflating parent views used to style the popup.
193     * @param defStyleAttr Default style attribute to use for popup content.
194     */
195    public ListPopupWindow(Context context, AttributeSet attrs, int defStyleAttr) {
196        this(context, attrs, defStyleAttr, 0);
197    }
198
199    /**
200     * Create a new, empty popup window capable of displaying items from a ListAdapter.
201     * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}.
202     *
203     * @param context Context used for contained views.
204     * @param attrs Attributes from inflating parent views used to style the popup.
205     * @param defStyleAttr Style attribute to read for default styling of popup content.
206     * @param defStyleRes Style resource ID to use for default styling of popup content.
207     */
208    public ListPopupWindow(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
209        mContext = context;
210        mPopup = new PopupWindow(context, attrs, defStyleAttr, defStyleRes);
211        mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
212        // Set the default layout direction to match the default locale one
213        final Locale locale = mContext.getResources().getConfiguration().locale;
214        mLayoutDirection = TextUtils.getLayoutDirectionFromLocale(locale);
215    }
216
217    /**
218     * Sets the adapter that provides the data and the views to represent the data
219     * in this popup window.
220     *
221     * @param adapter The adapter to use to create this window's content.
222     */
223    public void setAdapter(ListAdapter adapter) {
224        if (mObserver == null) {
225            mObserver = new PopupDataSetObserver();
226        } else if (mAdapter != null) {
227            mAdapter.unregisterDataSetObserver(mObserver);
228        }
229        mAdapter = adapter;
230        if (mAdapter != null) {
231            adapter.registerDataSetObserver(mObserver);
232        }
233
234        if (mDropDownList != null) {
235            mDropDownList.setAdapter(mAdapter);
236        }
237    }
238
239    /**
240     * Set where the optional prompt view should appear. The default is
241     * {@link #POSITION_PROMPT_ABOVE}.
242     *
243     * @param position A position constant declaring where the prompt should be displayed.
244     *
245     * @see #POSITION_PROMPT_ABOVE
246     * @see #POSITION_PROMPT_BELOW
247     */
248    public void setPromptPosition(int position) {
249        mPromptPosition = position;
250    }
251
252    /**
253     * @return Where the optional prompt view should appear.
254     *
255     * @see #POSITION_PROMPT_ABOVE
256     * @see #POSITION_PROMPT_BELOW
257     */
258    public int getPromptPosition() {
259        return mPromptPosition;
260    }
261
262    /**
263     * Set whether this window should be modal when shown.
264     *
265     * <p>If a popup window is modal, it will receive all touch and key input.
266     * If the user touches outside the popup window's content area the popup window
267     * will be dismissed.
268     *
269     * @param modal {@code true} if the popup window should be modal, {@code false} otherwise.
270     */
271    public void setModal(boolean modal) {
272        mModal = true;
273        mPopup.setFocusable(modal);
274    }
275
276    /**
277     * Returns whether the popup window will be modal when shown.
278     *
279     * @return {@code true} if the popup window will be modal, {@code false} otherwise.
280     */
281    public boolean isModal() {
282        return mModal;
283    }
284
285    /**
286     * Forces outside touches to be ignored. Normally if {@link #isDropDownAlwaysVisible()} is
287     * false, we allow outside touch to dismiss the dropdown. If this is set to true, then we
288     * ignore outside touch even when the drop down is not set to always visible.
289     *
290     * @hide Used only by AutoCompleteTextView to handle some internal special cases.
291     */
292    public void setForceIgnoreOutsideTouch(boolean forceIgnoreOutsideTouch) {
293        mForceIgnoreOutsideTouch = forceIgnoreOutsideTouch;
294    }
295
296    /**
297     * Sets whether the drop-down should remain visible under certain conditions.
298     *
299     * The drop-down will occupy the entire screen below {@link #getAnchorView} regardless
300     * of the size or content of the list.  {@link #getBackground()} will fill any space
301     * that is not used by the list.
302     *
303     * @param dropDownAlwaysVisible Whether to keep the drop-down visible.
304     *
305     * @hide Only used by AutoCompleteTextView under special conditions.
306     */
307    public void setDropDownAlwaysVisible(boolean dropDownAlwaysVisible) {
308        mDropDownAlwaysVisible = dropDownAlwaysVisible;
309    }
310
311    /**
312     * @return Whether the drop-down is visible under special conditions.
313     *
314     * @hide Only used by AutoCompleteTextView under special conditions.
315     */
316    public boolean isDropDownAlwaysVisible() {
317        return mDropDownAlwaysVisible;
318    }
319
320    /**
321     * Sets the operating mode for the soft input area.
322     *
323     * @param mode The desired mode, see
324     *        {@link android.view.WindowManager.LayoutParams#softInputMode}
325     *        for the full list
326     *
327     * @see android.view.WindowManager.LayoutParams#softInputMode
328     * @see #getSoftInputMode()
329     */
330    public void setSoftInputMode(int mode) {
331        mPopup.setSoftInputMode(mode);
332    }
333
334    /**
335     * Returns the current value in {@link #setSoftInputMode(int)}.
336     *
337     * @see #setSoftInputMode(int)
338     * @see android.view.WindowManager.LayoutParams#softInputMode
339     */
340    public int getSoftInputMode() {
341        return mPopup.getSoftInputMode();
342    }
343
344    /**
345     * Sets a drawable to use as the list item selector.
346     *
347     * @param selector List selector drawable to use in the popup.
348     */
349    public void setListSelector(Drawable selector) {
350        mDropDownListHighlight = selector;
351    }
352
353    /**
354     * @return The background drawable for the popup window.
355     */
356    public Drawable getBackground() {
357        return mPopup.getBackground();
358    }
359
360    /**
361     * Sets a drawable to be the background for the popup window.
362     *
363     * @param d A drawable to set as the background.
364     */
365    public void setBackgroundDrawable(Drawable d) {
366        mPopup.setBackgroundDrawable(d);
367    }
368
369    /**
370     * Set an animation style to use when the popup window is shown or dismissed.
371     *
372     * @param animationStyle Animation style to use.
373     */
374    public void setAnimationStyle(int animationStyle) {
375        mPopup.setAnimationStyle(animationStyle);
376    }
377
378    /**
379     * Returns the animation style that will be used when the popup window is
380     * shown or dismissed.
381     *
382     * @return Animation style that will be used.
383     */
384    public int getAnimationStyle() {
385        return mPopup.getAnimationStyle();
386    }
387
388    /**
389     * Returns the view that will be used to anchor this popup.
390     *
391     * @return The popup's anchor view
392     */
393    public View getAnchorView() {
394        return mDropDownAnchorView;
395    }
396
397    /**
398     * Sets the popup's anchor view. This popup will always be positioned relative to
399     * the anchor view when shown.
400     *
401     * @param anchor The view to use as an anchor.
402     */
403    public void setAnchorView(View anchor) {
404        mDropDownAnchorView = anchor;
405    }
406
407    /**
408     * @return The horizontal offset of the popup from its anchor in pixels.
409     */
410    public int getHorizontalOffset() {
411        return mDropDownHorizontalOffset;
412    }
413
414    /**
415     * Set the horizontal offset of this popup from its anchor view in pixels.
416     *
417     * @param offset The horizontal offset of the popup from its anchor.
418     */
419    public void setHorizontalOffset(int offset) {
420        mDropDownHorizontalOffset = offset;
421    }
422
423    /**
424     * @return The vertical offset of the popup from its anchor in pixels.
425     */
426    public int getVerticalOffset() {
427        if (!mDropDownVerticalOffsetSet) {
428            return 0;
429        }
430        return mDropDownVerticalOffset;
431    }
432
433    /**
434     * Set the vertical offset of this popup from its anchor view in pixels.
435     *
436     * @param offset The vertical offset of the popup from its anchor.
437     */
438    public void setVerticalOffset(int offset) {
439        mDropDownVerticalOffset = offset;
440        mDropDownVerticalOffsetSet = true;
441    }
442
443    /**
444     * Set the gravity of the dropdown list. This is commonly used to
445     * set gravity to START or END for alignment with the anchor.
446     *
447     * @param gravity Gravity value to use
448     */
449    public void setDropDownGravity(int gravity) {
450        mDropDownGravity = gravity;
451    }
452
453    /**
454     * @return The width of the popup window in pixels.
455     */
456    public int getWidth() {
457        return mDropDownWidth;
458    }
459
460    /**
461     * Sets the width of the popup window in pixels. Can also be {@link #MATCH_PARENT}
462     * or {@link #WRAP_CONTENT}.
463     *
464     * @param width Width of the popup window.
465     */
466    public void setWidth(int width) {
467        mDropDownWidth = width;
468    }
469
470    /**
471     * Sets the width of the popup window by the size of its content. The final width may be
472     * larger to accommodate styled window dressing.
473     *
474     * @param width Desired width of content in pixels.
475     */
476    public void setContentWidth(int width) {
477        Drawable popupBackground = mPopup.getBackground();
478        if (popupBackground != null) {
479            popupBackground.getPadding(mTempRect);
480            mDropDownWidth = mTempRect.left + mTempRect.right + width;
481        } else {
482            setWidth(width);
483        }
484    }
485
486    /**
487     * @return The height of the popup window in pixels.
488     */
489    public int getHeight() {
490        return mDropDownHeight;
491    }
492
493    /**
494     * Sets the height of the popup window in pixels. Can also be {@link #MATCH_PARENT}.
495     *
496     * @param height Height of the popup window.
497     */
498    public void setHeight(int height) {
499        mDropDownHeight = height;
500    }
501
502    /**
503     * Sets a listener to receive events when a list item is clicked.
504     *
505     * @param clickListener Listener to register
506     *
507     * @see ListView#setOnItemClickListener(android.widget.AdapterView.OnItemClickListener)
508     */
509    public void setOnItemClickListener(AdapterView.OnItemClickListener clickListener) {
510        mItemClickListener = clickListener;
511    }
512
513    /**
514     * Sets a listener to receive events when a list item is selected.
515     *
516     * @param selectedListener Listener to register.
517     *
518     * @see ListView#setOnItemSelectedListener(android.widget.AdapterView.OnItemSelectedListener)
519     */
520    public void setOnItemSelectedListener(AdapterView.OnItemSelectedListener selectedListener) {
521        mItemSelectedListener = selectedListener;
522    }
523
524    /**
525     * Set a view to act as a user prompt for this popup window. Where the prompt view will appear
526     * is controlled by {@link #setPromptPosition(int)}.
527     *
528     * @param prompt View to use as an informational prompt.
529     */
530    public void setPromptView(View prompt) {
531        boolean showing = isShowing();
532        if (showing) {
533            removePromptView();
534        }
535        mPromptView = prompt;
536        if (showing) {
537            show();
538        }
539    }
540
541    /**
542     * Post a {@link #show()} call to the UI thread.
543     */
544    public void postShow() {
545        mHandler.post(mShowDropDownRunnable);
546    }
547
548    /**
549     * Show the popup list. If the list is already showing, this method
550     * will recalculate the popup's size and position.
551     */
552    public void show() {
553        int height = buildDropDown();
554
555        int widthSpec = 0;
556        int heightSpec = 0;
557
558        boolean noInputMethod = isInputMethodNotNeeded();
559        mPopup.setAllowScrollingAnchorParent(!noInputMethod);
560
561        if (mPopup.isShowing()) {
562            if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) {
563                // The call to PopupWindow's update method below can accept -1 for any
564                // value you do not want to update.
565                widthSpec = -1;
566            } else if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) {
567                widthSpec = getAnchorView().getWidth();
568            } else {
569                widthSpec = mDropDownWidth;
570            }
571
572            if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
573                // The call to PopupWindow's update method below can accept -1 for any
574                // value you do not want to update.
575                heightSpec = noInputMethod ? height : ViewGroup.LayoutParams.MATCH_PARENT;
576                if (noInputMethod) {
577                    mPopup.setWindowLayoutMode(
578                            mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ?
579                                    ViewGroup.LayoutParams.MATCH_PARENT : 0, 0);
580                } else {
581                    mPopup.setWindowLayoutMode(
582                            mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ?
583                                    ViewGroup.LayoutParams.MATCH_PARENT : 0,
584                            ViewGroup.LayoutParams.MATCH_PARENT);
585                }
586            } else if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
587                heightSpec = height;
588            } else {
589                heightSpec = mDropDownHeight;
590            }
591
592            mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible);
593
594            mPopup.update(getAnchorView(), mDropDownHorizontalOffset,
595                    mDropDownVerticalOffset, widthSpec, heightSpec);
596        } else {
597            if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) {
598                widthSpec = ViewGroup.LayoutParams.MATCH_PARENT;
599            } else {
600                if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) {
601                    mPopup.setWidth(getAnchorView().getWidth());
602                } else {
603                    mPopup.setWidth(mDropDownWidth);
604                }
605            }
606
607            if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
608                heightSpec = ViewGroup.LayoutParams.MATCH_PARENT;
609            } else {
610                if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
611                    mPopup.setHeight(height);
612                } else {
613                    mPopup.setHeight(mDropDownHeight);
614                }
615            }
616
617            mPopup.setWindowLayoutMode(widthSpec, heightSpec);
618            mPopup.setClipToScreenEnabled(true);
619
620            // use outside touchable to dismiss drop down when touching outside of it, so
621            // only set this if the dropdown is not always visible
622            mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible);
623            mPopup.setTouchInterceptor(mTouchInterceptor);
624            mPopup.showAsDropDown(getAnchorView(),
625                    mDropDownHorizontalOffset, mDropDownVerticalOffset, mDropDownGravity);
626            mDropDownList.setSelection(ListView.INVALID_POSITION);
627
628            if (!mModal || mDropDownList.isInTouchMode()) {
629                clearListSelection();
630            }
631            if (!mModal) {
632                mHandler.post(mHideSelector);
633            }
634        }
635    }
636
637    /**
638     * Dismiss the popup window.
639     */
640    public void dismiss() {
641        mPopup.dismiss();
642        removePromptView();
643        mPopup.setContentView(null);
644        mDropDownList = null;
645        mHandler.removeCallbacks(mResizePopupRunnable);
646    }
647
648    /**
649     * Set a listener to receive a callback when the popup is dismissed.
650     *
651     * @param listener Listener that will be notified when the popup is dismissed.
652     */
653    public void setOnDismissListener(PopupWindow.OnDismissListener listener) {
654        mPopup.setOnDismissListener(listener);
655    }
656
657    private void removePromptView() {
658        if (mPromptView != null) {
659            final ViewParent parent = mPromptView.getParent();
660            if (parent instanceof ViewGroup) {
661                final ViewGroup group = (ViewGroup) parent;
662                group.removeView(mPromptView);
663            }
664        }
665    }
666
667    /**
668     * Control how the popup operates with an input method: one of
669     * {@link #INPUT_METHOD_FROM_FOCUSABLE}, {@link #INPUT_METHOD_NEEDED},
670     * or {@link #INPUT_METHOD_NOT_NEEDED}.
671     *
672     * <p>If the popup is showing, calling this method will take effect only
673     * the next time the popup is shown or through a manual call to the {@link #show()}
674     * method.</p>
675     *
676     * @see #getInputMethodMode()
677     * @see #show()
678     */
679    public void setInputMethodMode(int mode) {
680        mPopup.setInputMethodMode(mode);
681    }
682
683    /**
684     * Return the current value in {@link #setInputMethodMode(int)}.
685     *
686     * @see #setInputMethodMode(int)
687     */
688    public int getInputMethodMode() {
689        return mPopup.getInputMethodMode();
690    }
691
692    /**
693     * Set the selected position of the list.
694     * Only valid when {@link #isShowing()} == {@code true}.
695     *
696     * @param position List position to set as selected.
697     */
698    public void setSelection(int position) {
699        DropDownListView list = mDropDownList;
700        if (isShowing() && list != null) {
701            list.mListSelectionHidden = false;
702            list.setSelection(position);
703            if (list.getChoiceMode() != ListView.CHOICE_MODE_NONE) {
704                list.setItemChecked(position, true);
705            }
706        }
707    }
708
709    /**
710     * Clear any current list selection.
711     * Only valid when {@link #isShowing()} == {@code true}.
712     */
713    public void clearListSelection() {
714        final DropDownListView list = mDropDownList;
715        if (list != null) {
716            // WARNING: Please read the comment where mListSelectionHidden is declared
717            list.mListSelectionHidden = true;
718            list.hideSelector();
719            list.requestLayout();
720        }
721    }
722
723    /**
724     * @return {@code true} if the popup is currently showing, {@code false} otherwise.
725     */
726    public boolean isShowing() {
727        return mPopup.isShowing();
728    }
729
730    /**
731     * @return {@code true} if this popup is configured to assume the user does not need
732     * to interact with the IME while it is showing, {@code false} otherwise.
733     */
734    public boolean isInputMethodNotNeeded() {
735        return mPopup.getInputMethodMode() == INPUT_METHOD_NOT_NEEDED;
736    }
737
738    /**
739     * Perform an item click operation on the specified list adapter position.
740     *
741     * @param position Adapter position for performing the click
742     * @return true if the click action could be performed, false if not.
743     *         (e.g. if the popup was not showing, this method would return false.)
744     */
745    public boolean performItemClick(int position) {
746        if (isShowing()) {
747            if (mItemClickListener != null) {
748                final DropDownListView list = mDropDownList;
749                final View child = list.getChildAt(position - list.getFirstVisiblePosition());
750                final ListAdapter adapter = list.getAdapter();
751                mItemClickListener.onItemClick(list, child, position, adapter.getItemId(position));
752            }
753            return true;
754        }
755        return false;
756    }
757
758    /**
759     * @return The currently selected item or null if the popup is not showing.
760     */
761    public Object getSelectedItem() {
762        if (!isShowing()) {
763            return null;
764        }
765        return mDropDownList.getSelectedItem();
766    }
767
768    /**
769     * @return The position of the currently selected item or {@link ListView#INVALID_POSITION}
770     * if {@link #isShowing()} == {@code false}.
771     *
772     * @see ListView#getSelectedItemPosition()
773     */
774    public int getSelectedItemPosition() {
775        if (!isShowing()) {
776            return ListView.INVALID_POSITION;
777        }
778        return mDropDownList.getSelectedItemPosition();
779    }
780
781    /**
782     * @return The ID of the currently selected item or {@link ListView#INVALID_ROW_ID}
783     * if {@link #isShowing()} == {@code false}.
784     *
785     * @see ListView#getSelectedItemId()
786     */
787    public long getSelectedItemId() {
788        if (!isShowing()) {
789            return ListView.INVALID_ROW_ID;
790        }
791        return mDropDownList.getSelectedItemId();
792    }
793
794    /**
795     * @return The View for the currently selected item or null if
796     * {@link #isShowing()} == {@code false}.
797     *
798     * @see ListView#getSelectedView()
799     */
800    public View getSelectedView() {
801        if (!isShowing()) {
802            return null;
803        }
804        return mDropDownList.getSelectedView();
805    }
806
807    /**
808     * @return The {@link ListView} displayed within the popup window.
809     * Only valid when {@link #isShowing()} == {@code true}.
810     */
811    public ListView getListView() {
812        return mDropDownList;
813    }
814
815    /**
816     * The maximum number of list items that can be visible and still have
817     * the list expand when touched.
818     *
819     * @param max Max number of items that can be visible and still allow the list to expand.
820     */
821    void setListItemExpandMax(int max) {
822        mListItemExpandMaximum = max;
823    }
824
825    /**
826     * Filter key down events. By forwarding key down events to this function,
827     * views using non-modal ListPopupWindow can have it handle key selection of items.
828     *
829     * @param keyCode keyCode param passed to the host view's onKeyDown
830     * @param event event param passed to the host view's onKeyDown
831     * @return true if the event was handled, false if it was ignored.
832     *
833     * @see #setModal(boolean)
834     */
835    public boolean onKeyDown(int keyCode, KeyEvent event) {
836        // when the drop down is shown, we drive it directly
837        if (isShowing()) {
838            // the key events are forwarded to the list in the drop down view
839            // note that ListView handles space but we don't want that to happen
840            // also if selection is not currently in the drop down, then don't
841            // let center or enter presses go there since that would cause it
842            // to select one of its items
843            if (keyCode != KeyEvent.KEYCODE_SPACE
844                    && (mDropDownList.getSelectedItemPosition() >= 0
845                            || !KeyEvent.isConfirmKey(keyCode))) {
846                int curIndex = mDropDownList.getSelectedItemPosition();
847                boolean consumed;
848
849                final boolean below = !mPopup.isAboveAnchor();
850
851                final ListAdapter adapter = mAdapter;
852
853                boolean allEnabled;
854                int firstItem = Integer.MAX_VALUE;
855                int lastItem = Integer.MIN_VALUE;
856
857                if (adapter != null) {
858                    allEnabled = adapter.areAllItemsEnabled();
859                    firstItem = allEnabled ? 0 :
860                            mDropDownList.lookForSelectablePosition(0, true);
861                    lastItem = allEnabled ? adapter.getCount() - 1 :
862                            mDropDownList.lookForSelectablePosition(adapter.getCount() - 1, false);
863                }
864
865                if ((below && keyCode == KeyEvent.KEYCODE_DPAD_UP && curIndex <= firstItem) ||
866                        (!below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN && curIndex >= lastItem)) {
867                    // When the selection is at the top, we block the key
868                    // event to prevent focus from moving.
869                    clearListSelection();
870                    mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
871                    show();
872                    return true;
873                } else {
874                    // WARNING: Please read the comment where mListSelectionHidden
875                    //          is declared
876                    mDropDownList.mListSelectionHidden = false;
877                }
878
879                consumed = mDropDownList.onKeyDown(keyCode, event);
880                if (DEBUG) Log.v(TAG, "Key down: code=" + keyCode + " list consumed=" + consumed);
881
882                if (consumed) {
883                    // If it handled the key event, then the user is
884                    // navigating in the list, so we should put it in front.
885                    mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
886                    // Here's a little trick we need to do to make sure that
887                    // the list view is actually showing its focus indicator,
888                    // by ensuring it has focus and getting its window out
889                    // of touch mode.
890                    mDropDownList.requestFocusFromTouch();
891                    show();
892
893                    switch (keyCode) {
894                        // avoid passing the focus from the text view to the
895                        // next component
896                        case KeyEvent.KEYCODE_ENTER:
897                        case KeyEvent.KEYCODE_DPAD_CENTER:
898                        case KeyEvent.KEYCODE_DPAD_DOWN:
899                        case KeyEvent.KEYCODE_DPAD_UP:
900                            return true;
901                    }
902                } else {
903                    if (below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
904                        // when the selection is at the bottom, we block the
905                        // event to avoid going to the next focusable widget
906                        if (curIndex == lastItem) {
907                            return true;
908                        }
909                    } else if (!below && keyCode == KeyEvent.KEYCODE_DPAD_UP &&
910                            curIndex == firstItem) {
911                        return true;
912                    }
913                }
914            }
915        }
916
917        return false;
918    }
919
920    /**
921     * Filter key down events. By forwarding key up events to this function,
922     * views using non-modal ListPopupWindow can have it handle key selection of items.
923     *
924     * @param keyCode keyCode param passed to the host view's onKeyUp
925     * @param event event param passed to the host view's onKeyUp
926     * @return true if the event was handled, false if it was ignored.
927     *
928     * @see #setModal(boolean)
929     */
930    public boolean onKeyUp(int keyCode, KeyEvent event) {
931        if (isShowing() && mDropDownList.getSelectedItemPosition() >= 0) {
932            boolean consumed = mDropDownList.onKeyUp(keyCode, event);
933            if (consumed && KeyEvent.isConfirmKey(keyCode)) {
934                // if the list accepts the key events and the key event was a click, the text view
935                // gets the selected item from the drop down as its content
936                dismiss();
937            }
938            return consumed;
939        }
940        return false;
941    }
942
943    /**
944     * Filter pre-IME key events. By forwarding {@link View#onKeyPreIme(int, KeyEvent)}
945     * events to this function, views using ListPopupWindow can have it dismiss the popup
946     * when the back key is pressed.
947     *
948     * @param keyCode keyCode param passed to the host view's onKeyPreIme
949     * @param event event param passed to the host view's onKeyPreIme
950     * @return true if the event was handled, false if it was ignored.
951     *
952     * @see #setModal(boolean)
953     */
954    public boolean onKeyPreIme(int keyCode, KeyEvent event) {
955        if (keyCode == KeyEvent.KEYCODE_BACK && isShowing()) {
956            // special case for the back key, we do not even try to send it
957            // to the drop down list but instead, consume it immediately
958            final View anchorView = mDropDownAnchorView;
959            if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
960                KeyEvent.DispatcherState state = anchorView.getKeyDispatcherState();
961                if (state != null) {
962                    state.startTracking(event, this);
963                }
964                return true;
965            } else if (event.getAction() == KeyEvent.ACTION_UP) {
966                KeyEvent.DispatcherState state = anchorView.getKeyDispatcherState();
967                if (state != null) {
968                    state.handleUpEvent(event);
969                }
970                if (event.isTracking() && !event.isCanceled()) {
971                    dismiss();
972                    return true;
973                }
974            }
975        }
976        return false;
977    }
978
979    /**
980     * Returns an {@link OnTouchListener} that can be added to the source view
981     * to implement drag-to-open behavior. Generally, the source view should be
982     * the same view that was passed to {@link #setAnchorView}.
983     * <p>
984     * When the listener is set on a view, touching that view and dragging
985     * outside of its bounds will open the popup window. Lifting will select the
986     * currently touched list item.
987     * <p>
988     * Example usage:
989     * <pre>
990     * ListPopupWindow myPopup = new ListPopupWindow(context);
991     * myPopup.setAnchor(myAnchor);
992     * OnTouchListener dragListener = myPopup.createDragToOpenListener(myAnchor);
993     * myAnchor.setOnTouchListener(dragListener);
994     * </pre>
995     *
996     * @param src the view on which the resulting listener will be set
997     * @return a touch listener that controls drag-to-open behavior
998     */
999    public OnTouchListener createDragToOpenListener(View src) {
1000        return new ForwardingListener(src) {
1001            @Override
1002            public ListPopupWindow getPopup() {
1003                return ListPopupWindow.this;
1004            }
1005        };
1006    }
1007
1008    /**
1009     * <p>Builds the popup window's content and returns the height the popup
1010     * should have. Returns -1 when the content already exists.</p>
1011     *
1012     * @return the content's height or -1 if content already exists
1013     */
1014    private int buildDropDown() {
1015        ViewGroup dropDownView;
1016        int otherHeights = 0;
1017
1018        if (mDropDownList == null) {
1019            Context context = mContext;
1020
1021            /**
1022             * This Runnable exists for the sole purpose of checking if the view layout has got
1023             * completed and if so call showDropDown to display the drop down. This is used to show
1024             * the drop down as soon as possible after user opens up the search dialog, without
1025             * waiting for the normal UI pipeline to do it's job which is slower than this method.
1026             */
1027            mShowDropDownRunnable = new Runnable() {
1028                public void run() {
1029                    // View layout should be all done before displaying the drop down.
1030                    View view = getAnchorView();
1031                    if (view != null && view.getWindowToken() != null) {
1032                        show();
1033                    }
1034                }
1035            };
1036
1037            mDropDownList = new DropDownListView(context, !mModal);
1038            if (mDropDownListHighlight != null) {
1039                mDropDownList.setSelector(mDropDownListHighlight);
1040            }
1041            mDropDownList.setAdapter(mAdapter);
1042            mDropDownList.setOnItemClickListener(mItemClickListener);
1043            mDropDownList.setFocusable(true);
1044            mDropDownList.setFocusableInTouchMode(true);
1045            mDropDownList.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
1046                public void onItemSelected(AdapterView<?> parent, View view,
1047                        int position, long id) {
1048
1049                    if (position != -1) {
1050                        DropDownListView dropDownList = mDropDownList;
1051
1052                        if (dropDownList != null) {
1053                            dropDownList.mListSelectionHidden = false;
1054                        }
1055                    }
1056                }
1057
1058                public void onNothingSelected(AdapterView<?> parent) {
1059                }
1060            });
1061            mDropDownList.setOnScrollListener(mScrollListener);
1062
1063            if (mItemSelectedListener != null) {
1064                mDropDownList.setOnItemSelectedListener(mItemSelectedListener);
1065            }
1066
1067            dropDownView = mDropDownList;
1068
1069            View hintView = mPromptView;
1070            if (hintView != null) {
1071                // if a hint has been specified, we accomodate more space for it and
1072                // add a text view in the drop down menu, at the bottom of the list
1073                LinearLayout hintContainer = new LinearLayout(context);
1074                hintContainer.setOrientation(LinearLayout.VERTICAL);
1075
1076                LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams(
1077                        ViewGroup.LayoutParams.MATCH_PARENT, 0, 1.0f
1078                );
1079
1080                switch (mPromptPosition) {
1081                case POSITION_PROMPT_BELOW:
1082                    hintContainer.addView(dropDownView, hintParams);
1083                    hintContainer.addView(hintView);
1084                    break;
1085
1086                case POSITION_PROMPT_ABOVE:
1087                    hintContainer.addView(hintView);
1088                    hintContainer.addView(dropDownView, hintParams);
1089                    break;
1090
1091                default:
1092                    Log.e(TAG, "Invalid hint position " + mPromptPosition);
1093                    break;
1094                }
1095
1096                // measure the hint's height to find how much more vertical space
1097                // we need to add to the drop down's height
1098                int widthSpec = MeasureSpec.makeMeasureSpec(mDropDownWidth, MeasureSpec.AT_MOST);
1099                int heightSpec = MeasureSpec.UNSPECIFIED;
1100                hintView.measure(widthSpec, heightSpec);
1101
1102                hintParams = (LinearLayout.LayoutParams) hintView.getLayoutParams();
1103                otherHeights = hintView.getMeasuredHeight() + hintParams.topMargin
1104                        + hintParams.bottomMargin;
1105
1106                dropDownView = hintContainer;
1107            }
1108
1109            mPopup.setContentView(dropDownView);
1110        } else {
1111            dropDownView = (ViewGroup) mPopup.getContentView();
1112            final View view = mPromptView;
1113            if (view != null) {
1114                LinearLayout.LayoutParams hintParams =
1115                        (LinearLayout.LayoutParams) view.getLayoutParams();
1116                otherHeights = view.getMeasuredHeight() + hintParams.topMargin
1117                        + hintParams.bottomMargin;
1118            }
1119        }
1120
1121        // getMaxAvailableHeight() subtracts the padding, so we put it back
1122        // to get the available height for the whole window
1123        int padding = 0;
1124        Drawable background = mPopup.getBackground();
1125        if (background != null) {
1126            background.getPadding(mTempRect);
1127            padding = mTempRect.top + mTempRect.bottom;
1128
1129            // If we don't have an explicit vertical offset, determine one from the window
1130            // background so that content will line up.
1131            if (!mDropDownVerticalOffsetSet) {
1132                mDropDownVerticalOffset = -mTempRect.top;
1133            }
1134        } else {
1135            mTempRect.setEmpty();
1136        }
1137
1138        // Max height available on the screen for a popup.
1139        boolean ignoreBottomDecorations =
1140                mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED;
1141        final int maxHeight = mPopup.getMaxAvailableHeight(
1142                getAnchorView(), mDropDownVerticalOffset, ignoreBottomDecorations);
1143
1144        if (mDropDownAlwaysVisible || mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
1145            return maxHeight + padding;
1146        }
1147
1148        final int childWidthSpec;
1149        switch (mDropDownWidth) {
1150            case ViewGroup.LayoutParams.WRAP_CONTENT:
1151                childWidthSpec = MeasureSpec.makeMeasureSpec(
1152                        mContext.getResources().getDisplayMetrics().widthPixels -
1153                        (mTempRect.left + mTempRect.right),
1154                        MeasureSpec.AT_MOST);
1155                break;
1156            case ViewGroup.LayoutParams.MATCH_PARENT:
1157                childWidthSpec = MeasureSpec.makeMeasureSpec(
1158                        mContext.getResources().getDisplayMetrics().widthPixels -
1159                        (mTempRect.left + mTempRect.right),
1160                        MeasureSpec.EXACTLY);
1161                break;
1162            default:
1163                childWidthSpec = MeasureSpec.makeMeasureSpec(mDropDownWidth, MeasureSpec.EXACTLY);
1164                break;
1165        }
1166        final int listContent = mDropDownList.measureHeightOfChildren(childWidthSpec,
1167                0, ListView.NO_POSITION, maxHeight - otherHeights, -1);
1168        // add padding only if the list has items in it, that way we don't show
1169        // the popup if it is not needed
1170        if (listContent > 0) otherHeights += padding;
1171
1172        return listContent + otherHeights;
1173    }
1174
1175    /**
1176     * Abstract class that forwards touch events to a {@link ListPopupWindow}.
1177     *
1178     * @hide
1179     */
1180    public static abstract class ForwardingListener
1181            implements View.OnTouchListener, View.OnAttachStateChangeListener {
1182        /** Scaled touch slop, used for detecting movement outside bounds. */
1183        private final float mScaledTouchSlop;
1184
1185        /** Timeout before disallowing intercept on the source's parent. */
1186        private final int mTapTimeout;
1187
1188        /** Source view from which events are forwarded. */
1189        private final View mSrc;
1190
1191        /** Runnable used to prevent conflicts with scrolling parents. */
1192        private Runnable mDisallowIntercept;
1193
1194        /** Whether this listener is currently forwarding touch events. */
1195        private boolean mForwarding;
1196
1197        /** The id of the first pointer down in the current event stream. */
1198        private int mActivePointerId;
1199
1200        public ForwardingListener(View src) {
1201            mSrc = src;
1202            mScaledTouchSlop = ViewConfiguration.get(src.getContext()).getScaledTouchSlop();
1203            mTapTimeout = ViewConfiguration.getTapTimeout();
1204
1205            src.addOnAttachStateChangeListener(this);
1206        }
1207
1208        /**
1209         * Returns the popup to which this listener is forwarding events.
1210         * <p>
1211         * Override this to return the correct popup. If the popup is displayed
1212         * asynchronously, you may also need to override
1213         * {@link #onForwardingStopped} to prevent premature cancelation of
1214         * forwarding.
1215         *
1216         * @return the popup to which this listener is forwarding events
1217         */
1218        public abstract ListPopupWindow getPopup();
1219
1220        @Override
1221        public boolean onTouch(View v, MotionEvent event) {
1222            final boolean wasForwarding = mForwarding;
1223            final boolean forwarding;
1224            if (wasForwarding) {
1225                forwarding = onTouchForwarded(event) || !onForwardingStopped();
1226            } else {
1227                forwarding = onTouchObserved(event) && onForwardingStarted();
1228            }
1229
1230            mForwarding = forwarding;
1231            return forwarding || wasForwarding;
1232        }
1233
1234        @Override
1235        public void onViewAttachedToWindow(View v) {
1236        }
1237
1238        @Override
1239        public void onViewDetachedFromWindow(View v) {
1240            mForwarding = false;
1241            mActivePointerId = MotionEvent.INVALID_POINTER_ID;
1242
1243            if (mDisallowIntercept != null) {
1244                mSrc.removeCallbacks(mDisallowIntercept);
1245            }
1246        }
1247
1248        /**
1249         * Called when forwarding would like to start.
1250         * <p>
1251         * By default, this will show the popup returned by {@link #getPopup()}.
1252         * It may be overridden to perform another action, like clicking the
1253         * source view or preparing the popup before showing it.
1254         *
1255         * @return true to start forwarding, false otherwise
1256         */
1257        protected boolean onForwardingStarted() {
1258            final ListPopupWindow popup = getPopup();
1259            if (popup != null && !popup.isShowing()) {
1260                popup.show();
1261            }
1262            return true;
1263        }
1264
1265        /**
1266         * Called when forwarding would like to stop.
1267         * <p>
1268         * By default, this will dismiss the popup returned by
1269         * {@link #getPopup()}. It may be overridden to perform some other
1270         * action.
1271         *
1272         * @return true to stop forwarding, false otherwise
1273         */
1274        protected boolean onForwardingStopped() {
1275            final ListPopupWindow popup = getPopup();
1276            if (popup != null && popup.isShowing()) {
1277                popup.dismiss();
1278            }
1279            return true;
1280        }
1281
1282        /**
1283         * Observes motion events and determines when to start forwarding.
1284         *
1285         * @param srcEvent motion event in source view coordinates
1286         * @return true to start forwarding motion events, false otherwise
1287         */
1288        private boolean onTouchObserved(MotionEvent srcEvent) {
1289            final View src = mSrc;
1290            if (!src.isEnabled()) {
1291                return false;
1292            }
1293
1294            final int actionMasked = srcEvent.getActionMasked();
1295            switch (actionMasked) {
1296                case MotionEvent.ACTION_DOWN:
1297                    mActivePointerId = srcEvent.getPointerId(0);
1298                    if (mDisallowIntercept == null) {
1299                        mDisallowIntercept = new DisallowIntercept();
1300                    }
1301                    src.postDelayed(mDisallowIntercept, mTapTimeout);
1302                    break;
1303                case MotionEvent.ACTION_MOVE:
1304                    final int activePointerIndex = srcEvent.findPointerIndex(mActivePointerId);
1305                    if (activePointerIndex >= 0) {
1306                        final float x = srcEvent.getX(activePointerIndex);
1307                        final float y = srcEvent.getY(activePointerIndex);
1308                        if (!src.pointInView(x, y, mScaledTouchSlop)) {
1309                            // The pointer has moved outside of the view.
1310                            if (mDisallowIntercept != null) {
1311                                src.removeCallbacks(mDisallowIntercept);
1312                            }
1313                            src.getParent().requestDisallowInterceptTouchEvent(true);
1314                            return true;
1315                        }
1316                    }
1317                    break;
1318                case MotionEvent.ACTION_CANCEL:
1319                case MotionEvent.ACTION_UP:
1320                    if (mDisallowIntercept != null) {
1321                        src.removeCallbacks(mDisallowIntercept);
1322                    }
1323                    break;
1324            }
1325
1326            return false;
1327        }
1328
1329        /**
1330         * Handled forwarded motion events and determines when to stop
1331         * forwarding.
1332         *
1333         * @param srcEvent motion event in source view coordinates
1334         * @return true to continue forwarding motion events, false to cancel
1335         */
1336        private boolean onTouchForwarded(MotionEvent srcEvent) {
1337            final View src = mSrc;
1338            final ListPopupWindow popup = getPopup();
1339            if (popup == null || !popup.isShowing()) {
1340                return false;
1341            }
1342
1343            final DropDownListView dst = popup.mDropDownList;
1344            if (dst == null || !dst.isShown()) {
1345                return false;
1346            }
1347
1348            // Convert event to destination-local coordinates.
1349            final MotionEvent dstEvent = MotionEvent.obtainNoHistory(srcEvent);
1350            src.toGlobalMotionEvent(dstEvent);
1351            dst.toLocalMotionEvent(dstEvent);
1352
1353            // Forward converted event to destination view, then recycle it.
1354            final boolean handled = dst.onForwardedEvent(dstEvent, mActivePointerId);
1355            dstEvent.recycle();
1356            return handled;
1357        }
1358
1359        private class DisallowIntercept implements Runnable {
1360            @Override
1361            public void run() {
1362                final ViewParent parent = mSrc.getParent();
1363                parent.requestDisallowInterceptTouchEvent(true);
1364            }
1365        }
1366    }
1367
1368    /**
1369     * <p>Wrapper class for a ListView. This wrapper can hijack the focus to
1370     * make sure the list uses the appropriate drawables and states when
1371     * displayed on screen within a drop down. The focus is never actually
1372     * passed to the drop down in this mode; the list only looks focused.</p>
1373     */
1374    private static class DropDownListView extends ListView {
1375        /** Duration in milliseconds of the drag-to-open click animation. */
1376        private static final long CLICK_ANIM_DURATION = 150;
1377
1378        /** Target alpha value for drag-to-open click animation. */
1379        private static final int CLICK_ANIM_ALPHA = 0x80;
1380
1381        /** Wrapper around Drawable's <code>alpha</code> property. */
1382        private static final IntProperty<Drawable> DRAWABLE_ALPHA =
1383                new IntProperty<Drawable>("alpha") {
1384                    @Override
1385                    public void setValue(Drawable object, int value) {
1386                        object.setAlpha(value);
1387                    }
1388
1389                    @Override
1390                    public Integer get(Drawable object) {
1391                        return object.getAlpha();
1392                    }
1393                };
1394
1395        /*
1396         * WARNING: This is a workaround for a touch mode issue.
1397         *
1398         * Touch mode is propagated lazily to windows. This causes problems in
1399         * the following scenario:
1400         * - Type something in the AutoCompleteTextView and get some results
1401         * - Move down with the d-pad to select an item in the list
1402         * - Move up with the d-pad until the selection disappears
1403         * - Type more text in the AutoCompleteTextView *using the soft keyboard*
1404         *   and get new results; you are now in touch mode
1405         * - The selection comes back on the first item in the list, even though
1406         *   the list is supposed to be in touch mode
1407         *
1408         * Using the soft keyboard triggers the touch mode change but that change
1409         * is propagated to our window only after the first list layout, therefore
1410         * after the list attempts to resurrect the selection.
1411         *
1412         * The trick to work around this issue is to pretend the list is in touch
1413         * mode when we know that the selection should not appear, that is when
1414         * we know the user moved the selection away from the list.
1415         *
1416         * This boolean is set to true whenever we explicitly hide the list's
1417         * selection and reset to false whenever we know the user moved the
1418         * selection back to the list.
1419         *
1420         * When this boolean is true, isInTouchMode() returns true, otherwise it
1421         * returns super.isInTouchMode().
1422         */
1423        private boolean mListSelectionHidden;
1424
1425        /**
1426         * True if this wrapper should fake focus.
1427         */
1428        private boolean mHijackFocus;
1429
1430        /** Whether to force drawing of the pressed state selector. */
1431        private boolean mDrawsInPressedState;
1432
1433        /** Current drag-to-open click animation, if any. */
1434        private Animator mClickAnimation;
1435
1436        /** Helper for drag-to-open auto scrolling. */
1437        private AbsListViewAutoScroller mScrollHelper;
1438
1439        /**
1440         * <p>Creates a new list view wrapper.</p>
1441         *
1442         * @param context this view's context
1443         */
1444        public DropDownListView(Context context, boolean hijackFocus) {
1445            super(context, null, com.android.internal.R.attr.dropDownListViewStyle);
1446            mHijackFocus = hijackFocus;
1447            // TODO: Add an API to control this
1448            setCacheColorHint(0); // Transparent, since the background drawable could be anything.
1449        }
1450
1451        /**
1452         * Handles forwarded events.
1453         *
1454         * @param activePointerId id of the pointer that activated forwarding
1455         * @return whether the event was handled
1456         */
1457        public boolean onForwardedEvent(MotionEvent event, int activePointerId) {
1458            boolean handledEvent = true;
1459            boolean clearPressedItem = false;
1460
1461            final int actionMasked = event.getActionMasked();
1462            switch (actionMasked) {
1463                case MotionEvent.ACTION_CANCEL:
1464                    handledEvent = false;
1465                    break;
1466                case MotionEvent.ACTION_UP:
1467                    handledEvent = false;
1468                    // $FALL-THROUGH$
1469                case MotionEvent.ACTION_MOVE:
1470                    final int activeIndex = event.findPointerIndex(activePointerId);
1471                    if (activeIndex < 0) {
1472                        handledEvent = false;
1473                        break;
1474                    }
1475
1476                    final int x = (int) event.getX(activeIndex);
1477                    final int y = (int) event.getY(activeIndex);
1478                    final int position = pointToPosition(x, y);
1479                    if (position == INVALID_POSITION) {
1480                        clearPressedItem = true;
1481                        break;
1482                    }
1483
1484                    final View child = getChildAt(position - getFirstVisiblePosition());
1485                    setPressedItem(child, position);
1486                    handledEvent = true;
1487
1488                    if (actionMasked == MotionEvent.ACTION_UP) {
1489                        clickPressedItem(child, position);
1490                    }
1491                    break;
1492            }
1493
1494            // Failure to handle the event cancels forwarding.
1495            if (!handledEvent || clearPressedItem) {
1496                clearPressedItem();
1497            }
1498
1499            // Manage automatic scrolling.
1500            if (handledEvent) {
1501                if (mScrollHelper == null) {
1502                    mScrollHelper = new AbsListViewAutoScroller(this);
1503                }
1504                mScrollHelper.setEnabled(true);
1505                mScrollHelper.onTouch(this, event);
1506            } else if (mScrollHelper != null) {
1507                mScrollHelper.setEnabled(false);
1508            }
1509
1510            return handledEvent;
1511        }
1512
1513        /**
1514         * Starts an alpha animation on the selector. When the animation ends,
1515         * the list performs a click on the item.
1516         */
1517        private void clickPressedItem(final View child, final int position) {
1518            final long id = getItemIdAtPosition(position);
1519            final Animator anim = ObjectAnimator.ofInt(
1520                    mSelector, DRAWABLE_ALPHA, 0xFF, CLICK_ANIM_ALPHA, 0xFF);
1521            anim.setDuration(CLICK_ANIM_DURATION);
1522            anim.setInterpolator(new AccelerateDecelerateInterpolator());
1523            anim.addListener(new AnimatorListenerAdapter() {
1524                    @Override
1525                public void onAnimationEnd(Animator animation) {
1526                    performItemClick(child, position, id);
1527                }
1528            });
1529            anim.start();
1530
1531            if (mClickAnimation != null) {
1532                mClickAnimation.cancel();
1533            }
1534            mClickAnimation = anim;
1535        }
1536
1537        private void clearPressedItem() {
1538            mDrawsInPressedState = false;
1539            setPressed(false);
1540            updateSelectorState();
1541
1542            if (mClickAnimation != null) {
1543                mClickAnimation.cancel();
1544                mClickAnimation = null;
1545            }
1546        }
1547
1548        private void setPressedItem(View child, int position) {
1549            mDrawsInPressedState = true;
1550
1551            // Ordering is essential. First update the pressed state and layout
1552            // the children. This will ensure the selector actually gets drawn.
1553            setPressed(true);
1554            layoutChildren();
1555
1556            // Ensure that keyboard focus starts from the last touched position.
1557            setSelectedPositionInt(position);
1558            positionSelector(position, child);
1559
1560            // Refresh the drawable state to reflect the new pressed state,
1561            // which will also update the selector state.
1562            refreshDrawableState();
1563
1564            if (mClickAnimation != null) {
1565                mClickAnimation.cancel();
1566                mClickAnimation = null;
1567            }
1568        }
1569
1570        @Override
1571        boolean touchModeDrawsInPressedState() {
1572            return mDrawsInPressedState || super.touchModeDrawsInPressedState();
1573        }
1574
1575        /**
1576         * <p>Avoids jarring scrolling effect by ensuring that list elements
1577         * made of a text view fit on a single line.</p>
1578         *
1579         * @param position the item index in the list to get a view for
1580         * @return the view for the specified item
1581         */
1582        @Override
1583        View obtainView(int position, boolean[] isScrap) {
1584            View view = super.obtainView(position, isScrap);
1585
1586            if (view instanceof TextView) {
1587                ((TextView) view).setHorizontallyScrolling(true);
1588            }
1589
1590            return view;
1591        }
1592
1593        @Override
1594        public boolean isInTouchMode() {
1595            // WARNING: Please read the comment where mListSelectionHidden is declared
1596            return (mHijackFocus && mListSelectionHidden) || super.isInTouchMode();
1597        }
1598
1599        /**
1600         * <p>Returns the focus state in the drop down.</p>
1601         *
1602         * @return true always if hijacking focus
1603         */
1604        @Override
1605        public boolean hasWindowFocus() {
1606            return mHijackFocus || super.hasWindowFocus();
1607        }
1608
1609        /**
1610         * <p>Returns the focus state in the drop down.</p>
1611         *
1612         * @return true always if hijacking focus
1613         */
1614        @Override
1615        public boolean isFocused() {
1616            return mHijackFocus || super.isFocused();
1617        }
1618
1619        /**
1620         * <p>Returns the focus state in the drop down.</p>
1621         *
1622         * @return true always if hijacking focus
1623         */
1624        @Override
1625        public boolean hasFocus() {
1626            return mHijackFocus || super.hasFocus();
1627        }
1628    }
1629
1630    private class PopupDataSetObserver extends DataSetObserver {
1631        @Override
1632        public void onChanged() {
1633            if (isShowing()) {
1634                // Resize the popup to fit new content
1635                show();
1636            }
1637        }
1638
1639        @Override
1640        public void onInvalidated() {
1641            dismiss();
1642        }
1643    }
1644
1645    private class ListSelectorHider implements Runnable {
1646        public void run() {
1647            clearListSelection();
1648        }
1649    }
1650
1651    private class ResizePopupRunnable implements Runnable {
1652        public void run() {
1653            if (mDropDownList != null && mDropDownList.getCount() > mDropDownList.getChildCount() &&
1654                    mDropDownList.getChildCount() <= mListItemExpandMaximum) {
1655                mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
1656                show();
1657            }
1658        }
1659    }
1660
1661    private class PopupTouchInterceptor implements OnTouchListener {
1662        public boolean onTouch(View v, MotionEvent event) {
1663            final int action = event.getAction();
1664            final int x = (int) event.getX();
1665            final int y = (int) event.getY();
1666
1667            if (action == MotionEvent.ACTION_DOWN &&
1668                    mPopup != null && mPopup.isShowing() &&
1669                    (x >= 0 && x < mPopup.getWidth() && y >= 0 && y < mPopup.getHeight())) {
1670                mHandler.postDelayed(mResizePopupRunnable, EXPAND_LIST_TIMEOUT);
1671            } else if (action == MotionEvent.ACTION_UP) {
1672                mHandler.removeCallbacks(mResizePopupRunnable);
1673            }
1674            return false;
1675        }
1676    }
1677
1678    private class PopupScrollListener implements ListView.OnScrollListener {
1679        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
1680                int totalItemCount) {
1681
1682        }
1683
1684        public void onScrollStateChanged(AbsListView view, int scrollState) {
1685            if (scrollState == SCROLL_STATE_TOUCH_SCROLL &&
1686                    !isInputMethodNotNeeded() && mPopup.getContentView() != null) {
1687                mHandler.removeCallbacks(mResizePopupRunnable);
1688                mResizePopupRunnable.run();
1689            }
1690        }
1691    }
1692}
1693