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