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