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