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