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