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