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