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