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