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