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