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