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 17 package android.widget; 18 19 import android.content.Context; 20 import android.database.DataSetObserver; 21 import android.graphics.Rect; 22 import android.graphics.drawable.Drawable; 23 import android.os.Handler; 24 import android.util.AttributeSet; 25 import android.util.Log; 26 import android.view.KeyEvent; 27 import android.view.MotionEvent; 28 import android.view.View; 29 import android.view.View.MeasureSpec; 30 import android.view.View.OnTouchListener; 31 import android.view.ViewGroup; 32 import 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 */ 45 public 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 */ ListPopupWindow(Context context)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 */ ListPopupWindow(Context context, AttributeSet attrs)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 */ ListPopupWindow(Context context, AttributeSet attrs, int defStyleAttr)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 */ ListPopupWindow(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)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 */ setAdapter(ListAdapter adapter)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 */ setPromptPosition(int position)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 */ getPromptPosition()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 */ setModal(boolean modal)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 */ isModal()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 */ setForceIgnoreOutsideTouch(boolean forceIgnoreOutsideTouch)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 */ setDropDownAlwaysVisible(boolean dropDownAlwaysVisible)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 */ isDropDownAlwaysVisible()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 */ setSoftInputMode(int mode)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 */ getSoftInputMode()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 */ setListSelector(Drawable selector)330 public void setListSelector(Drawable selector) { 331 mDropDownListHighlight = selector; 332 } 333 334 /** 335 * @return The background drawable for the popup window. 336 */ getBackground()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 */ setBackgroundDrawable(Drawable d)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 */ setAnimationStyle(int animationStyle)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 */ getAnimationStyle()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 */ getAnchorView()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 */ setAnchorView(View anchor)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 */ getHorizontalOffset()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 */ setHorizontalOffset(int offset)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 */ getVerticalOffset()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 */ setVerticalOffset(int offset)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 */ getWidth()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 */ setWidth(int width)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 */ setContentWidth(int width)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 } else { 453 setWidth(width); 454 } 455 } 456 457 /** 458 * @return The height of the popup window in pixels. 459 */ getHeight()460 public int getHeight() { 461 return mDropDownHeight; 462 } 463 464 /** 465 * Sets the height of the popup window in pixels. Can also be {@link #MATCH_PARENT}. 466 * 467 * @param height Height of the popup window. 468 */ setHeight(int height)469 public void setHeight(int height) { 470 mDropDownHeight = height; 471 } 472 473 /** 474 * Sets a listener to receive events when a list item is clicked. 475 * 476 * @param clickListener Listener to register 477 * 478 * @see ListView#setOnItemClickListener(android.widget.AdapterView.OnItemClickListener) 479 */ setOnItemClickListener(AdapterView.OnItemClickListener clickListener)480 public void setOnItemClickListener(AdapterView.OnItemClickListener clickListener) { 481 mItemClickListener = clickListener; 482 } 483 484 /** 485 * Sets a listener to receive events when a list item is selected. 486 * 487 * @param selectedListener Listener to register. 488 * 489 * @see ListView#setOnItemSelectedListener(android.widget.AdapterView.OnItemSelectedListener) 490 */ setOnItemSelectedListener(AdapterView.OnItemSelectedListener selectedListener)491 public void setOnItemSelectedListener(AdapterView.OnItemSelectedListener selectedListener) { 492 mItemSelectedListener = selectedListener; 493 } 494 495 /** 496 * Set a view to act as a user prompt for this popup window. Where the prompt view will appear 497 * is controlled by {@link #setPromptPosition(int)}. 498 * 499 * @param prompt View to use as an informational prompt. 500 */ setPromptView(View prompt)501 public void setPromptView(View prompt) { 502 boolean showing = isShowing(); 503 if (showing) { 504 removePromptView(); 505 } 506 mPromptView = prompt; 507 if (showing) { 508 show(); 509 } 510 } 511 512 /** 513 * Post a {@link #show()} call to the UI thread. 514 */ postShow()515 public void postShow() { 516 mHandler.post(mShowDropDownRunnable); 517 } 518 519 /** 520 * Show the popup list. If the list is already showing, this method 521 * will recalculate the popup's size and position. 522 */ show()523 public void show() { 524 int height = buildDropDown(); 525 526 int widthSpec = 0; 527 int heightSpec = 0; 528 529 boolean noInputMethod = isInputMethodNotNeeded(); 530 mPopup.setAllowScrollingAnchorParent(!noInputMethod); 531 532 if (mPopup.isShowing()) { 533 if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) { 534 // The call to PopupWindow's update method below can accept -1 for any 535 // value you do not want to update. 536 widthSpec = -1; 537 } else if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) { 538 widthSpec = getAnchorView().getWidth(); 539 } else { 540 widthSpec = mDropDownWidth; 541 } 542 543 if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { 544 // The call to PopupWindow's update method below can accept -1 for any 545 // value you do not want to update. 546 heightSpec = noInputMethod ? height : ViewGroup.LayoutParams.MATCH_PARENT; 547 if (noInputMethod) { 548 mPopup.setWindowLayoutMode( 549 mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ? 550 ViewGroup.LayoutParams.MATCH_PARENT : 0, 0); 551 } else { 552 mPopup.setWindowLayoutMode( 553 mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ? 554 ViewGroup.LayoutParams.MATCH_PARENT : 0, 555 ViewGroup.LayoutParams.MATCH_PARENT); 556 } 557 } else if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { 558 heightSpec = height; 559 } else { 560 heightSpec = mDropDownHeight; 561 } 562 563 mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible); 564 565 mPopup.update(getAnchorView(), mDropDownHorizontalOffset, 566 mDropDownVerticalOffset, widthSpec, heightSpec); 567 } else { 568 if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) { 569 widthSpec = ViewGroup.LayoutParams.MATCH_PARENT; 570 } else { 571 if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) { 572 mPopup.setWidth(getAnchorView().getWidth()); 573 } else { 574 mPopup.setWidth(mDropDownWidth); 575 } 576 } 577 578 if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { 579 heightSpec = ViewGroup.LayoutParams.MATCH_PARENT; 580 } else { 581 if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { 582 mPopup.setHeight(height); 583 } else { 584 mPopup.setHeight(mDropDownHeight); 585 } 586 } 587 588 mPopup.setWindowLayoutMode(widthSpec, heightSpec); 589 mPopup.setClipToScreenEnabled(true); 590 591 // use outside touchable to dismiss drop down when touching outside of it, so 592 // only set this if the dropdown is not always visible 593 mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible); 594 mPopup.setTouchInterceptor(mTouchInterceptor); 595 mPopup.showAsDropDown(getAnchorView(), 596 mDropDownHorizontalOffset, mDropDownVerticalOffset); 597 mDropDownList.setSelection(ListView.INVALID_POSITION); 598 599 if (!mModal || mDropDownList.isInTouchMode()) { 600 clearListSelection(); 601 } 602 if (!mModal) { 603 mHandler.post(mHideSelector); 604 } 605 } 606 } 607 608 /** 609 * Dismiss the popup window. 610 */ dismiss()611 public void dismiss() { 612 mPopup.dismiss(); 613 removePromptView(); 614 mPopup.setContentView(null); 615 mDropDownList = null; 616 mHandler.removeCallbacks(mResizePopupRunnable); 617 } 618 619 /** 620 * Set a listener to receive a callback when the popup is dismissed. 621 * 622 * @param listener Listener that will be notified when the popup is dismissed. 623 */ setOnDismissListener(PopupWindow.OnDismissListener listener)624 public void setOnDismissListener(PopupWindow.OnDismissListener listener) { 625 mPopup.setOnDismissListener(listener); 626 } 627 removePromptView()628 private void removePromptView() { 629 if (mPromptView != null) { 630 final ViewParent parent = mPromptView.getParent(); 631 if (parent instanceof ViewGroup) { 632 final ViewGroup group = (ViewGroup) parent; 633 group.removeView(mPromptView); 634 } 635 } 636 } 637 638 /** 639 * Control how the popup operates with an input method: one of 640 * {@link #INPUT_METHOD_FROM_FOCUSABLE}, {@link #INPUT_METHOD_NEEDED}, 641 * or {@link #INPUT_METHOD_NOT_NEEDED}. 642 * 643 * <p>If the popup is showing, calling this method will take effect only 644 * the next time the popup is shown or through a manual call to the {@link #show()} 645 * method.</p> 646 * 647 * @see #getInputMethodMode() 648 * @see #show() 649 */ setInputMethodMode(int mode)650 public void setInputMethodMode(int mode) { 651 mPopup.setInputMethodMode(mode); 652 } 653 654 /** 655 * Return the current value in {@link #setInputMethodMode(int)}. 656 * 657 * @see #setInputMethodMode(int) 658 */ getInputMethodMode()659 public int getInputMethodMode() { 660 return mPopup.getInputMethodMode(); 661 } 662 663 /** 664 * Set the selected position of the list. 665 * Only valid when {@link #isShowing()} == {@code true}. 666 * 667 * @param position List position to set as selected. 668 */ setSelection(int position)669 public void setSelection(int position) { 670 DropDownListView list = mDropDownList; 671 if (isShowing() && list != null) { 672 list.mListSelectionHidden = false; 673 list.setSelection(position); 674 if (list.getChoiceMode() != ListView.CHOICE_MODE_NONE) { 675 list.setItemChecked(position, true); 676 } 677 } 678 } 679 680 /** 681 * Clear any current list selection. 682 * Only valid when {@link #isShowing()} == {@code true}. 683 */ clearListSelection()684 public void clearListSelection() { 685 final DropDownListView list = mDropDownList; 686 if (list != null) { 687 // WARNING: Please read the comment where mListSelectionHidden is declared 688 list.mListSelectionHidden = true; 689 list.hideSelector(); 690 list.requestLayout(); 691 } 692 } 693 694 /** 695 * @return {@code true} if the popup is currently showing, {@code false} otherwise. 696 */ isShowing()697 public boolean isShowing() { 698 return mPopup.isShowing(); 699 } 700 701 /** 702 * @return {@code true} if this popup is configured to assume the user does not need 703 * to interact with the IME while it is showing, {@code false} otherwise. 704 */ isInputMethodNotNeeded()705 public boolean isInputMethodNotNeeded() { 706 return mPopup.getInputMethodMode() == INPUT_METHOD_NOT_NEEDED; 707 } 708 709 /** 710 * Perform an item click operation on the specified list adapter position. 711 * 712 * @param position Adapter position for performing the click 713 * @return true if the click action could be performed, false if not. 714 * (e.g. if the popup was not showing, this method would return false.) 715 */ performItemClick(int position)716 public boolean performItemClick(int position) { 717 if (isShowing()) { 718 if (mItemClickListener != null) { 719 final DropDownListView list = mDropDownList; 720 final View child = list.getChildAt(position - list.getFirstVisiblePosition()); 721 final ListAdapter adapter = list.getAdapter(); 722 mItemClickListener.onItemClick(list, child, position, adapter.getItemId(position)); 723 } 724 return true; 725 } 726 return false; 727 } 728 729 /** 730 * @return The currently selected item or null if the popup is not showing. 731 */ getSelectedItem()732 public Object getSelectedItem() { 733 if (!isShowing()) { 734 return null; 735 } 736 return mDropDownList.getSelectedItem(); 737 } 738 739 /** 740 * @return The position of the currently selected item or {@link ListView#INVALID_POSITION} 741 * if {@link #isShowing()} == {@code false}. 742 * 743 * @see ListView#getSelectedItemPosition() 744 */ getSelectedItemPosition()745 public int getSelectedItemPosition() { 746 if (!isShowing()) { 747 return ListView.INVALID_POSITION; 748 } 749 return mDropDownList.getSelectedItemPosition(); 750 } 751 752 /** 753 * @return The ID of the currently selected item or {@link ListView#INVALID_ROW_ID} 754 * if {@link #isShowing()} == {@code false}. 755 * 756 * @see ListView#getSelectedItemId() 757 */ getSelectedItemId()758 public long getSelectedItemId() { 759 if (!isShowing()) { 760 return ListView.INVALID_ROW_ID; 761 } 762 return mDropDownList.getSelectedItemId(); 763 } 764 765 /** 766 * @return The View for the currently selected item or null if 767 * {@link #isShowing()} == {@code false}. 768 * 769 * @see ListView#getSelectedView() 770 */ getSelectedView()771 public View getSelectedView() { 772 if (!isShowing()) { 773 return null; 774 } 775 return mDropDownList.getSelectedView(); 776 } 777 778 /** 779 * @return The {@link ListView} displayed within the popup window. 780 * Only valid when {@link #isShowing()} == {@code true}. 781 */ getListView()782 public ListView getListView() { 783 return mDropDownList; 784 } 785 786 /** 787 * The maximum number of list items that can be visible and still have 788 * the list expand when touched. 789 * 790 * @param max Max number of items that can be visible and still allow the list to expand. 791 */ setListItemExpandMax(int max)792 void setListItemExpandMax(int max) { 793 mListItemExpandMaximum = max; 794 } 795 796 /** 797 * Filter key down events. By forwarding key down events to this function, 798 * views using non-modal ListPopupWindow can have it handle key selection of items. 799 * 800 * @param keyCode keyCode param passed to the host view's onKeyDown 801 * @param event event param passed to the host view's onKeyDown 802 * @return true if the event was handled, false if it was ignored. 803 * 804 * @see #setModal(boolean) 805 */ onKeyDown(int keyCode, KeyEvent event)806 public boolean onKeyDown(int keyCode, KeyEvent event) { 807 // when the drop down is shown, we drive it directly 808 if (isShowing()) { 809 // the key events are forwarded to the list in the drop down view 810 // note that ListView handles space but we don't want that to happen 811 // also if selection is not currently in the drop down, then don't 812 // let center or enter presses go there since that would cause it 813 // to select one of its items 814 if (keyCode != KeyEvent.KEYCODE_SPACE 815 && (mDropDownList.getSelectedItemPosition() >= 0 816 || (keyCode != KeyEvent.KEYCODE_ENTER 817 && keyCode != KeyEvent.KEYCODE_DPAD_CENTER))) { 818 int curIndex = mDropDownList.getSelectedItemPosition(); 819 boolean consumed; 820 821 final boolean below = !mPopup.isAboveAnchor(); 822 823 final ListAdapter adapter = mAdapter; 824 825 boolean allEnabled; 826 int firstItem = Integer.MAX_VALUE; 827 int lastItem = Integer.MIN_VALUE; 828 829 if (adapter != null) { 830 allEnabled = adapter.areAllItemsEnabled(); 831 firstItem = allEnabled ? 0 : 832 mDropDownList.lookForSelectablePosition(0, true); 833 lastItem = allEnabled ? adapter.getCount() - 1 : 834 mDropDownList.lookForSelectablePosition(adapter.getCount() - 1, false); 835 } 836 837 if ((below && keyCode == KeyEvent.KEYCODE_DPAD_UP && curIndex <= firstItem) || 838 (!below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN && curIndex >= lastItem)) { 839 // When the selection is at the top, we block the key 840 // event to prevent focus from moving. 841 clearListSelection(); 842 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); 843 show(); 844 return true; 845 } else { 846 // WARNING: Please read the comment where mListSelectionHidden 847 // is declared 848 mDropDownList.mListSelectionHidden = false; 849 } 850 851 consumed = mDropDownList.onKeyDown(keyCode, event); 852 if (DEBUG) Log.v(TAG, "Key down: code=" + keyCode + " list consumed=" + consumed); 853 854 if (consumed) { 855 // If it handled the key event, then the user is 856 // navigating in the list, so we should put it in front. 857 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); 858 // Here's a little trick we need to do to make sure that 859 // the list view is actually showing its focus indicator, 860 // by ensuring it has focus and getting its window out 861 // of touch mode. 862 mDropDownList.requestFocusFromTouch(); 863 show(); 864 865 switch (keyCode) { 866 // avoid passing the focus from the text view to the 867 // next component 868 case KeyEvent.KEYCODE_ENTER: 869 case KeyEvent.KEYCODE_DPAD_CENTER: 870 case KeyEvent.KEYCODE_DPAD_DOWN: 871 case KeyEvent.KEYCODE_DPAD_UP: 872 return true; 873 } 874 } else { 875 if (below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { 876 // when the selection is at the bottom, we block the 877 // event to avoid going to the next focusable widget 878 if (curIndex == lastItem) { 879 return true; 880 } 881 } else if (!below && keyCode == KeyEvent.KEYCODE_DPAD_UP && 882 curIndex == firstItem) { 883 return true; 884 } 885 } 886 } 887 } 888 889 return false; 890 } 891 892 /** 893 * Filter key down events. By forwarding key up events to this function, 894 * views using non-modal ListPopupWindow can have it handle key selection of items. 895 * 896 * @param keyCode keyCode param passed to the host view's onKeyUp 897 * @param event event param passed to the host view's onKeyUp 898 * @return true if the event was handled, false if it was ignored. 899 * 900 * @see #setModal(boolean) 901 */ onKeyUp(int keyCode, KeyEvent event)902 public boolean onKeyUp(int keyCode, KeyEvent event) { 903 if (isShowing() && mDropDownList.getSelectedItemPosition() >= 0) { 904 boolean consumed = mDropDownList.onKeyUp(keyCode, event); 905 if (consumed) { 906 switch (keyCode) { 907 // if the list accepts the key events and the key event 908 // was a click, the text view gets the selected item 909 // from the drop down as its content 910 case KeyEvent.KEYCODE_ENTER: 911 case KeyEvent.KEYCODE_DPAD_CENTER: 912 dismiss(); 913 break; 914 } 915 } 916 return consumed; 917 } 918 return false; 919 } 920 921 /** 922 * Filter pre-IME key events. By forwarding {@link View#onKeyPreIme(int, KeyEvent)} 923 * events to this function, views using ListPopupWindow can have it dismiss the popup 924 * when the back key is pressed. 925 * 926 * @param keyCode keyCode param passed to the host view's onKeyPreIme 927 * @param event event param passed to the host view's onKeyPreIme 928 * @return true if the event was handled, false if it was ignored. 929 * 930 * @see #setModal(boolean) 931 */ onKeyPreIme(int keyCode, KeyEvent event)932 public boolean onKeyPreIme(int keyCode, KeyEvent event) { 933 if (keyCode == KeyEvent.KEYCODE_BACK && isShowing()) { 934 // special case for the back key, we do not even try to send it 935 // to the drop down list but instead, consume it immediately 936 final View anchorView = mDropDownAnchorView; 937 if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { 938 KeyEvent.DispatcherState state = anchorView.getKeyDispatcherState(); 939 if (state != null) { 940 state.startTracking(event, this); 941 } 942 return true; 943 } else if (event.getAction() == KeyEvent.ACTION_UP) { 944 KeyEvent.DispatcherState state = anchorView.getKeyDispatcherState(); 945 if (state != null) { 946 state.handleUpEvent(event); 947 } 948 if (event.isTracking() && !event.isCanceled()) { 949 dismiss(); 950 return true; 951 } 952 } 953 } 954 return false; 955 } 956 957 /** 958 * <p>Builds the popup window's content and returns the height the popup 959 * should have. Returns -1 when the content already exists.</p> 960 * 961 * @return the content's height or -1 if content already exists 962 */ buildDropDown()963 private int buildDropDown() { 964 ViewGroup dropDownView; 965 int otherHeights = 0; 966 967 if (mDropDownList == null) { 968 Context context = mContext; 969 970 /** 971 * This Runnable exists for the sole purpose of checking if the view layout has got 972 * completed and if so call showDropDown to display the drop down. This is used to show 973 * the drop down as soon as possible after user opens up the search dialog, without 974 * waiting for the normal UI pipeline to do it's job which is slower than this method. 975 */ 976 mShowDropDownRunnable = new Runnable() { 977 public void run() { 978 // View layout should be all done before displaying the drop down. 979 View view = getAnchorView(); 980 if (view != null && view.getWindowToken() != null) { 981 show(); 982 } 983 } 984 }; 985 986 mDropDownList = new DropDownListView(context, !mModal); 987 if (mDropDownListHighlight != null) { 988 mDropDownList.setSelector(mDropDownListHighlight); 989 } 990 mDropDownList.setAdapter(mAdapter); 991 mDropDownList.setOnItemClickListener(mItemClickListener); 992 mDropDownList.setFocusable(true); 993 mDropDownList.setFocusableInTouchMode(true); 994 mDropDownList.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 995 public void onItemSelected(AdapterView<?> parent, View view, 996 int position, long id) { 997 998 if (position != -1) { 999 DropDownListView dropDownList = mDropDownList; 1000 1001 if (dropDownList != null) { 1002 dropDownList.mListSelectionHidden = false; 1003 } 1004 } 1005 } 1006 1007 public void onNothingSelected(AdapterView<?> parent) { 1008 } 1009 }); 1010 mDropDownList.setOnScrollListener(mScrollListener); 1011 1012 if (mItemSelectedListener != null) { 1013 mDropDownList.setOnItemSelectedListener(mItemSelectedListener); 1014 } 1015 1016 dropDownView = mDropDownList; 1017 1018 View hintView = mPromptView; 1019 if (hintView != null) { 1020 // if an hint has been specified, we accomodate more space for it and 1021 // add a text view in the drop down menu, at the bottom of the list 1022 LinearLayout hintContainer = new LinearLayout(context); 1023 hintContainer.setOrientation(LinearLayout.VERTICAL); 1024 1025 LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams( 1026 ViewGroup.LayoutParams.MATCH_PARENT, 0, 1.0f 1027 ); 1028 1029 switch (mPromptPosition) { 1030 case POSITION_PROMPT_BELOW: 1031 hintContainer.addView(dropDownView, hintParams); 1032 hintContainer.addView(hintView); 1033 break; 1034 1035 case POSITION_PROMPT_ABOVE: 1036 hintContainer.addView(hintView); 1037 hintContainer.addView(dropDownView, hintParams); 1038 break; 1039 1040 default: 1041 Log.e(TAG, "Invalid hint position " + mPromptPosition); 1042 break; 1043 } 1044 1045 // measure the hint's height to find how much more vertical space 1046 // we need to add to the drop down's height 1047 int widthSpec = MeasureSpec.makeMeasureSpec(mDropDownWidth, MeasureSpec.AT_MOST); 1048 int heightSpec = MeasureSpec.UNSPECIFIED; 1049 hintView.measure(widthSpec, heightSpec); 1050 1051 hintParams = (LinearLayout.LayoutParams) hintView.getLayoutParams(); 1052 otherHeights = hintView.getMeasuredHeight() + hintParams.topMargin 1053 + hintParams.bottomMargin; 1054 1055 dropDownView = hintContainer; 1056 } 1057 1058 mPopup.setContentView(dropDownView); 1059 } else { 1060 dropDownView = (ViewGroup) mPopup.getContentView(); 1061 final View view = mPromptView; 1062 if (view != null) { 1063 LinearLayout.LayoutParams hintParams = 1064 (LinearLayout.LayoutParams) view.getLayoutParams(); 1065 otherHeights = view.getMeasuredHeight() + hintParams.topMargin 1066 + hintParams.bottomMargin; 1067 } 1068 } 1069 1070 // getMaxAvailableHeight() subtracts the padding, so we put it back 1071 // to get the available height for the whole window 1072 int padding = 0; 1073 Drawable background = mPopup.getBackground(); 1074 if (background != null) { 1075 background.getPadding(mTempRect); 1076 padding = mTempRect.top + mTempRect.bottom; 1077 1078 // If we don't have an explicit vertical offset, determine one from the window 1079 // background so that content will line up. 1080 if (!mDropDownVerticalOffsetSet) { 1081 mDropDownVerticalOffset = -mTempRect.top; 1082 } 1083 } 1084 1085 // Max height available on the screen for a popup. 1086 boolean ignoreBottomDecorations = 1087 mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED; 1088 final int maxHeight = mPopup.getMaxAvailableHeight( 1089 getAnchorView(), mDropDownVerticalOffset, ignoreBottomDecorations); 1090 1091 if (mDropDownAlwaysVisible || mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { 1092 return maxHeight + padding; 1093 } 1094 1095 final int listContent = mDropDownList.measureHeightOfChildren(MeasureSpec.UNSPECIFIED, 1096 0, ListView.NO_POSITION, maxHeight - otherHeights, -1); 1097 // add padding only if the list has items in it, that way we don't show 1098 // the popup if it is not needed 1099 if (listContent > 0) otherHeights += padding; 1100 1101 return listContent + otherHeights; 1102 } 1103 1104 /** 1105 * <p>Wrapper class for a ListView. This wrapper can hijack the focus to 1106 * make sure the list uses the appropriate drawables and states when 1107 * displayed on screen within a drop down. The focus is never actually 1108 * passed to the drop down in this mode; the list only looks focused.</p> 1109 */ 1110 private static class DropDownListView extends ListView { 1111 private static final String TAG = ListPopupWindow.TAG + ".DropDownListView"; 1112 /* 1113 * WARNING: This is a workaround for a touch mode issue. 1114 * 1115 * Touch mode is propagated lazily to windows. This causes problems in 1116 * the following scenario: 1117 * - Type something in the AutoCompleteTextView and get some results 1118 * - Move down with the d-pad to select an item in the list 1119 * - Move up with the d-pad until the selection disappears 1120 * - Type more text in the AutoCompleteTextView *using the soft keyboard* 1121 * and get new results; you are now in touch mode 1122 * - The selection comes back on the first item in the list, even though 1123 * the list is supposed to be in touch mode 1124 * 1125 * Using the soft keyboard triggers the touch mode change but that change 1126 * is propagated to our window only after the first list layout, therefore 1127 * after the list attempts to resurrect the selection. 1128 * 1129 * The trick to work around this issue is to pretend the list is in touch 1130 * mode when we know that the selection should not appear, that is when 1131 * we know the user moved the selection away from the list. 1132 * 1133 * This boolean is set to true whenever we explicitly hide the list's 1134 * selection and reset to false whenever we know the user moved the 1135 * selection back to the list. 1136 * 1137 * When this boolean is true, isInTouchMode() returns true, otherwise it 1138 * returns super.isInTouchMode(). 1139 */ 1140 private boolean mListSelectionHidden; 1141 1142 /** 1143 * True if this wrapper should fake focus. 1144 */ 1145 private boolean mHijackFocus; 1146 1147 /** 1148 * <p>Creates a new list view wrapper.</p> 1149 * 1150 * @param context this view's context 1151 */ DropDownListView(Context context, boolean hijackFocus)1152 public DropDownListView(Context context, boolean hijackFocus) { 1153 super(context, null, com.android.internal.R.attr.dropDownListViewStyle); 1154 mHijackFocus = hijackFocus; 1155 // TODO: Add an API to control this 1156 setCacheColorHint(0); // Transparent, since the background drawable could be anything. 1157 } 1158 1159 /** 1160 * <p>Avoids jarring scrolling effect by ensuring that list elements 1161 * made of a text view fit on a single line.</p> 1162 * 1163 * @param position the item index in the list to get a view for 1164 * @return the view for the specified item 1165 */ 1166 @Override obtainView(int position, boolean[] isScrap)1167 View obtainView(int position, boolean[] isScrap) { 1168 View view = super.obtainView(position, isScrap); 1169 1170 if (view instanceof TextView) { 1171 ((TextView) view).setHorizontallyScrolling(true); 1172 } 1173 1174 return view; 1175 } 1176 1177 @Override isInTouchMode()1178 public boolean isInTouchMode() { 1179 // WARNING: Please read the comment where mListSelectionHidden is declared 1180 return (mHijackFocus && mListSelectionHidden) || super.isInTouchMode(); 1181 } 1182 1183 /** 1184 * <p>Returns the focus state in the drop down.</p> 1185 * 1186 * @return true always if hijacking focus 1187 */ 1188 @Override hasWindowFocus()1189 public boolean hasWindowFocus() { 1190 return mHijackFocus || super.hasWindowFocus(); 1191 } 1192 1193 /** 1194 * <p>Returns the focus state in the drop down.</p> 1195 * 1196 * @return true always if hijacking focus 1197 */ 1198 @Override isFocused()1199 public boolean isFocused() { 1200 return mHijackFocus || super.isFocused(); 1201 } 1202 1203 /** 1204 * <p>Returns the focus state in the drop down.</p> 1205 * 1206 * @return true always if hijacking focus 1207 */ 1208 @Override hasFocus()1209 public boolean hasFocus() { 1210 return mHijackFocus || super.hasFocus(); 1211 } 1212 } 1213 1214 private class PopupDataSetObserver extends DataSetObserver { 1215 @Override onChanged()1216 public void onChanged() { 1217 if (isShowing()) { 1218 // Resize the popup to fit new content 1219 show(); 1220 } 1221 } 1222 1223 @Override onInvalidated()1224 public void onInvalidated() { 1225 dismiss(); 1226 } 1227 } 1228 1229 private class ListSelectorHider implements Runnable { run()1230 public void run() { 1231 clearListSelection(); 1232 } 1233 } 1234 1235 private class ResizePopupRunnable implements Runnable { run()1236 public void run() { 1237 if (mDropDownList != null && mDropDownList.getCount() > mDropDownList.getChildCount() && 1238 mDropDownList.getChildCount() <= mListItemExpandMaximum) { 1239 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); 1240 show(); 1241 } 1242 } 1243 } 1244 1245 private class PopupTouchInterceptor implements OnTouchListener { onTouch(View v, MotionEvent event)1246 public boolean onTouch(View v, MotionEvent event) { 1247 final int action = event.getAction(); 1248 final int x = (int) event.getX(); 1249 final int y = (int) event.getY(); 1250 1251 if (action == MotionEvent.ACTION_DOWN && 1252 mPopup != null && mPopup.isShowing() && 1253 (x >= 0 && x < mPopup.getWidth() && y >= 0 && y < mPopup.getHeight())) { 1254 mHandler.postDelayed(mResizePopupRunnable, EXPAND_LIST_TIMEOUT); 1255 } else if (action == MotionEvent.ACTION_UP) { 1256 mHandler.removeCallbacks(mResizePopupRunnable); 1257 } 1258 return false; 1259 } 1260 } 1261 1262 private class PopupScrollListener implements ListView.OnScrollListener { onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)1263 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 1264 int totalItemCount) { 1265 1266 } 1267 onScrollStateChanged(AbsListView view, int scrollState)1268 public void onScrollStateChanged(AbsListView view, int scrollState) { 1269 if (scrollState == SCROLL_STATE_TOUCH_SCROLL && 1270 !isInputMethodNotNeeded() && mPopup.getContentView() != null) { 1271 mHandler.removeCallbacks(mResizePopupRunnable); 1272 mResizePopupRunnable.run(); 1273 } 1274 } 1275 } 1276 } 1277