Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2007 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.annotation.Widget;
     20 import android.app.AlertDialog;
     21 import android.content.Context;
     22 import android.content.DialogInterface;
     23 import android.content.DialogInterface.OnClickListener;
     24 import android.content.res.TypedArray;
     25 import android.database.DataSetObserver;
     26 import android.graphics.Rect;
     27 import android.graphics.drawable.Drawable;
     28 import android.os.Build;
     29 import android.os.Parcel;
     30 import android.os.Parcelable;
     31 import android.util.AttributeSet;
     32 import android.util.Log;
     33 import android.view.Gravity;
     34 import android.view.MotionEvent;
     35 import android.view.View;
     36 import android.view.ViewGroup;
     37 import android.view.ViewTreeObserver;
     38 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
     39 import android.view.accessibility.AccessibilityEvent;
     40 import android.view.accessibility.AccessibilityNodeInfo;
     41 import android.widget.ListPopupWindow.ForwardingListener;
     42 import android.widget.PopupWindow.OnDismissListener;
     43 
     44 
     45 /**
     46  * A view that displays one child at a time and lets the user pick among them.
     47  * The items in the Spinner come from the {@link Adapter} associated with
     48  * this view.
     49  *
     50  * <p>See the <a href="{@docRoot}guide/topics/ui/controls/spinner.html">Spinners</a> guide.</p>
     51  *
     52  * @attr ref android.R.styleable#Spinner_dropDownSelector
     53  * @attr ref android.R.styleable#Spinner_dropDownWidth
     54  * @attr ref android.R.styleable#Spinner_gravity
     55  * @attr ref android.R.styleable#Spinner_popupBackground
     56  * @attr ref android.R.styleable#Spinner_prompt
     57  * @attr ref android.R.styleable#Spinner_spinnerMode
     58  * @attr ref android.R.styleable#ListPopupWindow_dropDownVerticalOffset
     59  * @attr ref android.R.styleable#ListPopupWindow_dropDownHorizontalOffset
     60  */
     61 @Widget
     62 public class Spinner extends AbsSpinner implements OnClickListener {
     63     private static final String TAG = "Spinner";
     64 
     65     // Only measure this many items to get a decent max width.
     66     private static final int MAX_ITEMS_MEASURED = 15;
     67 
     68     /**
     69      * Use a dialog window for selecting spinner options.
     70      */
     71     public static final int MODE_DIALOG = 0;
     72 
     73     /**
     74      * Use a dropdown anchored to the Spinner for selecting spinner options.
     75      */
     76     public static final int MODE_DROPDOWN = 1;
     77 
     78     /**
     79      * Use the theme-supplied value to select the dropdown mode.
     80      */
     81     private static final int MODE_THEME = -1;
     82 
     83     /** Forwarding listener used to implement drag-to-open. */
     84     private ForwardingListener mForwardingListener;
     85 
     86     private SpinnerPopup mPopup;
     87     private DropDownAdapter mTempAdapter;
     88     int mDropDownWidth;
     89 
     90     private int mGravity;
     91     private boolean mDisableChildrenWhenDisabled;
     92 
     93     private Rect mTempRect = new Rect();
     94 
     95     /**
     96      * Construct a new spinner with the given context's theme.
     97      *
     98      * @param context The Context the view is running in, through which it can
     99      *        access the current theme, resources, etc.
    100      */
    101     public Spinner(Context context) {
    102         this(context, null);
    103     }
    104 
    105     /**
    106      * Construct a new spinner with the given context's theme and the supplied
    107      * mode of displaying choices. <code>mode</code> may be one of
    108      * {@link #MODE_DIALOG} or {@link #MODE_DROPDOWN}.
    109      *
    110      * @param context The Context the view is running in, through which it can
    111      *        access the current theme, resources, etc.
    112      * @param mode Constant describing how the user will select choices from the spinner.
    113      *
    114      * @see #MODE_DIALOG
    115      * @see #MODE_DROPDOWN
    116      */
    117     public Spinner(Context context, int mode) {
    118         this(context, null, com.android.internal.R.attr.spinnerStyle, mode);
    119     }
    120 
    121     /**
    122      * Construct a new spinner with the given context's theme and the supplied attribute set.
    123      *
    124      * @param context The Context the view is running in, through which it can
    125      *        access the current theme, resources, etc.
    126      * @param attrs The attributes of the XML tag that is inflating the view.
    127      */
    128     public Spinner(Context context, AttributeSet attrs) {
    129         this(context, attrs, com.android.internal.R.attr.spinnerStyle);
    130     }
    131 
    132     /**
    133      * Construct a new spinner with the given context's theme, the supplied attribute set,
    134      * and default style attribute.
    135      *
    136      * @param context The Context the view is running in, through which it can
    137      *        access the current theme, resources, etc.
    138      * @param attrs The attributes of the XML tag that is inflating the view.
    139      * @param defStyleAttr An attribute in the current theme that contains a
    140      *        reference to a style resource that supplies default values for
    141      *        the view. Can be 0 to not look for defaults.
    142      */
    143     public Spinner(Context context, AttributeSet attrs, int defStyleAttr) {
    144         this(context, attrs, defStyleAttr, 0, MODE_THEME);
    145     }
    146 
    147     /**
    148      * Construct a new spinner with the given context's theme, the supplied attribute set,
    149      * and default style. <code>mode</code> may be one of {@link #MODE_DIALOG} or
    150      * {@link #MODE_DROPDOWN} and determines how the user will select choices from the spinner.
    151      *
    152      * @param context The Context the view is running in, through which it can
    153      *        access the current theme, resources, etc.
    154      * @param attrs The attributes of the XML tag that is inflating the view.
    155      * @param defStyleAttr An attribute in the current theme that contains a
    156      *        reference to a style resource that supplies default values for
    157      *        the view. Can be 0 to not look for defaults.
    158      * @param mode Constant describing how the user will select choices from the spinner.
    159      *
    160      * @see #MODE_DIALOG
    161      * @see #MODE_DROPDOWN
    162      */
    163     public Spinner(Context context, AttributeSet attrs, int defStyleAttr, int mode) {
    164         this(context, attrs, defStyleAttr, 0, mode);
    165     }
    166 
    167     /**
    168      * Construct a new spinner with the given context's theme, the supplied attribute set,
    169      * and default style. <code>mode</code> may be one of {@link #MODE_DIALOG} or
    170      * {@link #MODE_DROPDOWN} and determines how the user will select choices from the spinner.
    171      *
    172      * @param context The Context the view is running in, through which it can
    173      *        access the current theme, resources, etc.
    174      * @param attrs The attributes of the XML tag that is inflating the view.
    175      * @param defStyleAttr An attribute in the current theme that contains a
    176      *        reference to a style resource that supplies default values for
    177      *        the view. Can be 0 to not look for defaults.
    178      * @param defStyleRes A resource identifier of a style resource that
    179      *        supplies default values for the view, used only if
    180      *        defStyleAttr is 0 or can not be found in the theme. Can be 0
    181      *        to not look for defaults.
    182      * @param mode Constant describing how the user will select choices from the spinner.
    183      *
    184      * @see #MODE_DIALOG
    185      * @see #MODE_DROPDOWN
    186      */
    187     public Spinner(
    188             Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes, int mode) {
    189         super(context, attrs, defStyleAttr, defStyleRes);
    190 
    191         final TypedArray a = context.obtainStyledAttributes(
    192                 attrs, com.android.internal.R.styleable.Spinner, defStyleAttr, defStyleRes);
    193 
    194         if (mode == MODE_THEME) {
    195             mode = a.getInt(com.android.internal.R.styleable.Spinner_spinnerMode, MODE_DIALOG);
    196         }
    197 
    198         switch (mode) {
    199         case MODE_DIALOG: {
    200             mPopup = new DialogPopup();
    201             break;
    202         }
    203 
    204         case MODE_DROPDOWN: {
    205             final DropdownPopup popup = new DropdownPopup(context, attrs, defStyleAttr, defStyleRes);
    206 
    207             mDropDownWidth = a.getLayoutDimension(
    208                     com.android.internal.R.styleable.Spinner_dropDownWidth,
    209                     ViewGroup.LayoutParams.WRAP_CONTENT);
    210             popup.setBackgroundDrawable(a.getDrawable(
    211                     com.android.internal.R.styleable.Spinner_popupBackground));
    212 
    213             mPopup = popup;
    214             mForwardingListener = new ForwardingListener(this) {
    215                 @Override
    216                 public ListPopupWindow getPopup() {
    217                     return popup;
    218                 }
    219 
    220                 @Override
    221                 public boolean onForwardingStarted() {
    222                     if (!mPopup.isShowing()) {
    223                         mPopup.show(getTextDirection(), getTextAlignment());
    224                     }
    225                     return true;
    226                 }
    227             };
    228             break;
    229         }
    230         }
    231 
    232         mGravity = a.getInt(com.android.internal.R.styleable.Spinner_gravity, Gravity.CENTER);
    233 
    234         mPopup.setPromptText(a.getString(com.android.internal.R.styleable.Spinner_prompt));
    235 
    236         mDisableChildrenWhenDisabled = a.getBoolean(
    237                 com.android.internal.R.styleable.Spinner_disableChildrenWhenDisabled, false);
    238 
    239         a.recycle();
    240 
    241         // Base constructor can call setAdapter before we initialize mPopup.
    242         // Finish setting things up if this happened.
    243         if (mTempAdapter != null) {
    244             mPopup.setAdapter(mTempAdapter);
    245             mTempAdapter = null;
    246         }
    247     }
    248 
    249     /**
    250      * Set the background drawable for the spinner's popup window of choices.
    251      * Only valid in {@link #MODE_DROPDOWN}; this method is a no-op in other modes.
    252      *
    253      * @param background Background drawable
    254      *
    255      * @attr ref android.R.styleable#Spinner_popupBackground
    256      */
    257     public void setPopupBackgroundDrawable(Drawable background) {
    258         if (!(mPopup instanceof DropdownPopup)) {
    259             Log.e(TAG, "setPopupBackgroundDrawable: incompatible spinner mode; ignoring...");
    260             return;
    261         }
    262         ((DropdownPopup) mPopup).setBackgroundDrawable(background);
    263     }
    264 
    265     /**
    266      * Set the background drawable for the spinner's popup window of choices.
    267      * Only valid in {@link #MODE_DROPDOWN}; this method is a no-op in other modes.
    268      *
    269      * @param resId Resource ID of a background drawable
    270      *
    271      * @attr ref android.R.styleable#Spinner_popupBackground
    272      */
    273     public void setPopupBackgroundResource(int resId) {
    274         setPopupBackgroundDrawable(getContext().getDrawable(resId));
    275     }
    276 
    277     /**
    278      * Get the background drawable for the spinner's popup window of choices.
    279      * Only valid in {@link #MODE_DROPDOWN}; other modes will return null.
    280      *
    281      * @return background Background drawable
    282      *
    283      * @attr ref android.R.styleable#Spinner_popupBackground
    284      */
    285     public Drawable getPopupBackground() {
    286         return mPopup.getBackground();
    287     }
    288 
    289     /**
    290      * Set a vertical offset in pixels for the spinner's popup window of choices.
    291      * Only valid in {@link #MODE_DROPDOWN}; this method is a no-op in other modes.
    292      *
    293      * @param pixels Vertical offset in pixels
    294      *
    295      * @attr ref android.R.styleable#ListPopupWindow_dropDownVerticalOffset
    296      */
    297     public void setDropDownVerticalOffset(int pixels) {
    298         mPopup.setVerticalOffset(pixels);
    299     }
    300 
    301     /**
    302      * Get the configured vertical offset in pixels for the spinner's popup window of choices.
    303      * Only valid in {@link #MODE_DROPDOWN}; other modes will return 0.
    304      *
    305      * @return Vertical offset in pixels
    306      *
    307      * @attr ref android.R.styleable#ListPopupWindow_dropDownVerticalOffset
    308      */
    309     public int getDropDownVerticalOffset() {
    310         return mPopup.getVerticalOffset();
    311     }
    312 
    313     /**
    314      * Set a horizontal offset in pixels for the spinner's popup window of choices.
    315      * Only valid in {@link #MODE_DROPDOWN}; this method is a no-op in other modes.
    316      *
    317      * @param pixels Horizontal offset in pixels
    318      *
    319      * @attr ref android.R.styleable#ListPopupWindow_dropDownHorizontalOffset
    320      */
    321     public void setDropDownHorizontalOffset(int pixels) {
    322         mPopup.setHorizontalOffset(pixels);
    323     }
    324 
    325     /**
    326      * Get the configured horizontal offset in pixels for the spinner's popup window of choices.
    327      * Only valid in {@link #MODE_DROPDOWN}; other modes will return 0.
    328      *
    329      * @return Horizontal offset in pixels
    330      *
    331      * @attr ref android.R.styleable#ListPopupWindow_dropDownHorizontalOffset
    332      */
    333     public int getDropDownHorizontalOffset() {
    334         return mPopup.getHorizontalOffset();
    335     }
    336 
    337     /**
    338      * Set the width of the spinner's popup window of choices in pixels. This value
    339      * may also be set to {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT}
    340      * to match the width of the Spinner itself, or
    341      * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} to wrap to the measured size
    342      * of contained dropdown list items.
    343      *
    344      * <p>Only valid in {@link #MODE_DROPDOWN}; this method is a no-op in other modes.</p>
    345      *
    346      * @param pixels Width in pixels, WRAP_CONTENT, or MATCH_PARENT
    347      *
    348      * @attr ref android.R.styleable#Spinner_dropDownWidth
    349      */
    350     public void setDropDownWidth(int pixels) {
    351         if (!(mPopup instanceof DropdownPopup)) {
    352             Log.e(TAG, "Cannot set dropdown width for MODE_DIALOG, ignoring");
    353             return;
    354         }
    355         mDropDownWidth = pixels;
    356     }
    357 
    358     /**
    359      * Get the configured width of the spinner's popup window of choices in pixels.
    360      * The returned value may also be {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT}
    361      * meaning the popup window will match the width of the Spinner itself, or
    362      * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} to wrap to the measured size
    363      * of contained dropdown list items.
    364      *
    365      * @return Width in pixels, WRAP_CONTENT, or MATCH_PARENT
    366      *
    367      * @attr ref android.R.styleable#Spinner_dropDownWidth
    368      */
    369     public int getDropDownWidth() {
    370         return mDropDownWidth;
    371     }
    372 
    373     @Override
    374     public void setEnabled(boolean enabled) {
    375         super.setEnabled(enabled);
    376         if (mDisableChildrenWhenDisabled) {
    377             final int count = getChildCount();
    378             for (int i = 0; i < count; i++) {
    379                 getChildAt(i).setEnabled(enabled);
    380             }
    381         }
    382     }
    383 
    384     /**
    385      * Describes how the selected item view is positioned. Currently only the horizontal component
    386      * is used. The default is determined by the current theme.
    387      *
    388      * @param gravity See {@link android.view.Gravity}
    389      *
    390      * @attr ref android.R.styleable#Spinner_gravity
    391      */
    392     public void setGravity(int gravity) {
    393         if (mGravity != gravity) {
    394             if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == 0) {
    395                 gravity |= Gravity.START;
    396             }
    397             mGravity = gravity;
    398             requestLayout();
    399         }
    400     }
    401 
    402     /**
    403      * Describes how the selected item view is positioned. The default is determined by the
    404      * current theme.
    405      *
    406      * @return A {@link android.view.Gravity Gravity} value
    407      */
    408     public int getGravity() {
    409         return mGravity;
    410     }
    411 
    412     /**
    413      * Sets the Adapter used to provide the data which backs this Spinner.
    414      * <p>
    415      * Note that Spinner overrides {@link Adapter#getViewTypeCount()} on the
    416      * Adapter associated with this view. Calling
    417      * {@link Adapter#getItemViewType(int) getItemViewType(int)} on the object
    418      * returned from {@link #getAdapter()} will always return 0. Calling
    419      * {@link Adapter#getViewTypeCount() getViewTypeCount()} will always return
    420      * 1. On API {@link Build.VERSION_CODES#LOLLIPOP} and above, attempting to set an
    421      * adapter with more than one view type will throw an
    422      * {@link IllegalArgumentException}.
    423      *
    424      * @param adapter the adapter to set
    425      *
    426      * @see AbsSpinner#setAdapter(SpinnerAdapter)
    427      * @throws IllegalArgumentException if the adapter has more than one view
    428      *         type
    429      */
    430     @Override
    431     public void setAdapter(SpinnerAdapter adapter) {
    432         super.setAdapter(adapter);
    433 
    434         mRecycler.clear();
    435 
    436         final int targetSdkVersion = mContext.getApplicationInfo().targetSdkVersion;
    437         if (targetSdkVersion >= Build.VERSION_CODES.LOLLIPOP
    438                 && adapter != null && adapter.getViewTypeCount() != 1) {
    439             throw new IllegalArgumentException("Spinner adapter view type count must be 1");
    440         }
    441 
    442         if (mPopup != null) {
    443             mPopup.setAdapter(new DropDownAdapter(adapter));
    444         } else {
    445             mTempAdapter = new DropDownAdapter(adapter);
    446         }
    447     }
    448 
    449     @Override
    450     public int getBaseline() {
    451         View child = null;
    452 
    453         if (getChildCount() > 0) {
    454             child = getChildAt(0);
    455         } else if (mAdapter != null && mAdapter.getCount() > 0) {
    456             child = makeView(0, false);
    457             mRecycler.put(0, child);
    458         }
    459 
    460         if (child != null) {
    461             final int childBaseline = child.getBaseline();
    462             return childBaseline >= 0 ? child.getTop() + childBaseline : -1;
    463         } else {
    464             return -1;
    465         }
    466     }
    467 
    468     @Override
    469     protected void onDetachedFromWindow() {
    470         super.onDetachedFromWindow();
    471 
    472         if (mPopup != null && mPopup.isShowing()) {
    473             mPopup.dismiss();
    474         }
    475     }
    476 
    477     /**
    478      * <p>A spinner does not support item click events. Calling this method
    479      * will raise an exception.</p>
    480      * <p>Instead use {@link AdapterView#setOnItemSelectedListener}.
    481      *
    482      * @param l this listener will be ignored
    483      */
    484     @Override
    485     public void setOnItemClickListener(OnItemClickListener l) {
    486         throw new RuntimeException("setOnItemClickListener cannot be used with a spinner.");
    487     }
    488 
    489     /**
    490      * @hide internal use only
    491      */
    492     public void setOnItemClickListenerInt(OnItemClickListener l) {
    493         super.setOnItemClickListener(l);
    494     }
    495 
    496     @Override
    497     public boolean onTouchEvent(MotionEvent event) {
    498         if (mForwardingListener != null && mForwardingListener.onTouch(this, event)) {
    499             return true;
    500         }
    501 
    502         return super.onTouchEvent(event);
    503     }
    504 
    505     @Override
    506     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    507         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    508         if (mPopup != null && MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.AT_MOST) {
    509             final int measuredWidth = getMeasuredWidth();
    510             setMeasuredDimension(Math.min(Math.max(measuredWidth,
    511                     measureContentWidth(getAdapter(), getBackground())),
    512                     MeasureSpec.getSize(widthMeasureSpec)),
    513                     getMeasuredHeight());
    514         }
    515     }
    516 
    517     /**
    518      * @see android.view.View#onLayout(boolean,int,int,int,int)
    519      *
    520      * Creates and positions all views
    521      *
    522      */
    523     @Override
    524     protected void onLayout(boolean changed, int l, int t, int r, int b) {
    525         super.onLayout(changed, l, t, r, b);
    526         mInLayout = true;
    527         layout(0, false);
    528         mInLayout = false;
    529     }
    530 
    531     /**
    532      * Creates and positions all views for this Spinner.
    533      *
    534      * @param delta Change in the selected position. +1 means selection is moving to the right,
    535      * so views are scrolling to the left. -1 means selection is moving to the left.
    536      */
    537     @Override
    538     void layout(int delta, boolean animate) {
    539         int childrenLeft = mSpinnerPadding.left;
    540         int childrenWidth = mRight - mLeft - mSpinnerPadding.left - mSpinnerPadding.right;
    541 
    542         if (mDataChanged) {
    543             handleDataChanged();
    544         }
    545 
    546         // Handle the empty set by removing all views
    547         if (mItemCount == 0) {
    548             resetList();
    549             return;
    550         }
    551 
    552         if (mNextSelectedPosition >= 0) {
    553             setSelectedPositionInt(mNextSelectedPosition);
    554         }
    555 
    556         recycleAllViews();
    557 
    558         // Clear out old views
    559         removeAllViewsInLayout();
    560 
    561         // Make selected view and position it
    562         mFirstPosition = mSelectedPosition;
    563 
    564         if (mAdapter != null) {
    565             View sel = makeView(mSelectedPosition, true);
    566             int width = sel.getMeasuredWidth();
    567             int selectedOffset = childrenLeft;
    568             final int layoutDirection = getLayoutDirection();
    569             final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
    570             switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
    571                 case Gravity.CENTER_HORIZONTAL:
    572                     selectedOffset = childrenLeft + (childrenWidth / 2) - (width / 2);
    573                     break;
    574                 case Gravity.RIGHT:
    575                     selectedOffset = childrenLeft + childrenWidth - width;
    576                     break;
    577             }
    578             sel.offsetLeftAndRight(selectedOffset);
    579         }
    580 
    581         // Flush any cached views that did not get reused above
    582         mRecycler.clear();
    583 
    584         invalidate();
    585 
    586         checkSelectionChanged();
    587 
    588         mDataChanged = false;
    589         mNeedSync = false;
    590         setNextSelectedPositionInt(mSelectedPosition);
    591     }
    592 
    593     /**
    594      * Obtain a view, either by pulling an existing view from the recycler or
    595      * by getting a new one from the adapter. If we are animating, make sure
    596      * there is enough information in the view's layout parameters to animate
    597      * from the old to new positions.
    598      *
    599      * @param position Position in the spinner for the view to obtain
    600      * @param addChild true to add the child to the spinner, false to obtain and configure only.
    601      * @return A view for the given position
    602      */
    603     private View makeView(int position, boolean addChild) {
    604         View child;
    605 
    606         if (!mDataChanged) {
    607             child = mRecycler.get(position);
    608             if (child != null) {
    609                 // Position the view
    610                 setUpChild(child, addChild);
    611 
    612                 return child;
    613             }
    614         }
    615 
    616         // Nothing found in the recycler -- ask the adapter for a view
    617         child = mAdapter.getView(position, null, this);
    618 
    619         // Position the view
    620         setUpChild(child, addChild);
    621 
    622         return child;
    623     }
    624 
    625     /**
    626      * Helper for makeAndAddView to set the position of a view
    627      * and fill out its layout paramters.
    628      *
    629      * @param child The view to position
    630      * @param addChild true if the child should be added to the Spinner during setup
    631      */
    632     private void setUpChild(View child, boolean addChild) {
    633 
    634         // Respect layout params that are already in the view. Otherwise
    635         // make some up...
    636         ViewGroup.LayoutParams lp = child.getLayoutParams();
    637         if (lp == null) {
    638             lp = generateDefaultLayoutParams();
    639         }
    640 
    641         if (addChild) {
    642             addViewInLayout(child, 0, lp);
    643         }
    644 
    645         child.setSelected(hasFocus());
    646         if (mDisableChildrenWhenDisabled) {
    647             child.setEnabled(isEnabled());
    648         }
    649 
    650         // Get measure specs
    651         int childHeightSpec = ViewGroup.getChildMeasureSpec(mHeightMeasureSpec,
    652                 mSpinnerPadding.top + mSpinnerPadding.bottom, lp.height);
    653         int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
    654                 mSpinnerPadding.left + mSpinnerPadding.right, lp.width);
    655 
    656         // Measure child
    657         child.measure(childWidthSpec, childHeightSpec);
    658 
    659         int childLeft;
    660         int childRight;
    661 
    662         // Position vertically based on gravity setting
    663         int childTop = mSpinnerPadding.top
    664                 + ((getMeasuredHeight() - mSpinnerPadding.bottom -
    665                         mSpinnerPadding.top - child.getMeasuredHeight()) / 2);
    666         int childBottom = childTop + child.getMeasuredHeight();
    667 
    668         int width = child.getMeasuredWidth();
    669         childLeft = 0;
    670         childRight = childLeft + width;
    671 
    672         child.layout(childLeft, childTop, childRight, childBottom);
    673     }
    674 
    675     @Override
    676     public boolean performClick() {
    677         boolean handled = super.performClick();
    678 
    679         if (!handled) {
    680             handled = true;
    681 
    682             if (!mPopup.isShowing()) {
    683                 mPopup.show(getTextDirection(), getTextAlignment());
    684             }
    685         }
    686 
    687         return handled;
    688     }
    689 
    690     public void onClick(DialogInterface dialog, int which) {
    691         setSelection(which);
    692         dialog.dismiss();
    693     }
    694 
    695     @Override
    696     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
    697         super.onInitializeAccessibilityEvent(event);
    698         event.setClassName(Spinner.class.getName());
    699     }
    700 
    701     @Override
    702     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
    703         super.onInitializeAccessibilityNodeInfo(info);
    704         info.setClassName(Spinner.class.getName());
    705 
    706         if (mAdapter != null) {
    707             info.setCanOpenPopup(true);
    708         }
    709     }
    710 
    711     /**
    712      * Sets the prompt to display when the dialog is shown.
    713      * @param prompt the prompt to set
    714      */
    715     public void setPrompt(CharSequence prompt) {
    716         mPopup.setPromptText(prompt);
    717     }
    718 
    719     /**
    720      * Sets the prompt to display when the dialog is shown.
    721      * @param promptId the resource ID of the prompt to display when the dialog is shown
    722      */
    723     public void setPromptId(int promptId) {
    724         setPrompt(getContext().getText(promptId));
    725     }
    726 
    727     /**
    728      * @return The prompt to display when the dialog is shown
    729      */
    730     public CharSequence getPrompt() {
    731         return mPopup.getHintText();
    732     }
    733 
    734     int measureContentWidth(SpinnerAdapter adapter, Drawable background) {
    735         if (adapter == null) {
    736             return 0;
    737         }
    738 
    739         int width = 0;
    740         View itemView = null;
    741         int itemType = 0;
    742         final int widthMeasureSpec =
    743             MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
    744         final int heightMeasureSpec =
    745             MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
    746 
    747         // Make sure the number of items we'll measure is capped. If it's a huge data set
    748         // with wildly varying sizes, oh well.
    749         int start = Math.max(0, getSelectedItemPosition());
    750         final int end = Math.min(adapter.getCount(), start + MAX_ITEMS_MEASURED);
    751         final int count = end - start;
    752         start = Math.max(0, start - (MAX_ITEMS_MEASURED - count));
    753         for (int i = start; i < end; i++) {
    754             final int positionType = adapter.getItemViewType(i);
    755             if (positionType != itemType) {
    756                 itemType = positionType;
    757                 itemView = null;
    758             }
    759             itemView = adapter.getView(i, itemView, this);
    760             if (itemView.getLayoutParams() == null) {
    761                 itemView.setLayoutParams(new ViewGroup.LayoutParams(
    762                         ViewGroup.LayoutParams.WRAP_CONTENT,
    763                         ViewGroup.LayoutParams.WRAP_CONTENT));
    764             }
    765             itemView.measure(widthMeasureSpec, heightMeasureSpec);
    766             width = Math.max(width, itemView.getMeasuredWidth());
    767         }
    768 
    769         // Add background padding to measured width
    770         if (background != null) {
    771             background.getPadding(mTempRect);
    772             width += mTempRect.left + mTempRect.right;
    773         }
    774 
    775         return width;
    776     }
    777 
    778     @Override
    779     public Parcelable onSaveInstanceState() {
    780         final SavedState ss = new SavedState(super.onSaveInstanceState());
    781         ss.showDropdown = mPopup != null && mPopup.isShowing();
    782         return ss;
    783     }
    784 
    785     @Override
    786     public void onRestoreInstanceState(Parcelable state) {
    787         SavedState ss = (SavedState) state;
    788 
    789         super.onRestoreInstanceState(ss.getSuperState());
    790 
    791         if (ss.showDropdown) {
    792             ViewTreeObserver vto = getViewTreeObserver();
    793             if (vto != null) {
    794                 final OnGlobalLayoutListener listener = new OnGlobalLayoutListener() {
    795                     @Override
    796                     public void onGlobalLayout() {
    797                         if (!mPopup.isShowing()) {
    798                             mPopup.show(getTextDirection(), getTextAlignment());
    799                         }
    800                         final ViewTreeObserver vto = getViewTreeObserver();
    801                         if (vto != null) {
    802                             vto.removeOnGlobalLayoutListener(this);
    803                         }
    804                     }
    805                 };
    806                 vto.addOnGlobalLayoutListener(listener);
    807             }
    808         }
    809     }
    810 
    811     static class SavedState extends AbsSpinner.SavedState {
    812         boolean showDropdown;
    813 
    814         SavedState(Parcelable superState) {
    815             super(superState);
    816         }
    817 
    818         private SavedState(Parcel in) {
    819             super(in);
    820             showDropdown = in.readByte() != 0;
    821         }
    822 
    823         @Override
    824         public void writeToParcel(Parcel out, int flags) {
    825             super.writeToParcel(out, flags);
    826             out.writeByte((byte) (showDropdown ? 1 : 0));
    827         }
    828 
    829         public static final Parcelable.Creator<SavedState> CREATOR =
    830                 new Parcelable.Creator<SavedState>() {
    831             public SavedState createFromParcel(Parcel in) {
    832                 return new SavedState(in);
    833             }
    834 
    835             public SavedState[] newArray(int size) {
    836                 return new SavedState[size];
    837             }
    838         };
    839     }
    840 
    841     /**
    842      * <p>Wrapper class for an Adapter. Transforms the embedded Adapter instance
    843      * into a ListAdapter.</p>
    844      */
    845     private static class DropDownAdapter implements ListAdapter, SpinnerAdapter {
    846         private SpinnerAdapter mAdapter;
    847         private ListAdapter mListAdapter;
    848 
    849         /**
    850          * <p>Creates a new ListAdapter wrapper for the specified adapter.</p>
    851          *
    852          * @param adapter the Adapter to transform into a ListAdapter
    853          */
    854         public DropDownAdapter(SpinnerAdapter adapter) {
    855             this.mAdapter = adapter;
    856             if (adapter instanceof ListAdapter) {
    857                 this.mListAdapter = (ListAdapter) adapter;
    858             }
    859         }
    860 
    861         public int getCount() {
    862             return mAdapter == null ? 0 : mAdapter.getCount();
    863         }
    864 
    865         public Object getItem(int position) {
    866             return mAdapter == null ? null : mAdapter.getItem(position);
    867         }
    868 
    869         public long getItemId(int position) {
    870             return mAdapter == null ? -1 : mAdapter.getItemId(position);
    871         }
    872 
    873         public View getView(int position, View convertView, ViewGroup parent) {
    874             return getDropDownView(position, convertView, parent);
    875         }
    876 
    877         public View getDropDownView(int position, View convertView, ViewGroup parent) {
    878             return (mAdapter == null) ? null : mAdapter.getDropDownView(position, convertView, parent);
    879         }
    880 
    881         public boolean hasStableIds() {
    882             return mAdapter != null && mAdapter.hasStableIds();
    883         }
    884 
    885         public void registerDataSetObserver(DataSetObserver observer) {
    886             if (mAdapter != null) {
    887                 mAdapter.registerDataSetObserver(observer);
    888             }
    889         }
    890 
    891         public void unregisterDataSetObserver(DataSetObserver observer) {
    892             if (mAdapter != null) {
    893                 mAdapter.unregisterDataSetObserver(observer);
    894             }
    895         }
    896 
    897         /**
    898          * If the wrapped SpinnerAdapter is also a ListAdapter, delegate this call.
    899          * Otherwise, return true.
    900          */
    901         public boolean areAllItemsEnabled() {
    902             final ListAdapter adapter = mListAdapter;
    903             if (adapter != null) {
    904                 return adapter.areAllItemsEnabled();
    905             } else {
    906                 return true;
    907             }
    908         }
    909 
    910         /**
    911          * If the wrapped SpinnerAdapter is also a ListAdapter, delegate this call.
    912          * Otherwise, return true.
    913          */
    914         public boolean isEnabled(int position) {
    915             final ListAdapter adapter = mListAdapter;
    916             if (adapter != null) {
    917                 return adapter.isEnabled(position);
    918             } else {
    919                 return true;
    920             }
    921         }
    922 
    923         public int getItemViewType(int position) {
    924             return 0;
    925         }
    926 
    927         public int getViewTypeCount() {
    928             return 1;
    929         }
    930 
    931         public boolean isEmpty() {
    932             return getCount() == 0;
    933         }
    934     }
    935 
    936     /**
    937      * Implements some sort of popup selection interface for selecting a spinner option.
    938      * Allows for different spinner modes.
    939      */
    940     private interface SpinnerPopup {
    941         public void setAdapter(ListAdapter adapter);
    942 
    943         /**
    944          * Show the popup
    945          */
    946         public void show(int textDirection, int textAlignment);
    947 
    948         /**
    949          * Dismiss the popup
    950          */
    951         public void dismiss();
    952 
    953         /**
    954          * @return true if the popup is showing, false otherwise.
    955          */
    956         public boolean isShowing();
    957 
    958         /**
    959          * Set hint text to be displayed to the user. This should provide
    960          * a description of the choice being made.
    961          * @param hintText Hint text to set.
    962          */
    963         public void setPromptText(CharSequence hintText);
    964         public CharSequence getHintText();
    965 
    966         public void setBackgroundDrawable(Drawable bg);
    967         public void setVerticalOffset(int px);
    968         public void setHorizontalOffset(int px);
    969         public Drawable getBackground();
    970         public int getVerticalOffset();
    971         public int getHorizontalOffset();
    972     }
    973 
    974     private class DialogPopup implements SpinnerPopup, DialogInterface.OnClickListener {
    975         private AlertDialog mPopup;
    976         private ListAdapter mListAdapter;
    977         private CharSequence mPrompt;
    978 
    979         public void dismiss() {
    980             if (mPopup != null) {
    981                 mPopup.dismiss();
    982                 mPopup = null;
    983             }
    984         }
    985 
    986         public boolean isShowing() {
    987             return mPopup != null ? mPopup.isShowing() : false;
    988         }
    989 
    990         public void setAdapter(ListAdapter adapter) {
    991             mListAdapter = adapter;
    992         }
    993 
    994         public void setPromptText(CharSequence hintText) {
    995             mPrompt = hintText;
    996         }
    997 
    998         public CharSequence getHintText() {
    999             return mPrompt;
   1000         }
   1001 
   1002         public void show(int textDirection, int textAlignment) {
   1003             if (mListAdapter == null) {
   1004                 return;
   1005             }
   1006             AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
   1007             if (mPrompt != null) {
   1008                 builder.setTitle(mPrompt);
   1009             }
   1010             mPopup = builder.setSingleChoiceItems(mListAdapter,
   1011                     getSelectedItemPosition(), this).create();
   1012             final ListView listView = mPopup.getListView();
   1013             listView.setTextDirection(textDirection);
   1014             listView.setTextAlignment(textAlignment);
   1015             mPopup.show();
   1016         }
   1017 
   1018         public void onClick(DialogInterface dialog, int which) {
   1019             setSelection(which);
   1020             if (mOnItemClickListener != null) {
   1021                 performItemClick(null, which, mListAdapter.getItemId(which));
   1022             }
   1023             dismiss();
   1024         }
   1025 
   1026         @Override
   1027         public void setBackgroundDrawable(Drawable bg) {
   1028             Log.e(TAG, "Cannot set popup background for MODE_DIALOG, ignoring");
   1029         }
   1030 
   1031         @Override
   1032         public void setVerticalOffset(int px) {
   1033             Log.e(TAG, "Cannot set vertical offset for MODE_DIALOG, ignoring");
   1034         }
   1035 
   1036         @Override
   1037         public void setHorizontalOffset(int px) {
   1038             Log.e(TAG, "Cannot set horizontal offset for MODE_DIALOG, ignoring");
   1039         }
   1040 
   1041         @Override
   1042         public Drawable getBackground() {
   1043             return null;
   1044         }
   1045 
   1046         @Override
   1047         public int getVerticalOffset() {
   1048             return 0;
   1049         }
   1050 
   1051         @Override
   1052         public int getHorizontalOffset() {
   1053             return 0;
   1054         }
   1055     }
   1056 
   1057     private class DropdownPopup extends ListPopupWindow implements SpinnerPopup {
   1058         private CharSequence mHintText;
   1059         private ListAdapter mAdapter;
   1060 
   1061         public DropdownPopup(
   1062                 Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
   1063             super(context, attrs, defStyleAttr, defStyleRes);
   1064 
   1065             setAnchorView(Spinner.this);
   1066             setModal(true);
   1067             setPromptPosition(POSITION_PROMPT_ABOVE);
   1068             setOnItemClickListener(new OnItemClickListener() {
   1069                 public void onItemClick(AdapterView parent, View v, int position, long id) {
   1070                     Spinner.this.setSelection(position);
   1071                     if (mOnItemClickListener != null) {
   1072                         Spinner.this.performItemClick(v, position, mAdapter.getItemId(position));
   1073                     }
   1074                     dismiss();
   1075                 }
   1076             });
   1077         }
   1078 
   1079         @Override
   1080         public void setAdapter(ListAdapter adapter) {
   1081             super.setAdapter(adapter);
   1082             mAdapter = adapter;
   1083         }
   1084 
   1085         public CharSequence getHintText() {
   1086             return mHintText;
   1087         }
   1088 
   1089         public void setPromptText(CharSequence hintText) {
   1090             // Hint text is ignored for dropdowns, but maintain it here.
   1091             mHintText = hintText;
   1092         }
   1093 
   1094         void computeContentWidth() {
   1095             final Drawable background = getBackground();
   1096             int hOffset = 0;
   1097             if (background != null) {
   1098                 background.getPadding(mTempRect);
   1099                 hOffset = isLayoutRtl() ? mTempRect.right : -mTempRect.left;
   1100             } else {
   1101                 mTempRect.left = mTempRect.right = 0;
   1102             }
   1103 
   1104             final int spinnerPaddingLeft = Spinner.this.getPaddingLeft();
   1105             final int spinnerPaddingRight = Spinner.this.getPaddingRight();
   1106             final int spinnerWidth = Spinner.this.getWidth();
   1107 
   1108             if (mDropDownWidth == WRAP_CONTENT) {
   1109                 int contentWidth =  measureContentWidth(
   1110                         (SpinnerAdapter) mAdapter, getBackground());
   1111                 final int contentWidthLimit = mContext.getResources()
   1112                         .getDisplayMetrics().widthPixels - mTempRect.left - mTempRect.right;
   1113                 if (contentWidth > contentWidthLimit) {
   1114                     contentWidth = contentWidthLimit;
   1115                 }
   1116                 setContentWidth(Math.max(
   1117                        contentWidth, spinnerWidth - spinnerPaddingLeft - spinnerPaddingRight));
   1118             } else if (mDropDownWidth == MATCH_PARENT) {
   1119                 setContentWidth(spinnerWidth - spinnerPaddingLeft - spinnerPaddingRight);
   1120             } else {
   1121                 setContentWidth(mDropDownWidth);
   1122             }
   1123 
   1124             if (isLayoutRtl()) {
   1125                 hOffset += spinnerWidth - spinnerPaddingRight - getWidth();
   1126             } else {
   1127                 hOffset += spinnerPaddingLeft;
   1128             }
   1129             setHorizontalOffset(hOffset);
   1130         }
   1131 
   1132         public void show(int textDirection, int textAlignment) {
   1133             final boolean wasShowing = isShowing();
   1134 
   1135             computeContentWidth();
   1136 
   1137             setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
   1138             super.show();
   1139             final ListView listView = getListView();
   1140             listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
   1141             listView.setTextDirection(textDirection);
   1142             listView.setTextAlignment(textAlignment);
   1143             setSelection(Spinner.this.getSelectedItemPosition());
   1144 
   1145             if (wasShowing) {
   1146                 // Skip setting up the layout/dismiss listener below. If we were previously
   1147                 // showing it will still stick around.
   1148                 return;
   1149             }
   1150 
   1151             // Make sure we hide if our anchor goes away.
   1152             // TODO: This might be appropriate to push all the way down to PopupWindow,
   1153             // but it may have other side effects to investigate first. (Text editing handles, etc.)
   1154             final ViewTreeObserver vto = getViewTreeObserver();
   1155             if (vto != null) {
   1156                 final OnGlobalLayoutListener layoutListener = new OnGlobalLayoutListener() {
   1157                     @Override
   1158                     public void onGlobalLayout() {
   1159                         if (!Spinner.this.isVisibleToUser()) {
   1160                             dismiss();
   1161                         } else {
   1162                             computeContentWidth();
   1163 
   1164                             // Use super.show here to update; we don't want to move the selected
   1165                             // position or adjust other things that would be reset otherwise.
   1166                             DropdownPopup.super.show();
   1167                         }
   1168                     }
   1169                 };
   1170                 vto.addOnGlobalLayoutListener(layoutListener);
   1171                 setOnDismissListener(new OnDismissListener() {
   1172                     @Override public void onDismiss() {
   1173                         final ViewTreeObserver vto = getViewTreeObserver();
   1174                         if (vto != null) {
   1175                             vto.removeOnGlobalLayoutListener(layoutListener);
   1176                         }
   1177                     }
   1178                 });
   1179             }
   1180         }
   1181     }
   1182 }
   1183