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