1/* 2 * Copyright (C) 2014 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.widget; 18 19import android.content.Context; 20import android.content.res.ColorStateList; 21import android.content.res.Resources; 22import android.content.res.TypedArray; 23import android.database.DataSetObserver; 24import android.graphics.PorterDuff; 25import android.graphics.Rect; 26import android.graphics.drawable.Drawable; 27import android.os.Build; 28import android.support.annotation.DrawableRes; 29import android.support.annotation.Nullable; 30import android.support.v4.content.ContextCompat; 31import android.support.v4.view.TintableBackgroundView; 32import android.support.v4.view.ViewCompat; 33import android.support.v7.appcompat.R; 34import android.support.v7.view.ContextThemeWrapper; 35import android.support.v7.view.menu.ShowableListMenu; 36import android.util.AttributeSet; 37import android.util.Log; 38import android.view.MotionEvent; 39import android.view.View; 40import android.view.ViewGroup; 41import android.view.ViewTreeObserver; 42import android.widget.AdapterView; 43import android.widget.ArrayAdapter; 44import android.widget.ListAdapter; 45import android.widget.ListView; 46import android.widget.PopupWindow; 47import android.widget.Spinner; 48import android.widget.SpinnerAdapter; 49 50 51/** 52 * A {@link Spinner} which supports compatible features on older versions of the platform, 53 * including: 54 * <ul> 55 * <li>Dynamic tinting of the background via the background tint methods in 56 * {@link android.support.v4.view.ViewCompat}.</li> 57 * <li>Configuring the background tint using {@link R.attr#backgroundTint} and 58 * {@link R.attr#backgroundTintMode}.</li> 59 * <li>Setting the popup theme using {@link R.attr#popupTheme}.</li> 60 * </ul> 61 * 62 * <p>This will automatically be used when you use {@link Spinner} in your layouts. 63 * You should only need to manually use this class when writing custom views.</p> 64 */ 65public class AppCompatSpinner extends Spinner implements TintableBackgroundView { 66 67 private static final boolean IS_AT_LEAST_M = Build.VERSION.SDK_INT >= 23; 68 private static final boolean IS_AT_LEAST_JB = Build.VERSION.SDK_INT >= 16; 69 70 private static final int[] ATTRS_ANDROID_SPINNERMODE = {android.R.attr.spinnerMode}; 71 72 private static final int MAX_ITEMS_MEASURED = 15; 73 74 private static final String TAG = "AppCompatSpinner"; 75 76 private static final int MODE_DIALOG = 0; 77 private static final int MODE_DROPDOWN = 1; 78 private static final int MODE_THEME = -1; 79 80 private AppCompatDrawableManager mDrawableManager; 81 82 private AppCompatBackgroundHelper mBackgroundTintHelper; 83 84 /** Context used to inflate the popup window or dialog. */ 85 private Context mPopupContext; 86 87 /** Forwarding listener used to implement drag-to-open. */ 88 private ForwardingListener mForwardingListener; 89 90 /** Temporary holder for setAdapter() calls from the super constructor. */ 91 private SpinnerAdapter mTempAdapter; 92 93 private boolean mPopupSet; 94 95 private DropdownPopup mPopup; 96 97 private int mDropDownWidth; 98 99 private final Rect mTempRect = new Rect(); 100 101 /** 102 * Construct a new spinner with the given context's theme. 103 * 104 * @param context The Context the view is running in, through which it can 105 * access the current theme, resources, etc. 106 */ 107 public AppCompatSpinner(Context context) { 108 this(context, null); 109 } 110 111 /** 112 * Construct a new spinner with the given context's theme and the supplied 113 * mode of displaying choices. <code>mode</code> may be one of 114 * {@link #MODE_DIALOG} or {@link #MODE_DROPDOWN}. 115 * 116 * @param context The Context the view is running in, through which it can 117 * access the current theme, resources, etc. 118 * @param mode Constant describing how the user will select choices from the spinner. 119 * @see #MODE_DIALOG 120 * @see #MODE_DROPDOWN 121 */ 122 public AppCompatSpinner(Context context, int mode) { 123 this(context, null, R.attr.spinnerStyle, mode); 124 } 125 126 /** 127 * Construct a new spinner with the given context's theme and the supplied attribute set. 128 * 129 * @param context The Context the view is running in, through which it can 130 * access the current theme, resources, etc. 131 * @param attrs The attributes of the XML tag that is inflating the view. 132 */ 133 public AppCompatSpinner(Context context, AttributeSet attrs) { 134 this(context, attrs, R.attr.spinnerStyle); 135 } 136 137 /** 138 * Construct a new spinner with the given context's theme, the supplied attribute set, 139 * and default style attribute. 140 * 141 * @param context The Context the view is running in, through which it can 142 * access the current theme, resources, etc. 143 * @param attrs The attributes of the XML tag that is inflating the view. 144 * @param defStyleAttr An attribute in the current theme that contains a 145 * reference to a style resource that supplies default values for 146 * the view. Can be 0 to not look for defaults. 147 */ 148 public AppCompatSpinner(Context context, AttributeSet attrs, int defStyleAttr) { 149 this(context, attrs, defStyleAttr, MODE_THEME); 150 } 151 152 /** 153 * Construct a new spinner with the given context's theme, the supplied attribute set, 154 * and default style. <code>mode</code> may be one of {@link #MODE_DIALOG} or 155 * {@link #MODE_DROPDOWN} and determines how the user will select choices from the spinner. 156 * 157 * @param context The Context the view is running in, through which it can 158 * access the current theme, resources, etc. 159 * @param attrs The attributes of the XML tag that is inflating the view. 160 * @param defStyleAttr An attribute in the current theme that contains a 161 * reference to a style resource that supplies default values for 162 * the view. Can be 0 to not look for defaults. 163 * @param mode Constant describing how the user will select choices from the spinner. 164 * @see #MODE_DIALOG 165 * @see #MODE_DROPDOWN 166 */ 167 public AppCompatSpinner(Context context, AttributeSet attrs, int defStyleAttr, int mode) { 168 this(context, attrs, defStyleAttr, mode, null); 169 } 170 171 172 /** 173 * Constructs a new spinner with the given context's theme, the supplied 174 * attribute set, default styles, popup mode (one of {@link #MODE_DIALOG} 175 * or {@link #MODE_DROPDOWN}), and the context against which the popup 176 * should be inflated. 177 * 178 * @param context The context against which the view is inflated, which 179 * provides access to the current theme, resources, etc. 180 * @param attrs The attributes of the XML tag that is inflating the view. 181 * @param defStyleAttr An attribute in the current theme that contains a 182 * reference to a style resource that supplies default 183 * values for the view. Can be 0 to not look for 184 * defaults. 185 * @param mode Constant describing how the user will select choices from 186 * the spinner. 187 * @param popupTheme The theme against which the dialog or dropdown popup 188 * should be inflated. May be {@code null} to use the 189 * view theme. If set, this will override any value 190 * specified by 191 * {@link R.styleable#Spinner_popupTheme}. 192 * @see #MODE_DIALOG 193 * @see #MODE_DROPDOWN 194 */ 195 public AppCompatSpinner(Context context, AttributeSet attrs, int defStyleAttr, int mode, 196 Resources.Theme popupTheme) { 197 super(context, attrs, defStyleAttr); 198 199 TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs, 200 R.styleable.Spinner, defStyleAttr, 0); 201 202 mDrawableManager = AppCompatDrawableManager.get(); 203 mBackgroundTintHelper = new AppCompatBackgroundHelper(this, mDrawableManager); 204 205 if (popupTheme != null) { 206 mPopupContext = new ContextThemeWrapper(context, popupTheme); 207 } else { 208 final int popupThemeResId = a.getResourceId(R.styleable.Spinner_popupTheme, 0); 209 if (popupThemeResId != 0) { 210 mPopupContext = new ContextThemeWrapper(context, popupThemeResId); 211 } else { 212 // If we're running on a < M device, we'll use the current context and still handle 213 // any dropdown popup 214 mPopupContext = !IS_AT_LEAST_M ? context : null; 215 } 216 } 217 218 if (mPopupContext != null) { 219 if (mode == MODE_THEME) { 220 if (Build.VERSION.SDK_INT >= 11) { 221 // If we're running on API v11+ we will try and read android:spinnerMode 222 TypedArray aa = null; 223 try { 224 aa = context.obtainStyledAttributes(attrs, ATTRS_ANDROID_SPINNERMODE, 225 defStyleAttr, 0); 226 if (aa.hasValue(0)) { 227 mode = aa.getInt(0, MODE_DIALOG); 228 } 229 } catch (Exception e) { 230 Log.i(TAG, "Could not read android:spinnerMode", e); 231 } finally { 232 if (aa != null) { 233 aa.recycle(); 234 } 235 } 236 } else { 237 // Else, we use a default mode of dropdown 238 mode = MODE_DROPDOWN; 239 } 240 } 241 242 if (mode == MODE_DROPDOWN) { 243 final DropdownPopup popup = new DropdownPopup(mPopupContext, attrs, defStyleAttr); 244 final TintTypedArray pa = TintTypedArray.obtainStyledAttributes( 245 mPopupContext, attrs, R.styleable.Spinner, defStyleAttr, 0); 246 mDropDownWidth = pa.getLayoutDimension(R.styleable.Spinner_android_dropDownWidth, 247 LayoutParams.WRAP_CONTENT); 248 popup.setBackgroundDrawable( 249 pa.getDrawable(R.styleable.Spinner_android_popupBackground)); 250 popup.setPromptText(a.getString(R.styleable.Spinner_android_prompt)); 251 pa.recycle(); 252 253 mPopup = popup; 254 mForwardingListener = new ForwardingListener(this) { 255 @Override 256 public ShowableListMenu getPopup() { 257 return popup; 258 } 259 260 @Override 261 public boolean onForwardingStarted() { 262 if (!mPopup.isShowing()) { 263 mPopup.show(); 264 } 265 return true; 266 } 267 }; 268 } 269 } 270 271 final CharSequence[] entries = a.getTextArray(R.styleable.Spinner_android_entries); 272 if (entries != null) { 273 final ArrayAdapter<CharSequence> adapter = new ArrayAdapter<>( 274 context, android.R.layout.simple_spinner_item, entries); 275 adapter.setDropDownViewResource(R.layout.support_simple_spinner_dropdown_item); 276 setAdapter(adapter); 277 } 278 279 a.recycle(); 280 281 mPopupSet = true; 282 283 // Base constructors can call setAdapter before we initialize mPopup. 284 // Finish setting things up if this happened. 285 if (mTempAdapter != null) { 286 setAdapter(mTempAdapter); 287 mTempAdapter = null; 288 } 289 290 mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr); 291 } 292 293 /** 294 * @return the context used to inflate the Spinner's popup or dialog window 295 */ 296 public Context getPopupContext() { 297 if (mPopup != null) { 298 return mPopupContext; 299 } else if (IS_AT_LEAST_M) { 300 return super.getPopupContext(); 301 } 302 return null; 303 } 304 305 public void setPopupBackgroundDrawable(Drawable background) { 306 if (mPopup != null) { 307 mPopup.setBackgroundDrawable(background); 308 } else if (IS_AT_LEAST_JB) { 309 super.setPopupBackgroundDrawable(background); 310 } 311 } 312 313 public void setPopupBackgroundResource(@DrawableRes int resId) { 314 setPopupBackgroundDrawable(ContextCompat.getDrawable(getPopupContext(), resId)); 315 } 316 317 public Drawable getPopupBackground() { 318 if (mPopup != null) { 319 return mPopup.getBackground(); 320 } else if (IS_AT_LEAST_JB) { 321 return super.getPopupBackground(); 322 } 323 return null; 324 } 325 326 public void setDropDownVerticalOffset(int pixels) { 327 if (mPopup != null) { 328 mPopup.setVerticalOffset(pixels); 329 } else if (IS_AT_LEAST_JB) { 330 super.setDropDownVerticalOffset(pixels); 331 } 332 } 333 334 public int getDropDownVerticalOffset() { 335 if (mPopup != null) { 336 return mPopup.getVerticalOffset(); 337 } else if (IS_AT_LEAST_JB) { 338 return super.getDropDownVerticalOffset(); 339 } 340 return 0; 341 } 342 343 public void setDropDownHorizontalOffset(int pixels) { 344 if (mPopup != null) { 345 mPopup.setHorizontalOffset(pixels); 346 } else if (IS_AT_LEAST_JB) { 347 super.setDropDownHorizontalOffset(pixels); 348 } 349 } 350 351 /** 352 * Get the configured horizontal offset in pixels for the spinner's popup window of choices. 353 * Only valid in {@link #MODE_DROPDOWN}; other modes will return 0. 354 * 355 * @return Horizontal offset in pixels 356 */ 357 public int getDropDownHorizontalOffset() { 358 if (mPopup != null) { 359 return mPopup.getHorizontalOffset(); 360 } else if (IS_AT_LEAST_JB) { 361 return super.getDropDownHorizontalOffset(); 362 } 363 return 0; 364 } 365 366 public void setDropDownWidth(int pixels) { 367 if (mPopup != null) { 368 mDropDownWidth = pixels; 369 } else if (IS_AT_LEAST_JB) { 370 super.setDropDownWidth(pixels); 371 } 372 } 373 374 public int getDropDownWidth() { 375 if (mPopup != null) { 376 return mDropDownWidth; 377 } else if (IS_AT_LEAST_JB) { 378 return super.getDropDownWidth(); 379 } 380 return 0; 381 } 382 383 @Override 384 public void setAdapter(SpinnerAdapter adapter) { 385 // The super constructor may call setAdapter before we're prepared. 386 // Postpone doing anything until we've finished construction. 387 if (!mPopupSet) { 388 mTempAdapter = adapter; 389 return; 390 } 391 392 super.setAdapter(adapter); 393 394 if (mPopup != null) { 395 final Context popupContext = mPopupContext == null ? getContext() : mPopupContext; 396 mPopup.setAdapter(new DropDownAdapter(adapter, popupContext.getTheme())); 397 } 398 } 399 400 @Override 401 protected void onDetachedFromWindow() { 402 super.onDetachedFromWindow(); 403 404 if (mPopup != null && mPopup.isShowing()) { 405 mPopup.dismiss(); 406 } 407 } 408 409 @Override 410 public boolean onTouchEvent(MotionEvent event) { 411 if (mForwardingListener != null && mForwardingListener.onTouch(this, event)) { 412 return true; 413 } 414 return super.onTouchEvent(event); 415 } 416 417 @Override 418 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 419 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 420 421 if (mPopup != null && MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.AT_MOST) { 422 final int measuredWidth = getMeasuredWidth(); 423 setMeasuredDimension(Math.min(Math.max(measuredWidth, 424 compatMeasureContentWidth(getAdapter(), getBackground())), 425 MeasureSpec.getSize(widthMeasureSpec)), 426 getMeasuredHeight()); 427 } 428 } 429 430 @Override 431 public boolean performClick() { 432 if (mPopup != null) { 433 // If we have a popup, show it if needed, or just consume the click... 434 if (!mPopup.isShowing()) { 435 mPopup.show(); 436 } 437 return true; 438 } 439 440 // Else let the platform handle the click 441 return super.performClick(); 442 } 443 444 @Override 445 public void setPrompt(CharSequence prompt) { 446 if (mPopup != null) { 447 mPopup.setPromptText(prompt); 448 } else { 449 super.setPrompt(prompt); 450 } 451 } 452 453 @Override 454 public CharSequence getPrompt() { 455 return mPopup != null ? mPopup.getHintText() : super.getPrompt(); 456 } 457 458 @Override 459 public void setBackgroundResource(@DrawableRes int resId) { 460 super.setBackgroundResource(resId); 461 if (mBackgroundTintHelper != null) { 462 mBackgroundTintHelper.onSetBackgroundResource(resId); 463 } 464 } 465 466 @Override 467 public void setBackgroundDrawable(Drawable background) { 468 super.setBackgroundDrawable(background); 469 if (mBackgroundTintHelper != null) { 470 mBackgroundTintHelper.onSetBackgroundDrawable(background); 471 } 472 } 473 474 /** 475 * This should be accessed via 476 * {@link android.support.v4.view.ViewCompat#setBackgroundTintList(android.view.View, 477 * ColorStateList)} 478 * 479 * @hide 480 */ 481 @Override 482 public void setSupportBackgroundTintList(@Nullable ColorStateList tint) { 483 if (mBackgroundTintHelper != null) { 484 mBackgroundTintHelper.setSupportBackgroundTintList(tint); 485 } 486 } 487 488 /** 489 * This should be accessed via 490 * {@link android.support.v4.view.ViewCompat#getBackgroundTintList(android.view.View)} 491 * 492 * @hide 493 */ 494 @Override 495 @Nullable 496 public ColorStateList getSupportBackgroundTintList() { 497 return mBackgroundTintHelper != null 498 ? mBackgroundTintHelper.getSupportBackgroundTintList() : null; 499 } 500 501 /** 502 * This should be accessed via 503 * {@link android.support.v4.view.ViewCompat#setBackgroundTintMode(android.view.View, 504 * PorterDuff.Mode)} 505 * 506 * @hide 507 */ 508 @Override 509 public void setSupportBackgroundTintMode(@Nullable PorterDuff.Mode tintMode) { 510 if (mBackgroundTintHelper != null) { 511 mBackgroundTintHelper.setSupportBackgroundTintMode(tintMode); 512 } 513 } 514 515 /** 516 * This should be accessed via 517 * {@link android.support.v4.view.ViewCompat#getBackgroundTintMode(android.view.View)} 518 * 519 * @hide 520 */ 521 @Override 522 @Nullable 523 public PorterDuff.Mode getSupportBackgroundTintMode() { 524 return mBackgroundTintHelper != null 525 ? mBackgroundTintHelper.getSupportBackgroundTintMode() : null; 526 } 527 528 @Override 529 protected void drawableStateChanged() { 530 super.drawableStateChanged(); 531 if (mBackgroundTintHelper != null) { 532 mBackgroundTintHelper.applySupportBackgroundTint(); 533 } 534 } 535 536 private int compatMeasureContentWidth(SpinnerAdapter adapter, Drawable background) { 537 if (adapter == null) { 538 return 0; 539 } 540 541 int width = 0; 542 View itemView = null; 543 int itemType = 0; 544 final int widthMeasureSpec = 545 MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.UNSPECIFIED); 546 final int heightMeasureSpec = 547 MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.UNSPECIFIED); 548 549 // Make sure the number of items we'll measure is capped. If it's a huge data set 550 // with wildly varying sizes, oh well. 551 int start = Math.max(0, getSelectedItemPosition()); 552 final int end = Math.min(adapter.getCount(), start + MAX_ITEMS_MEASURED); 553 final int count = end - start; 554 start = Math.max(0, start - (MAX_ITEMS_MEASURED - count)); 555 for (int i = start; i < end; i++) { 556 final int positionType = adapter.getItemViewType(i); 557 if (positionType != itemType) { 558 itemType = positionType; 559 itemView = null; 560 } 561 itemView = adapter.getView(i, itemView, this); 562 if (itemView.getLayoutParams() == null) { 563 itemView.setLayoutParams(new LayoutParams( 564 LayoutParams.WRAP_CONTENT, 565 LayoutParams.WRAP_CONTENT)); 566 } 567 itemView.measure(widthMeasureSpec, heightMeasureSpec); 568 width = Math.max(width, itemView.getMeasuredWidth()); 569 } 570 571 // Add background padding to measured width 572 if (background != null) { 573 background.getPadding(mTempRect); 574 width += mTempRect.left + mTempRect.right; 575 } 576 577 return width; 578 } 579 580 /** 581 * <p>Wrapper class for an Adapter. Transforms the embedded Adapter instance 582 * into a ListAdapter.</p> 583 */ 584 private static class DropDownAdapter implements ListAdapter, SpinnerAdapter { 585 586 private SpinnerAdapter mAdapter; 587 588 private ListAdapter mListAdapter; 589 590 /** 591 * Creates a new ListAdapter wrapper for the specified adapter. 592 * 593 * @param adapter the SpinnerAdapter to transform into a ListAdapter 594 * @param dropDownTheme the theme against which to inflate drop-down 595 * views, may be {@null} to use default theme 596 */ 597 public DropDownAdapter(@Nullable SpinnerAdapter adapter, 598 @Nullable Resources.Theme dropDownTheme) { 599 mAdapter = adapter; 600 601 if (adapter instanceof ListAdapter) { 602 mListAdapter = (ListAdapter) adapter; 603 } 604 605 if (dropDownTheme != null) { 606 if (IS_AT_LEAST_M && adapter instanceof android.widget.ThemedSpinnerAdapter) { 607 final android.widget.ThemedSpinnerAdapter themedAdapter = 608 (android.widget.ThemedSpinnerAdapter) adapter; 609 if (themedAdapter.getDropDownViewTheme() != dropDownTheme) { 610 themedAdapter.setDropDownViewTheme(dropDownTheme); 611 } 612 } else if (adapter instanceof ThemedSpinnerAdapter) { 613 final ThemedSpinnerAdapter themedAdapter = (ThemedSpinnerAdapter) adapter; 614 if (themedAdapter.getDropDownViewTheme() == null) { 615 themedAdapter.setDropDownViewTheme(dropDownTheme); 616 } 617 } 618 } 619 } 620 621 public int getCount() { 622 return mAdapter == null ? 0 : mAdapter.getCount(); 623 } 624 625 public Object getItem(int position) { 626 return mAdapter == null ? null : mAdapter.getItem(position); 627 } 628 629 public long getItemId(int position) { 630 return mAdapter == null ? -1 : mAdapter.getItemId(position); 631 } 632 633 public View getView(int position, View convertView, ViewGroup parent) { 634 return getDropDownView(position, convertView, parent); 635 } 636 637 public View getDropDownView(int position, View convertView, ViewGroup parent) { 638 return (mAdapter == null) ? null 639 : mAdapter.getDropDownView(position, convertView, parent); 640 } 641 642 public boolean hasStableIds() { 643 return mAdapter != null && mAdapter.hasStableIds(); 644 } 645 646 public void registerDataSetObserver(DataSetObserver observer) { 647 if (mAdapter != null) { 648 mAdapter.registerDataSetObserver(observer); 649 } 650 } 651 652 public void unregisterDataSetObserver(DataSetObserver observer) { 653 if (mAdapter != null) { 654 mAdapter.unregisterDataSetObserver(observer); 655 } 656 } 657 658 /** 659 * If the wrapped SpinnerAdapter is also a ListAdapter, delegate this call. 660 * Otherwise, return true. 661 */ 662 public boolean areAllItemsEnabled() { 663 final ListAdapter adapter = mListAdapter; 664 if (adapter != null) { 665 return adapter.areAllItemsEnabled(); 666 } else { 667 return true; 668 } 669 } 670 671 /** 672 * If the wrapped SpinnerAdapter is also a ListAdapter, delegate this call. 673 * Otherwise, return true. 674 */ 675 public boolean isEnabled(int position) { 676 final ListAdapter adapter = mListAdapter; 677 if (adapter != null) { 678 return adapter.isEnabled(position); 679 } else { 680 return true; 681 } 682 } 683 684 public int getItemViewType(int position) { 685 return 0; 686 } 687 688 public int getViewTypeCount() { 689 return 1; 690 } 691 692 public boolean isEmpty() { 693 return getCount() == 0; 694 } 695 } 696 697 private class DropdownPopup extends ListPopupWindow { 698 private CharSequence mHintText; 699 private ListAdapter mAdapter; 700 private final Rect mVisibleRect = new Rect(); 701 702 public DropdownPopup(Context context, AttributeSet attrs, int defStyleAttr) { 703 super(context, attrs, defStyleAttr); 704 705 setAnchorView(AppCompatSpinner.this); 706 setModal(true); 707 setPromptPosition(POSITION_PROMPT_ABOVE); 708 709 setOnItemClickListener(new AdapterView.OnItemClickListener() { 710 @Override 711 public void onItemClick(AdapterView<?> parent, View v, int position, long id) { 712 AppCompatSpinner.this.setSelection(position); 713 if (getOnItemClickListener() != null) { 714 AppCompatSpinner.this 715 .performItemClick(v, position, mAdapter.getItemId(position)); 716 } 717 dismiss(); 718 } 719 }); 720 } 721 722 @Override 723 public void setAdapter(ListAdapter adapter) { 724 super.setAdapter(adapter); 725 mAdapter = adapter; 726 } 727 728 public CharSequence getHintText() { 729 return mHintText; 730 } 731 732 public void setPromptText(CharSequence hintText) { 733 // Hint text is ignored for dropdowns, but maintain it here. 734 mHintText = hintText; 735 } 736 737 void computeContentWidth() { 738 final Drawable background = getBackground(); 739 int hOffset = 0; 740 if (background != null) { 741 background.getPadding(mTempRect); 742 hOffset = ViewUtils.isLayoutRtl(AppCompatSpinner.this) ? mTempRect.right 743 : -mTempRect.left; 744 } else { 745 mTempRect.left = mTempRect.right = 0; 746 } 747 748 final int spinnerPaddingLeft = AppCompatSpinner.this.getPaddingLeft(); 749 final int spinnerPaddingRight = AppCompatSpinner.this.getPaddingRight(); 750 final int spinnerWidth = AppCompatSpinner.this.getWidth(); 751 if (mDropDownWidth == WRAP_CONTENT) { 752 int contentWidth = compatMeasureContentWidth( 753 (SpinnerAdapter) mAdapter, getBackground()); 754 final int contentWidthLimit = getContext().getResources() 755 .getDisplayMetrics().widthPixels - mTempRect.left - mTempRect.right; 756 if (contentWidth > contentWidthLimit) { 757 contentWidth = contentWidthLimit; 758 } 759 setContentWidth(Math.max( 760 contentWidth, spinnerWidth - spinnerPaddingLeft - spinnerPaddingRight)); 761 } else if (mDropDownWidth == MATCH_PARENT) { 762 setContentWidth(spinnerWidth - spinnerPaddingLeft - spinnerPaddingRight); 763 } else { 764 setContentWidth(mDropDownWidth); 765 } 766 if (ViewUtils.isLayoutRtl(AppCompatSpinner.this)) { 767 hOffset += spinnerWidth - spinnerPaddingRight - getWidth(); 768 } else { 769 hOffset += spinnerPaddingLeft; 770 } 771 setHorizontalOffset(hOffset); 772 } 773 774 public void show() { 775 final boolean wasShowing = isShowing(); 776 777 computeContentWidth(); 778 779 setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED); 780 super.show(); 781 final ListView listView = getListView(); 782 listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); 783 setSelection(AppCompatSpinner.this.getSelectedItemPosition()); 784 785 if (wasShowing) { 786 // Skip setting up the layout/dismiss listener below. If we were previously 787 // showing it will still stick around. 788 return; 789 } 790 791 // Make sure we hide if our anchor goes away. 792 // TODO: This might be appropriate to push all the way down to PopupWindow, 793 // but it may have other side effects to investigate first. (Text editing handles, etc.) 794 final ViewTreeObserver vto = getViewTreeObserver(); 795 if (vto != null) { 796 final ViewTreeObserver.OnGlobalLayoutListener layoutListener 797 = new ViewTreeObserver.OnGlobalLayoutListener() { 798 @Override 799 public void onGlobalLayout() { 800 if (!isVisibleToUser(AppCompatSpinner.this)) { 801 dismiss(); 802 } else { 803 computeContentWidth(); 804 805 // Use super.show here to update; we don't want to move the selected 806 // position or adjust other things that would be reset otherwise. 807 DropdownPopup.super.show(); 808 } 809 } 810 }; 811 vto.addOnGlobalLayoutListener(layoutListener); 812 setOnDismissListener(new PopupWindow.OnDismissListener() { 813 @Override 814 public void onDismiss() { 815 final ViewTreeObserver vto = getViewTreeObserver(); 816 if (vto != null) { 817 vto.removeGlobalOnLayoutListener(layoutListener); 818 } 819 } 820 }); 821 } 822 } 823 824 /** 825 * Simplified version of the the hidden View.isVisibleToUser() 826 */ 827 private boolean isVisibleToUser(View view) { 828 return ViewCompat.isAttachedToWindow(view) && view.getGlobalVisibleRect(mVisibleRect); 829 } 830 } 831} 832