ListPopupWindow.java revision 55807c973bbe6c0694b0a9a0b4dbb8d6e682ba95
1/* 2 * Copyright (C) 2010 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.widget; 18 19import android.annotation.AttrRes; 20import android.annotation.NonNull; 21import android.annotation.Nullable; 22import android.annotation.StyleRes; 23import android.content.Context; 24import android.content.res.TypedArray; 25import android.database.DataSetObserver; 26import android.graphics.Rect; 27import android.graphics.drawable.Drawable; 28import android.os.Handler; 29import android.util.AttributeSet; 30import android.util.Log; 31import android.view.Gravity; 32import android.view.KeyEvent; 33import android.view.MotionEvent; 34import android.view.View; 35import android.view.View.MeasureSpec; 36import android.view.View.OnTouchListener; 37import android.view.ViewGroup; 38import android.view.ViewParent; 39import android.view.WindowManager; 40import android.widget.AdapterView.OnItemSelectedListener; 41 42import com.android.internal.R; 43import com.android.internal.view.menu.ShowableListMenu; 44 45/** 46 * A ListPopupWindow anchors itself to a host view and displays a 47 * list of choices. 48 * 49 * <p>ListPopupWindow contains a number of tricky behaviors surrounding 50 * positioning, scrolling parents to fit the dropdown, interacting 51 * sanely with the IME if present, and others. 52 * 53 * @see android.widget.AutoCompleteTextView 54 * @see android.widget.Spinner 55 */ 56public class ListPopupWindow implements ShowableListMenu { 57 private static final String TAG = "ListPopupWindow"; 58 private static final boolean DEBUG = false; 59 60 /** 61 * This value controls the length of time that the user 62 * must leave a pointer down without scrolling to expand 63 * the autocomplete dropdown list to cover the IME. 64 */ 65 private static final int EXPAND_LIST_TIMEOUT = 250; 66 67 private Context mContext; 68 private ListAdapter mAdapter; 69 private DropDownListView mDropDownList; 70 71 private int mDropDownHeight = ViewGroup.LayoutParams.WRAP_CONTENT; 72 private int mDropDownWidth = ViewGroup.LayoutParams.WRAP_CONTENT; 73 private int mDropDownHorizontalOffset; 74 private int mDropDownVerticalOffset; 75 private int mDropDownWindowLayoutType = WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL; 76 private boolean mDropDownVerticalOffsetSet; 77 private boolean mIsAnimatedFromAnchor = true; 78 private boolean mOverlapAnchor; 79 80 private int mDropDownGravity = Gravity.NO_GRAVITY; 81 82 private boolean mDropDownAlwaysVisible = false; 83 private boolean mForceIgnoreOutsideTouch = false; 84 int mListItemExpandMaximum = Integer.MAX_VALUE; 85 86 private View mPromptView; 87 private int mPromptPosition = POSITION_PROMPT_ABOVE; 88 89 private DataSetObserver mObserver; 90 91 private View mDropDownAnchorView; 92 93 private Drawable mDropDownListHighlight; 94 95 private AdapterView.OnItemClickListener mItemClickListener; 96 private AdapterView.OnItemSelectedListener mItemSelectedListener; 97 98 private final ResizePopupRunnable mResizePopupRunnable = new ResizePopupRunnable(); 99 private final PopupTouchInterceptor mTouchInterceptor = new PopupTouchInterceptor(); 100 private final PopupScrollListener mScrollListener = new PopupScrollListener(); 101 private final ListSelectorHider mHideSelector = new ListSelectorHider(); 102 private Runnable mShowDropDownRunnable; 103 104 private final Handler mHandler; 105 106 private final Rect mTempRect = new Rect(); 107 108 /** 109 * Optional anchor-relative bounds to be used as the transition epicenter. 110 * When {@code null}, the anchor bounds are used as the epicenter. 111 */ 112 private Rect mEpicenterBounds; 113 114 private boolean mModal; 115 116 PopupWindow mPopup; 117 118 /** 119 * The provided prompt view should appear above list content. 120 * 121 * @see #setPromptPosition(int) 122 * @see #getPromptPosition() 123 * @see #setPromptView(View) 124 */ 125 public static final int POSITION_PROMPT_ABOVE = 0; 126 127 /** 128 * The provided prompt view should appear below list content. 129 * 130 * @see #setPromptPosition(int) 131 * @see #getPromptPosition() 132 * @see #setPromptView(View) 133 */ 134 public static final int POSITION_PROMPT_BELOW = 1; 135 136 /** 137 * Alias for {@link ViewGroup.LayoutParams#MATCH_PARENT}. 138 * If used to specify a popup width, the popup will match the width of the anchor view. 139 * If used to specify a popup height, the popup will fill available space. 140 */ 141 public static final int MATCH_PARENT = ViewGroup.LayoutParams.MATCH_PARENT; 142 143 /** 144 * Alias for {@link ViewGroup.LayoutParams#WRAP_CONTENT}. 145 * If used to specify a popup width, the popup will use the width of its content. 146 */ 147 public static final int WRAP_CONTENT = ViewGroup.LayoutParams.WRAP_CONTENT; 148 149 /** 150 * Mode for {@link #setInputMethodMode(int)}: the requirements for the 151 * input method should be based on the focusability of the popup. That is 152 * if it is focusable than it needs to work with the input method, else 153 * it doesn't. 154 */ 155 public static final int INPUT_METHOD_FROM_FOCUSABLE = PopupWindow.INPUT_METHOD_FROM_FOCUSABLE; 156 157 /** 158 * Mode for {@link #setInputMethodMode(int)}: this popup always needs to 159 * work with an input method, regardless of whether it is focusable. This 160 * means that it will always be displayed so that the user can also operate 161 * the input method while it is shown. 162 */ 163 public static final int INPUT_METHOD_NEEDED = PopupWindow.INPUT_METHOD_NEEDED; 164 165 /** 166 * Mode for {@link #setInputMethodMode(int)}: this popup never needs to 167 * work with an input method, regardless of whether it is focusable. This 168 * means that it will always be displayed to use as much space on the 169 * screen as needed, regardless of whether this covers the input method. 170 */ 171 public static final int INPUT_METHOD_NOT_NEEDED = PopupWindow.INPUT_METHOD_NOT_NEEDED; 172 173 /** 174 * Create a new, empty popup window capable of displaying items from a ListAdapter. 175 * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}. 176 * 177 * @param context Context used for contained views. 178 */ 179 public ListPopupWindow(@NonNull Context context) { 180 this(context, null, com.android.internal.R.attr.listPopupWindowStyle, 0); 181 } 182 183 /** 184 * Create a new, empty popup window capable of displaying items from a ListAdapter. 185 * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}. 186 * 187 * @param context Context used for contained views. 188 * @param attrs Attributes from inflating parent views used to style the popup. 189 */ 190 public ListPopupWindow(@NonNull Context context, @Nullable AttributeSet attrs) { 191 this(context, attrs, com.android.internal.R.attr.listPopupWindowStyle, 0); 192 } 193 194 /** 195 * Create a new, empty popup window capable of displaying items from a ListAdapter. 196 * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}. 197 * 198 * @param context Context used for contained views. 199 * @param attrs Attributes from inflating parent views used to style the popup. 200 * @param defStyleAttr Default style attribute to use for popup content. 201 */ 202 public ListPopupWindow(@NonNull Context context, @Nullable AttributeSet attrs, 203 @AttrRes int defStyleAttr) { 204 this(context, attrs, defStyleAttr, 0); 205 } 206 207 /** 208 * Create a new, empty popup window capable of displaying items from a ListAdapter. 209 * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}. 210 * 211 * @param context Context used for contained views. 212 * @param attrs Attributes from inflating parent views used to style the popup. 213 * @param defStyleAttr Style attribute to read for default styling of popup content. 214 * @param defStyleRes Style resource ID to use for default styling of popup content. 215 */ 216 public ListPopupWindow(@NonNull Context context, @Nullable AttributeSet attrs, 217 @AttrRes int defStyleAttr, @StyleRes int defStyleRes) { 218 mContext = context; 219 mHandler = new Handler(context.getMainLooper()); 220 221 final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ListPopupWindow, 222 defStyleAttr, defStyleRes); 223 mDropDownHorizontalOffset = a.getDimensionPixelOffset( 224 R.styleable.ListPopupWindow_dropDownHorizontalOffset, 0); 225 mDropDownVerticalOffset = a.getDimensionPixelOffset( 226 R.styleable.ListPopupWindow_dropDownVerticalOffset, 0); 227 if (mDropDownVerticalOffset != 0) { 228 mDropDownVerticalOffsetSet = true; 229 } 230 a.recycle(); 231 232 mPopup = new PopupWindow(context, attrs, defStyleAttr, defStyleRes); 233 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); 234 } 235 236 /** 237 * Sets the adapter that provides the data and the views to represent the data 238 * in this popup window. 239 * 240 * @param adapter The adapter to use to create this window's content. 241 */ 242 public void setAdapter(@Nullable ListAdapter adapter) { 243 if (mObserver == null) { 244 mObserver = new PopupDataSetObserver(); 245 } else if (mAdapter != null) { 246 mAdapter.unregisterDataSetObserver(mObserver); 247 } 248 mAdapter = adapter; 249 if (mAdapter != null) { 250 adapter.registerDataSetObserver(mObserver); 251 } 252 253 if (mDropDownList != null) { 254 mDropDownList.setAdapter(mAdapter); 255 } 256 } 257 258 /** 259 * Set where the optional prompt view should appear. The default is 260 * {@link #POSITION_PROMPT_ABOVE}. 261 * 262 * @param position A position constant declaring where the prompt should be displayed. 263 * 264 * @see #POSITION_PROMPT_ABOVE 265 * @see #POSITION_PROMPT_BELOW 266 */ 267 public void setPromptPosition(int position) { 268 mPromptPosition = position; 269 } 270 271 /** 272 * @return Where the optional prompt view should appear. 273 * 274 * @see #POSITION_PROMPT_ABOVE 275 * @see #POSITION_PROMPT_BELOW 276 */ 277 public int getPromptPosition() { 278 return mPromptPosition; 279 } 280 281 /** 282 * Set whether this window should be modal when shown. 283 * 284 * <p>If a popup window is modal, it will receive all touch and key input. 285 * If the user touches outside the popup window's content area the popup window 286 * will be dismissed. 287 * 288 * @param modal {@code true} if the popup window should be modal, {@code false} otherwise. 289 */ 290 public void setModal(boolean modal) { 291 mModal = modal; 292 mPopup.setFocusable(modal); 293 } 294 295 /** 296 * Returns whether the popup window will be modal when shown. 297 * 298 * @return {@code true} if the popup window will be modal, {@code false} otherwise. 299 */ 300 public boolean isModal() { 301 return mModal; 302 } 303 304 /** 305 * Forces outside touches to be ignored. Normally if {@link #isDropDownAlwaysVisible()} is 306 * false, we allow outside touch to dismiss the dropdown. If this is set to true, then we 307 * ignore outside touch even when the drop down is not set to always visible. 308 * 309 * @hide Used only by AutoCompleteTextView to handle some internal special cases. 310 */ 311 public void setForceIgnoreOutsideTouch(boolean forceIgnoreOutsideTouch) { 312 mForceIgnoreOutsideTouch = forceIgnoreOutsideTouch; 313 } 314 315 /** 316 * Sets whether the drop-down should remain visible under certain conditions. 317 * 318 * The drop-down will occupy the entire screen below {@link #getAnchorView} regardless 319 * of the size or content of the list. {@link #getBackground()} will fill any space 320 * that is not used by the list. 321 * 322 * @param dropDownAlwaysVisible Whether to keep the drop-down visible. 323 * 324 * @hide Only used by AutoCompleteTextView under special conditions. 325 */ 326 public void setDropDownAlwaysVisible(boolean dropDownAlwaysVisible) { 327 mDropDownAlwaysVisible = dropDownAlwaysVisible; 328 } 329 330 /** 331 * @return Whether the drop-down is visible under special conditions. 332 * 333 * @hide Only used by AutoCompleteTextView under special conditions. 334 */ 335 public boolean isDropDownAlwaysVisible() { 336 return mDropDownAlwaysVisible; 337 } 338 339 /** 340 * Sets the operating mode for the soft input area. 341 * 342 * @param mode The desired mode, see 343 * {@link android.view.WindowManager.LayoutParams#softInputMode} 344 * for the full list 345 * 346 * @see android.view.WindowManager.LayoutParams#softInputMode 347 * @see #getSoftInputMode() 348 */ 349 public void setSoftInputMode(int mode) { 350 mPopup.setSoftInputMode(mode); 351 } 352 353 /** 354 * Returns the current value in {@link #setSoftInputMode(int)}. 355 * 356 * @see #setSoftInputMode(int) 357 * @see android.view.WindowManager.LayoutParams#softInputMode 358 */ 359 public int getSoftInputMode() { 360 return mPopup.getSoftInputMode(); 361 } 362 363 /** 364 * Sets a drawable to use as the list item selector. 365 * 366 * @param selector List selector drawable to use in the popup. 367 */ 368 public void setListSelector(Drawable selector) { 369 mDropDownListHighlight = selector; 370 } 371 372 /** 373 * @return The background drawable for the popup window. 374 */ 375 public @Nullable Drawable getBackground() { 376 return mPopup.getBackground(); 377 } 378 379 /** 380 * Sets a drawable to be the background for the popup window. 381 * 382 * @param d A drawable to set as the background. 383 */ 384 public void setBackgroundDrawable(@Nullable Drawable d) { 385 mPopup.setBackgroundDrawable(d); 386 } 387 388 /** 389 * Set an animation style to use when the popup window is shown or dismissed. 390 * 391 * @param animationStyle Animation style to use. 392 */ 393 public void setAnimationStyle(@StyleRes int animationStyle) { 394 mPopup.setAnimationStyle(animationStyle); 395 } 396 397 /** 398 * Returns the animation style that will be used when the popup window is 399 * shown or dismissed. 400 * 401 * @return Animation style that will be used. 402 */ 403 public @StyleRes int getAnimationStyle() { 404 return mPopup.getAnimationStyle(); 405 } 406 407 /** 408 * Returns the view that will be used to anchor this popup. 409 * 410 * @return The popup's anchor view 411 */ 412 public @Nullable View getAnchorView() { 413 return mDropDownAnchorView; 414 } 415 416 /** 417 * Sets the popup's anchor view. This popup will always be positioned relative to 418 * the anchor view when shown. 419 * 420 * @param anchor The view to use as an anchor. 421 */ 422 public void setAnchorView(@Nullable View anchor) { 423 mDropDownAnchorView = anchor; 424 } 425 426 /** 427 * @return The horizontal offset of the popup from its anchor in pixels. 428 */ 429 public int getHorizontalOffset() { 430 return mDropDownHorizontalOffset; 431 } 432 433 /** 434 * Set the horizontal offset of this popup from its anchor view in pixels. 435 * 436 * @param offset The horizontal offset of the popup from its anchor. 437 */ 438 public void setHorizontalOffset(int offset) { 439 mDropDownHorizontalOffset = offset; 440 } 441 442 /** 443 * @return The vertical offset of the popup from its anchor in pixels. 444 */ 445 public int getVerticalOffset() { 446 if (!mDropDownVerticalOffsetSet) { 447 return 0; 448 } 449 return mDropDownVerticalOffset; 450 } 451 452 /** 453 * Set the vertical offset of this popup from its anchor view in pixels. 454 * 455 * @param offset The vertical offset of the popup from its anchor. 456 */ 457 public void setVerticalOffset(int offset) { 458 mDropDownVerticalOffset = offset; 459 mDropDownVerticalOffsetSet = true; 460 } 461 462 /** 463 * Specifies the anchor-relative bounds of the popup's transition 464 * epicenter. 465 * 466 * @param bounds anchor-relative bounds 467 * @hide 468 */ 469 public void setEpicenterBounds(Rect bounds) { 470 mEpicenterBounds = bounds; 471 } 472 473 /** 474 * Set the gravity of the dropdown list. This is commonly used to 475 * set gravity to START or END for alignment with the anchor. 476 * 477 * @param gravity Gravity value to use 478 */ 479 public void setDropDownGravity(int gravity) { 480 mDropDownGravity = gravity; 481 } 482 483 /** 484 * @return The width of the popup window in pixels. 485 */ 486 public int getWidth() { 487 return mDropDownWidth; 488 } 489 490 /** 491 * Sets the width of the popup window in pixels. Can also be {@link #MATCH_PARENT} 492 * or {@link #WRAP_CONTENT}. 493 * 494 * @param width Width of the popup window. 495 */ 496 public void setWidth(int width) { 497 mDropDownWidth = width; 498 } 499 500 /** 501 * Sets the width of the popup window by the size of its content. The final width may be 502 * larger to accommodate styled window dressing. 503 * 504 * @param width Desired width of content in pixels. 505 */ 506 public void setContentWidth(int width) { 507 Drawable popupBackground = mPopup.getBackground(); 508 if (popupBackground != null) { 509 popupBackground.getPadding(mTempRect); 510 mDropDownWidth = mTempRect.left + mTempRect.right + width; 511 } else { 512 setWidth(width); 513 } 514 } 515 516 /** 517 * @return The height of the popup window in pixels. 518 */ 519 public int getHeight() { 520 return mDropDownHeight; 521 } 522 523 /** 524 * Sets the height of the popup window in pixels. Can also be {@link #MATCH_PARENT}. 525 * 526 * @param height Height of the popup window. 527 */ 528 public void setHeight(int height) { 529 mDropDownHeight = height; 530 } 531 532 /** 533 * Set the layout type for this popup window. 534 * <p> 535 * See {@link WindowManager.LayoutParams#type} for possible values. 536 * 537 * @param layoutType Layout type for this window. 538 * 539 * @see WindowManager.LayoutParams#type 540 */ 541 public void setWindowLayoutType(int layoutType) { 542 mDropDownWindowLayoutType = layoutType; 543 } 544 545 /** 546 * Sets a listener to receive events when a list item is clicked. 547 * 548 * @param clickListener Listener to register 549 * 550 * @see ListView#setOnItemClickListener(android.widget.AdapterView.OnItemClickListener) 551 */ 552 public void setOnItemClickListener(@Nullable AdapterView.OnItemClickListener clickListener) { 553 mItemClickListener = clickListener; 554 } 555 556 /** 557 * Sets a listener to receive events when a list item is selected. 558 * 559 * @param selectedListener Listener to register. 560 * 561 * @see ListView#setOnItemSelectedListener(OnItemSelectedListener) 562 */ 563 public void setOnItemSelectedListener(@Nullable OnItemSelectedListener selectedListener) { 564 mItemSelectedListener = selectedListener; 565 } 566 567 /** 568 * Set a view to act as a user prompt for this popup window. Where the prompt view will appear 569 * is controlled by {@link #setPromptPosition(int)}. 570 * 571 * @param prompt View to use as an informational prompt. 572 */ 573 public void setPromptView(@Nullable View prompt) { 574 boolean showing = isShowing(); 575 if (showing) { 576 removePromptView(); 577 } 578 mPromptView = prompt; 579 if (showing) { 580 show(); 581 } 582 } 583 584 /** 585 * Post a {@link #show()} call to the UI thread. 586 */ 587 public void postShow() { 588 mHandler.post(mShowDropDownRunnable); 589 } 590 591 /** 592 * Show the popup list. If the list is already showing, this method 593 * will recalculate the popup's size and position. 594 */ 595 @Override 596 public void show() { 597 int height = buildDropDown(); 598 599 final boolean noInputMethod = isInputMethodNotNeeded(); 600 mPopup.setAllowScrollingAnchorParent(!noInputMethod); 601 mPopup.setWindowLayoutType(mDropDownWindowLayoutType); 602 603 if (mPopup.isShowing()) { 604 if (!getAnchorView().isAttachedToWindow()) { 605 //Don't update position if the anchor view is detached from window. 606 return; 607 } 608 final int widthSpec; 609 if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) { 610 // The call to PopupWindow's update method below can accept -1 for any 611 // value you do not want to update. 612 widthSpec = -1; 613 } else if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) { 614 widthSpec = getAnchorView().getWidth(); 615 } else { 616 widthSpec = mDropDownWidth; 617 } 618 619 final int heightSpec; 620 if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { 621 // The call to PopupWindow's update method below can accept -1 for any 622 // value you do not want to update. 623 heightSpec = noInputMethod ? height : ViewGroup.LayoutParams.MATCH_PARENT; 624 if (noInputMethod) { 625 mPopup.setWidth(mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ? 626 ViewGroup.LayoutParams.MATCH_PARENT : 0); 627 mPopup.setHeight(0); 628 } else { 629 mPopup.setWidth(mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ? 630 ViewGroup.LayoutParams.MATCH_PARENT : 0); 631 mPopup.setHeight(ViewGroup.LayoutParams.MATCH_PARENT); 632 } 633 } else if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { 634 heightSpec = height; 635 } else { 636 heightSpec = mDropDownHeight; 637 } 638 639 mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible); 640 641 mPopup.update(getAnchorView(), mDropDownHorizontalOffset, 642 mDropDownVerticalOffset, (widthSpec < 0)? -1 : widthSpec, 643 (heightSpec < 0)? -1 : heightSpec); 644 } else { 645 final int widthSpec; 646 if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) { 647 widthSpec = ViewGroup.LayoutParams.MATCH_PARENT; 648 } else { 649 if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) { 650 widthSpec = getAnchorView().getWidth(); 651 } else { 652 widthSpec = mDropDownWidth; 653 } 654 } 655 656 final int heightSpec; 657 if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { 658 heightSpec = ViewGroup.LayoutParams.MATCH_PARENT; 659 } else { 660 if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { 661 heightSpec = height; 662 } else { 663 heightSpec = mDropDownHeight; 664 } 665 } 666 667 mPopup.setWidth(widthSpec); 668 mPopup.setHeight(heightSpec); 669 mPopup.setClipToScreenEnabled(true); 670 671 // use outside touchable to dismiss drop down when touching outside of it, so 672 // only set this if the dropdown is not always visible 673 mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible); 674 mPopup.setTouchInterceptor(mTouchInterceptor); 675 mPopup.setEpicenterBounds(mEpicenterBounds); 676 mPopup.setOverlapAnchor(mOverlapAnchor); 677 mPopup.showAsDropDown(getAnchorView(), mDropDownHorizontalOffset, 678 mDropDownVerticalOffset, mDropDownGravity); 679 mDropDownList.setSelection(ListView.INVALID_POSITION); 680 681 if (!mModal || mDropDownList.isInTouchMode()) { 682 clearListSelection(); 683 } 684 if (!mModal) { 685 mHandler.post(mHideSelector); 686 } 687 } 688 } 689 690 /** 691 * Dismiss the popup window. 692 */ 693 @Override 694 public void dismiss() { 695 mPopup.dismiss(); 696 removePromptView(); 697 mPopup.setContentView(null); 698 mDropDownList = null; 699 mHandler.removeCallbacks(mResizePopupRunnable); 700 } 701 702 /** 703 * Set a listener to receive a callback when the popup is dismissed. 704 * 705 * @param listener Listener that will be notified when the popup is dismissed. 706 */ 707 public void setOnDismissListener(@Nullable PopupWindow.OnDismissListener listener) { 708 mPopup.setOnDismissListener(listener); 709 } 710 711 private void removePromptView() { 712 if (mPromptView != null) { 713 final ViewParent parent = mPromptView.getParent(); 714 if (parent instanceof ViewGroup) { 715 final ViewGroup group = (ViewGroup) parent; 716 group.removeView(mPromptView); 717 } 718 } 719 } 720 721 /** 722 * Control how the popup operates with an input method: one of 723 * {@link #INPUT_METHOD_FROM_FOCUSABLE}, {@link #INPUT_METHOD_NEEDED}, 724 * or {@link #INPUT_METHOD_NOT_NEEDED}. 725 * 726 * <p>If the popup is showing, calling this method will take effect only 727 * the next time the popup is shown or through a manual call to the {@link #show()} 728 * method.</p> 729 * 730 * @see #getInputMethodMode() 731 * @see #show() 732 */ 733 public void setInputMethodMode(int mode) { 734 mPopup.setInputMethodMode(mode); 735 } 736 737 /** 738 * Return the current value in {@link #setInputMethodMode(int)}. 739 * 740 * @see #setInputMethodMode(int) 741 */ 742 public int getInputMethodMode() { 743 return mPopup.getInputMethodMode(); 744 } 745 746 /** 747 * Set the selected position of the list. 748 * Only valid when {@link #isShowing()} == {@code true}. 749 * 750 * @param position List position to set as selected. 751 */ 752 public void setSelection(int position) { 753 DropDownListView list = mDropDownList; 754 if (isShowing() && list != null) { 755 list.setListSelectionHidden(false); 756 list.setSelection(position); 757 if (list.getChoiceMode() != ListView.CHOICE_MODE_NONE) { 758 list.setItemChecked(position, true); 759 } 760 } 761 } 762 763 /** 764 * Clear any current list selection. 765 * Only valid when {@link #isShowing()} == {@code true}. 766 */ 767 public void clearListSelection() { 768 final DropDownListView list = mDropDownList; 769 if (list != null) { 770 // WARNING: Please read the comment where mListSelectionHidden is declared 771 list.setListSelectionHidden(true); 772 list.hideSelector(); 773 list.requestLayout(); 774 } 775 } 776 777 /** 778 * @return {@code true} if the popup is currently showing, {@code false} otherwise. 779 */ 780 @Override 781 public boolean isShowing() { 782 return mPopup.isShowing(); 783 } 784 785 /** 786 * @return {@code true} if this popup is configured to assume the user does not need 787 * to interact with the IME while it is showing, {@code false} otherwise. 788 */ 789 public boolean isInputMethodNotNeeded() { 790 return mPopup.getInputMethodMode() == INPUT_METHOD_NOT_NEEDED; 791 } 792 793 /** 794 * Perform an item click operation on the specified list adapter position. 795 * 796 * @param position Adapter position for performing the click 797 * @return true if the click action could be performed, false if not. 798 * (e.g. if the popup was not showing, this method would return false.) 799 */ 800 public boolean performItemClick(int position) { 801 if (isShowing()) { 802 if (mItemClickListener != null) { 803 final DropDownListView list = mDropDownList; 804 final View child = list.getChildAt(position - list.getFirstVisiblePosition()); 805 final ListAdapter adapter = list.getAdapter(); 806 mItemClickListener.onItemClick(list, child, position, adapter.getItemId(position)); 807 } 808 return true; 809 } 810 return false; 811 } 812 813 /** 814 * @return The currently selected item or null if the popup is not showing. 815 */ 816 public @Nullable Object getSelectedItem() { 817 if (!isShowing()) { 818 return null; 819 } 820 return mDropDownList.getSelectedItem(); 821 } 822 823 /** 824 * @return The position of the currently selected item or {@link ListView#INVALID_POSITION} 825 * if {@link #isShowing()} == {@code false}. 826 * 827 * @see ListView#getSelectedItemPosition() 828 */ 829 public int getSelectedItemPosition() { 830 if (!isShowing()) { 831 return ListView.INVALID_POSITION; 832 } 833 return mDropDownList.getSelectedItemPosition(); 834 } 835 836 /** 837 * @return The ID of the currently selected item or {@link ListView#INVALID_ROW_ID} 838 * if {@link #isShowing()} == {@code false}. 839 * 840 * @see ListView#getSelectedItemId() 841 */ 842 public long getSelectedItemId() { 843 if (!isShowing()) { 844 return ListView.INVALID_ROW_ID; 845 } 846 return mDropDownList.getSelectedItemId(); 847 } 848 849 /** 850 * @return The View for the currently selected item or null if 851 * {@link #isShowing()} == {@code false}. 852 * 853 * @see ListView#getSelectedView() 854 */ 855 public @Nullable View getSelectedView() { 856 if (!isShowing()) { 857 return null; 858 } 859 return mDropDownList.getSelectedView(); 860 } 861 862 /** 863 * @return The {@link ListView} displayed within the popup window. 864 * Only valid when {@link #isShowing()} == {@code true}. 865 */ 866 @Override 867 public @Nullable ListView getListView() { 868 return mDropDownList; 869 } 870 871 @NonNull DropDownListView createDropDownListView(Context context, boolean hijackFocus) { 872 return new DropDownListView(context, hijackFocus); 873 } 874 875 /** 876 * The maximum number of list items that can be visible and still have 877 * the list expand when touched. 878 * 879 * @param max Max number of items that can be visible and still allow the list to expand. 880 */ 881 void setListItemExpandMax(int max) { 882 mListItemExpandMaximum = max; 883 } 884 885 /** 886 * Filter key down events. By forwarding key down events to this function, 887 * views using non-modal ListPopupWindow can have it handle key selection of items. 888 * 889 * @param keyCode keyCode param passed to the host view's onKeyDown 890 * @param event event param passed to the host view's onKeyDown 891 * @return true if the event was handled, false if it was ignored. 892 * 893 * @see #setModal(boolean) 894 * @see #onKeyUp(int, KeyEvent) 895 */ 896 public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) { 897 // when the drop down is shown, we drive it directly 898 if (isShowing()) { 899 // the key events are forwarded to the list in the drop down view 900 // note that ListView handles space but we don't want that to happen 901 // also if selection is not currently in the drop down, then don't 902 // let center or enter presses go there since that would cause it 903 // to select one of its items 904 if (keyCode != KeyEvent.KEYCODE_SPACE 905 && (mDropDownList.getSelectedItemPosition() >= 0 906 || !KeyEvent.isConfirmKey(keyCode))) { 907 int curIndex = mDropDownList.getSelectedItemPosition(); 908 boolean consumed; 909 910 final boolean below = !mPopup.isAboveAnchor(); 911 912 final ListAdapter adapter = mAdapter; 913 914 boolean allEnabled; 915 int firstItem = Integer.MAX_VALUE; 916 int lastItem = Integer.MIN_VALUE; 917 918 if (adapter != null) { 919 allEnabled = adapter.areAllItemsEnabled(); 920 firstItem = allEnabled ? 0 : 921 mDropDownList.lookForSelectablePosition(0, true); 922 lastItem = allEnabled ? adapter.getCount() - 1 : 923 mDropDownList.lookForSelectablePosition(adapter.getCount() - 1, false); 924 } 925 926 if ((below && keyCode == KeyEvent.KEYCODE_DPAD_UP && curIndex <= firstItem) || 927 (!below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN && curIndex >= lastItem)) { 928 // When the selection is at the top, we block the key 929 // event to prevent focus from moving. 930 clearListSelection(); 931 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); 932 show(); 933 return true; 934 } else { 935 // WARNING: Please read the comment where mListSelectionHidden 936 // is declared 937 mDropDownList.setListSelectionHidden(false); 938 } 939 940 consumed = mDropDownList.onKeyDown(keyCode, event); 941 if (DEBUG) Log.v(TAG, "Key down: code=" + keyCode + " list consumed=" + consumed); 942 943 if (consumed) { 944 // If it handled the key event, then the user is 945 // navigating in the list, so we should put it in front. 946 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); 947 // Here's a little trick we need to do to make sure that 948 // the list view is actually showing its focus indicator, 949 // by ensuring it has focus and getting its window out 950 // of touch mode. 951 mDropDownList.requestFocusFromTouch(); 952 show(); 953 954 switch (keyCode) { 955 // avoid passing the focus from the text view to the 956 // next component 957 case KeyEvent.KEYCODE_ENTER: 958 case KeyEvent.KEYCODE_DPAD_CENTER: 959 case KeyEvent.KEYCODE_DPAD_DOWN: 960 case KeyEvent.KEYCODE_DPAD_UP: 961 return true; 962 } 963 } else { 964 if (below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { 965 // when the selection is at the bottom, we block the 966 // event to avoid going to the next focusable widget 967 if (curIndex == lastItem) { 968 return true; 969 } 970 } else if (!below && keyCode == KeyEvent.KEYCODE_DPAD_UP && 971 curIndex == firstItem) { 972 return true; 973 } 974 } 975 } 976 } 977 978 return false; 979 } 980 981 /** 982 * Filter key up events. By forwarding key up events to this function, 983 * views using non-modal ListPopupWindow can have it handle key selection of items. 984 * 985 * @param keyCode keyCode param passed to the host view's onKeyUp 986 * @param event event param passed to the host view's onKeyUp 987 * @return true if the event was handled, false if it was ignored. 988 * 989 * @see #setModal(boolean) 990 * @see #onKeyDown(int, KeyEvent) 991 */ 992 public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) { 993 if (isShowing() && mDropDownList.getSelectedItemPosition() >= 0) { 994 boolean consumed = mDropDownList.onKeyUp(keyCode, event); 995 if (consumed && KeyEvent.isConfirmKey(keyCode)) { 996 // if the list accepts the key events and the key event was a click, the text view 997 // gets the selected item from the drop down as its content 998 dismiss(); 999 } 1000 return consumed; 1001 } 1002 return false; 1003 } 1004 1005 /** 1006 * Filter pre-IME key events. By forwarding {@link View#onKeyPreIme(int, KeyEvent)} 1007 * events to this function, views using ListPopupWindow can have it dismiss the popup 1008 * when the back key is pressed. 1009 * 1010 * @param keyCode keyCode param passed to the host view's onKeyPreIme 1011 * @param event event param passed to the host view's onKeyPreIme 1012 * @return true if the event was handled, false if it was ignored. 1013 * 1014 * @see #setModal(boolean) 1015 */ 1016 public boolean onKeyPreIme(int keyCode, @NonNull KeyEvent event) { 1017 if (keyCode == KeyEvent.KEYCODE_BACK && isShowing()) { 1018 // special case for the back key, we do not even try to send it 1019 // to the drop down list but instead, consume it immediately 1020 final View anchorView = mDropDownAnchorView; 1021 if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { 1022 KeyEvent.DispatcherState state = anchorView.getKeyDispatcherState(); 1023 if (state != null) { 1024 state.startTracking(event, this); 1025 } 1026 return true; 1027 } else if (event.getAction() == KeyEvent.ACTION_UP) { 1028 KeyEvent.DispatcherState state = anchorView.getKeyDispatcherState(); 1029 if (state != null) { 1030 state.handleUpEvent(event); 1031 } 1032 if (event.isTracking() && !event.isCanceled()) { 1033 dismiss(); 1034 return true; 1035 } 1036 } 1037 } 1038 return false; 1039 } 1040 1041 /** 1042 * Returns an {@link OnTouchListener} that can be added to the source view 1043 * to implement drag-to-open behavior. Generally, the source view should be 1044 * the same view that was passed to {@link #setAnchorView}. 1045 * <p> 1046 * When the listener is set on a view, touching that view and dragging 1047 * outside of its bounds will open the popup window. Lifting will select the 1048 * currently touched list item. 1049 * <p> 1050 * Example usage: 1051 * <pre> 1052 * ListPopupWindow myPopup = new ListPopupWindow(context); 1053 * myPopup.setAnchor(myAnchor); 1054 * OnTouchListener dragListener = myPopup.createDragToOpenListener(myAnchor); 1055 * myAnchor.setOnTouchListener(dragListener); 1056 * </pre> 1057 * 1058 * @param src the view on which the resulting listener will be set 1059 * @return a touch listener that controls drag-to-open behavior 1060 */ 1061 public OnTouchListener createDragToOpenListener(View src) { 1062 return new ForwardingListener(src) { 1063 @Override 1064 public ShowableListMenu getPopup() { 1065 return ListPopupWindow.this; 1066 } 1067 }; 1068 } 1069 1070 /** 1071 * <p>Builds the popup window's content and returns the height the popup 1072 * should have. Returns -1 when the content already exists.</p> 1073 * 1074 * @return the content's height or -1 if content already exists 1075 */ 1076 private int buildDropDown() { 1077 ViewGroup dropDownView; 1078 int otherHeights = 0; 1079 1080 if (mDropDownList == null) { 1081 Context context = mContext; 1082 1083 /** 1084 * This Runnable exists for the sole purpose of checking if the view layout has got 1085 * completed and if so call showDropDown to display the drop down. This is used to show 1086 * the drop down as soon as possible after user opens up the search dialog, without 1087 * waiting for the normal UI pipeline to do it's job which is slower than this method. 1088 */ 1089 mShowDropDownRunnable = new Runnable() { 1090 public void run() { 1091 // View layout should be all done before displaying the drop down. 1092 View view = getAnchorView(); 1093 if (view != null && view.getWindowToken() != null) { 1094 show(); 1095 } 1096 } 1097 }; 1098 1099 mDropDownList = createDropDownListView(context, !mModal); 1100 if (mDropDownListHighlight != null) { 1101 mDropDownList.setSelector(mDropDownListHighlight); 1102 } 1103 mDropDownList.setAdapter(mAdapter); 1104 mDropDownList.setOnItemClickListener(mItemClickListener); 1105 mDropDownList.setFocusable(true); 1106 mDropDownList.setFocusableInTouchMode(true); 1107 mDropDownList.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 1108 public void onItemSelected(AdapterView<?> parent, View view, 1109 int position, long id) { 1110 1111 if (position != -1) { 1112 DropDownListView dropDownList = mDropDownList; 1113 1114 if (dropDownList != null) { 1115 dropDownList.setListSelectionHidden(false); 1116 } 1117 } 1118 } 1119 1120 public void onNothingSelected(AdapterView<?> parent) { 1121 } 1122 }); 1123 mDropDownList.setOnScrollListener(mScrollListener); 1124 1125 if (mItemSelectedListener != null) { 1126 mDropDownList.setOnItemSelectedListener(mItemSelectedListener); 1127 } 1128 1129 dropDownView = mDropDownList; 1130 1131 View hintView = mPromptView; 1132 if (hintView != null) { 1133 // if a hint has been specified, we accomodate more space for it and 1134 // add a text view in the drop down menu, at the bottom of the list 1135 LinearLayout hintContainer = new LinearLayout(context); 1136 hintContainer.setOrientation(LinearLayout.VERTICAL); 1137 1138 LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams( 1139 ViewGroup.LayoutParams.MATCH_PARENT, 0, 1.0f 1140 ); 1141 1142 switch (mPromptPosition) { 1143 case POSITION_PROMPT_BELOW: 1144 hintContainer.addView(dropDownView, hintParams); 1145 hintContainer.addView(hintView); 1146 break; 1147 1148 case POSITION_PROMPT_ABOVE: 1149 hintContainer.addView(hintView); 1150 hintContainer.addView(dropDownView, hintParams); 1151 break; 1152 1153 default: 1154 Log.e(TAG, "Invalid hint position " + mPromptPosition); 1155 break; 1156 } 1157 1158 // Measure the hint's height to find how much more vertical 1159 // space we need to add to the drop down's height. 1160 final int widthSize; 1161 final int widthMode; 1162 if (mDropDownWidth >= 0) { 1163 widthMode = MeasureSpec.AT_MOST; 1164 widthSize = mDropDownWidth; 1165 } else { 1166 widthMode = MeasureSpec.UNSPECIFIED; 1167 widthSize = 0; 1168 } 1169 final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode); 1170 final int heightSpec = MeasureSpec.UNSPECIFIED; 1171 hintView.measure(widthSpec, heightSpec); 1172 1173 hintParams = (LinearLayout.LayoutParams) hintView.getLayoutParams(); 1174 otherHeights = hintView.getMeasuredHeight() + hintParams.topMargin 1175 + hintParams.bottomMargin; 1176 1177 dropDownView = hintContainer; 1178 } 1179 1180 mPopup.setContentView(dropDownView); 1181 } else { 1182 final View view = mPromptView; 1183 if (view != null) { 1184 LinearLayout.LayoutParams hintParams = 1185 (LinearLayout.LayoutParams) view.getLayoutParams(); 1186 otherHeights = view.getMeasuredHeight() + hintParams.topMargin 1187 + hintParams.bottomMargin; 1188 } 1189 } 1190 1191 // getMaxAvailableHeight() subtracts the padding, so we put it back 1192 // to get the available height for the whole window. 1193 final int padding; 1194 final Drawable background = mPopup.getBackground(); 1195 if (background != null) { 1196 background.getPadding(mTempRect); 1197 padding = mTempRect.top + mTempRect.bottom; 1198 1199 // If we don't have an explicit vertical offset, determine one from 1200 // the window background so that content will line up. 1201 if (!mDropDownVerticalOffsetSet) { 1202 mDropDownVerticalOffset = -mTempRect.top; 1203 } 1204 } else { 1205 mTempRect.setEmpty(); 1206 padding = 0; 1207 } 1208 1209 // Max height available on the screen for a popup. 1210 final boolean ignoreBottomDecorations = 1211 mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED; 1212 final int maxHeight = mPopup.getMaxAvailableHeight( 1213 getAnchorView(), mDropDownVerticalOffset, ignoreBottomDecorations); 1214 if (mDropDownAlwaysVisible || mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { 1215 return maxHeight + padding; 1216 } 1217 1218 final int childWidthSpec; 1219 switch (mDropDownWidth) { 1220 case ViewGroup.LayoutParams.WRAP_CONTENT: 1221 childWidthSpec = MeasureSpec.makeMeasureSpec( 1222 mContext.getResources().getDisplayMetrics().widthPixels 1223 - (mTempRect.left + mTempRect.right), 1224 MeasureSpec.AT_MOST); 1225 break; 1226 case ViewGroup.LayoutParams.MATCH_PARENT: 1227 childWidthSpec = MeasureSpec.makeMeasureSpec( 1228 mContext.getResources().getDisplayMetrics().widthPixels 1229 - (mTempRect.left + mTempRect.right), 1230 MeasureSpec.EXACTLY); 1231 break; 1232 default: 1233 childWidthSpec = MeasureSpec.makeMeasureSpec(mDropDownWidth, MeasureSpec.EXACTLY); 1234 break; 1235 } 1236 1237 // Add padding only if the list has items in it, that way we don't show 1238 // the popup if it is not needed. 1239 final int listContent = mDropDownList.measureHeightOfChildren(childWidthSpec, 1240 0, DropDownListView.NO_POSITION, maxHeight - otherHeights, -1); 1241 if (listContent > 0) { 1242 final int listPadding = mDropDownList.getPaddingTop() 1243 + mDropDownList.getPaddingBottom(); 1244 otherHeights += padding + listPadding; 1245 } 1246 1247 return listContent + otherHeights; 1248 } 1249 1250 /** 1251 * @hide 1252 */ 1253 public void setOverlapAnchor(boolean overlap) { 1254 mOverlapAnchor = overlap; 1255 } 1256 1257 private class PopupDataSetObserver extends DataSetObserver { 1258 @Override 1259 public void onChanged() { 1260 if (isShowing()) { 1261 // Resize the popup to fit new content 1262 show(); 1263 } 1264 } 1265 1266 @Override 1267 public void onInvalidated() { 1268 dismiss(); 1269 } 1270 } 1271 1272 private class ListSelectorHider implements Runnable { 1273 public void run() { 1274 clearListSelection(); 1275 } 1276 } 1277 1278 private class ResizePopupRunnable implements Runnable { 1279 public void run() { 1280 if (mDropDownList != null && mDropDownList.isAttachedToWindow() 1281 && mDropDownList.getCount() > mDropDownList.getChildCount() 1282 && mDropDownList.getChildCount() <= mListItemExpandMaximum) { 1283 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); 1284 show(); 1285 } 1286 } 1287 } 1288 1289 private class PopupTouchInterceptor implements OnTouchListener { 1290 public boolean onTouch(View v, MotionEvent event) { 1291 final int action = event.getAction(); 1292 final int x = (int) event.getX(); 1293 final int y = (int) event.getY(); 1294 1295 if (action == MotionEvent.ACTION_DOWN && 1296 mPopup != null && mPopup.isShowing() && 1297 (x >= 0 && x < mPopup.getWidth() && y >= 0 && y < mPopup.getHeight())) { 1298 mHandler.postDelayed(mResizePopupRunnable, EXPAND_LIST_TIMEOUT); 1299 } else if (action == MotionEvent.ACTION_UP) { 1300 mHandler.removeCallbacks(mResizePopupRunnable); 1301 } 1302 return false; 1303 } 1304 } 1305 1306 private class PopupScrollListener implements ListView.OnScrollListener { 1307 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 1308 int totalItemCount) { 1309 1310 } 1311 1312 public void onScrollStateChanged(AbsListView view, int scrollState) { 1313 if (scrollState == SCROLL_STATE_TOUCH_SCROLL && 1314 !isInputMethodNotNeeded() && mPopup.getContentView() != null) { 1315 mHandler.removeCallbacks(mResizePopupRunnable); 1316 mResizePopupRunnable.run(); 1317 } 1318 } 1319 } 1320} 1321