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