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