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