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