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