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