ListPopupWindow.java revision c8bfc68cddc15787943e7db1ba1cfeb26d4f74d3
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.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.animation.ObjectAnimator; 22import android.content.Context; 23import android.content.res.TypedArray; 24import android.database.DataSetObserver; 25import android.graphics.Rect; 26import android.graphics.drawable.Drawable; 27import android.os.Handler; 28import android.os.SystemClock; 29import android.text.TextUtils; 30import android.util.AttributeSet; 31import android.util.IntProperty; 32import android.util.Log; 33import android.view.Gravity; 34import android.view.KeyEvent; 35import android.view.MotionEvent; 36import android.view.View; 37import android.view.View.MeasureSpec; 38import android.view.View.OnTouchListener; 39import android.view.ViewConfiguration; 40import android.view.ViewGroup; 41import android.view.ViewParent; 42import android.view.WindowManager; 43import android.view.animation.AccelerateDecelerateInterpolator; 44 45import com.android.internal.R; 46import com.android.internal.widget.AutoScrollHelper.AbsListViewAutoScroller; 47 48import java.util.Locale; 49 50/** 51 * A ListPopupWindow anchors itself to a host view and displays a 52 * list of choices. 53 * 54 * <p>ListPopupWindow contains a number of tricky behaviors surrounding 55 * positioning, scrolling parents to fit the dropdown, interacting 56 * sanely with the IME if present, and others. 57 * 58 * @see android.widget.AutoCompleteTextView 59 * @see android.widget.Spinner 60 */ 61public class ListPopupWindow { 62 private static final String TAG = "ListPopupWindow"; 63 private static final boolean DEBUG = false; 64 65 /** 66 * This value controls the length of time that the user 67 * must leave a pointer down without scrolling to expand 68 * the autocomplete dropdown list to cover the IME. 69 */ 70 private static final int EXPAND_LIST_TIMEOUT = 250; 71 72 private Context mContext; 73 private PopupWindow mPopup; 74 private ListAdapter mAdapter; 75 private DropDownListView mDropDownList; 76 77 private int mDropDownHeight = ViewGroup.LayoutParams.WRAP_CONTENT; 78 private int mDropDownWidth = ViewGroup.LayoutParams.WRAP_CONTENT; 79 private int mDropDownHorizontalOffset; 80 private int mDropDownVerticalOffset; 81 private int mDropDownWindowLayoutType = WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL; 82 private boolean mDropDownVerticalOffsetSet; 83 84 private int mDropDownGravity = Gravity.NO_GRAVITY; 85 86 private boolean mDropDownAlwaysVisible = false; 87 private boolean mForceIgnoreOutsideTouch = false; 88 int mListItemExpandMaximum = Integer.MAX_VALUE; 89 90 private View mPromptView; 91 private int mPromptPosition = POSITION_PROMPT_ABOVE; 92 93 private DataSetObserver mObserver; 94 95 private View mDropDownAnchorView; 96 97 private Drawable mDropDownListHighlight; 98 99 private AdapterView.OnItemClickListener mItemClickListener; 100 private AdapterView.OnItemSelectedListener mItemSelectedListener; 101 102 private final ResizePopupRunnable mResizePopupRunnable = new ResizePopupRunnable(); 103 private final PopupTouchInterceptor mTouchInterceptor = new PopupTouchInterceptor(); 104 private final PopupScrollListener mScrollListener = new PopupScrollListener(); 105 private final ListSelectorHider mHideSelector = new ListSelectorHider(); 106 private Runnable mShowDropDownRunnable; 107 108 private Handler mHandler = new Handler(); 109 110 private Rect mTempRect = new Rect(); 111 112 private boolean mModal; 113 114 private int mLayoutDirection; 115 116 /** 117 * The provided prompt view should appear above list content. 118 * 119 * @see #setPromptPosition(int) 120 * @see #getPromptPosition() 121 * @see #setPromptView(View) 122 */ 123 public static final int POSITION_PROMPT_ABOVE = 0; 124 125 /** 126 * The provided prompt view should appear below list content. 127 * 128 * @see #setPromptPosition(int) 129 * @see #getPromptPosition() 130 * @see #setPromptView(View) 131 */ 132 public static final int POSITION_PROMPT_BELOW = 1; 133 134 /** 135 * Alias for {@link ViewGroup.LayoutParams#MATCH_PARENT}. 136 * If used to specify a popup width, the popup will match the width of the anchor view. 137 * If used to specify a popup height, the popup will fill available space. 138 */ 139 public static final int MATCH_PARENT = ViewGroup.LayoutParams.MATCH_PARENT; 140 141 /** 142 * Alias for {@link ViewGroup.LayoutParams#WRAP_CONTENT}. 143 * If used to specify a popup width, the popup will use the width of its content. 144 */ 145 public static final int WRAP_CONTENT = ViewGroup.LayoutParams.WRAP_CONTENT; 146 147 /** 148 * Mode for {@link #setInputMethodMode(int)}: the requirements for the 149 * input method should be based on the focusability of the popup. That is 150 * if it is focusable than it needs to work with the input method, else 151 * it doesn't. 152 */ 153 public static final int INPUT_METHOD_FROM_FOCUSABLE = PopupWindow.INPUT_METHOD_FROM_FOCUSABLE; 154 155 /** 156 * Mode for {@link #setInputMethodMode(int)}: this popup always needs to 157 * work with an input method, regardless of whether it is focusable. This 158 * means that it will always be displayed so that the user can also operate 159 * the input method while it is shown. 160 */ 161 public static final int INPUT_METHOD_NEEDED = PopupWindow.INPUT_METHOD_NEEDED; 162 163 /** 164 * Mode for {@link #setInputMethodMode(int)}: this popup never needs to 165 * work with an input method, regardless of whether it is focusable. This 166 * means that it will always be displayed to use as much space on the 167 * screen as needed, regardless of whether this covers the input method. 168 */ 169 public static final int INPUT_METHOD_NOT_NEEDED = PopupWindow.INPUT_METHOD_NOT_NEEDED; 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 */ 177 public ListPopupWindow(Context context) { 178 this(context, null, com.android.internal.R.attr.listPopupWindowStyle, 0); 179 } 180 181 /** 182 * Create a new, empty popup window capable of displaying items from a ListAdapter. 183 * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}. 184 * 185 * @param context Context used for contained views. 186 * @param attrs Attributes from inflating parent views used to style the popup. 187 */ 188 public ListPopupWindow(Context context, AttributeSet attrs) { 189 this(context, attrs, com.android.internal.R.attr.listPopupWindowStyle, 0); 190 } 191 192 /** 193 * Create a new, empty popup window capable of displaying items from a ListAdapter. 194 * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}. 195 * 196 * @param context Context used for contained views. 197 * @param attrs Attributes from inflating parent views used to style the popup. 198 * @param defStyleAttr Default style attribute to use for popup content. 199 */ 200 public ListPopupWindow(Context context, AttributeSet attrs, int defStyleAttr) { 201 this(context, attrs, defStyleAttr, 0); 202 } 203 204 /** 205 * Create a new, empty popup window capable of displaying items from a ListAdapter. 206 * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}. 207 * 208 * @param context Context used for contained views. 209 * @param attrs Attributes from inflating parent views used to style the popup. 210 * @param defStyleAttr Style attribute to read for default styling of popup content. 211 * @param defStyleRes Style resource ID to use for default styling of popup content. 212 */ 213 public ListPopupWindow(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 214 mContext = context; 215 216 final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ListPopupWindow, 217 defStyleAttr, defStyleRes); 218 mDropDownHorizontalOffset = a.getDimensionPixelOffset( 219 R.styleable.ListPopupWindow_dropDownHorizontalOffset, 0); 220 mDropDownVerticalOffset = a.getDimensionPixelOffset( 221 R.styleable.ListPopupWindow_dropDownVerticalOffset, 0); 222 if (mDropDownVerticalOffset != 0) { 223 mDropDownVerticalOffsetSet = true; 224 } 225 a.recycle(); 226 227 mPopup = new PopupWindow(context, attrs, defStyleAttr, defStyleRes); 228 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); 229 // Set the default layout direction to match the default locale one 230 final Locale locale = mContext.getResources().getConfiguration().locale; 231 mLayoutDirection = TextUtils.getLayoutDirectionFromLocale(locale); 232 } 233 234 /** 235 * Sets the adapter that provides the data and the views to represent the data 236 * in this popup window. 237 * 238 * @param adapter The adapter to use to create this window's content. 239 */ 240 public void setAdapter(ListAdapter adapter) { 241 if (mObserver == null) { 242 mObserver = new PopupDataSetObserver(); 243 } else if (mAdapter != null) { 244 mAdapter.unregisterDataSetObserver(mObserver); 245 } 246 mAdapter = adapter; 247 if (mAdapter != null) { 248 adapter.registerDataSetObserver(mObserver); 249 } 250 251 if (mDropDownList != null) { 252 mDropDownList.setAdapter(mAdapter); 253 } 254 } 255 256 /** 257 * Set where the optional prompt view should appear. The default is 258 * {@link #POSITION_PROMPT_ABOVE}. 259 * 260 * @param position A position constant declaring where the prompt should be displayed. 261 * 262 * @see #POSITION_PROMPT_ABOVE 263 * @see #POSITION_PROMPT_BELOW 264 */ 265 public void setPromptPosition(int position) { 266 mPromptPosition = position; 267 } 268 269 /** 270 * @return Where the optional prompt view should appear. 271 * 272 * @see #POSITION_PROMPT_ABOVE 273 * @see #POSITION_PROMPT_BELOW 274 */ 275 public int getPromptPosition() { 276 return mPromptPosition; 277 } 278 279 /** 280 * Set whether this window should be modal when shown. 281 * 282 * <p>If a popup window is modal, it will receive all touch and key input. 283 * If the user touches outside the popup window's content area the popup window 284 * will be dismissed. 285 * 286 * @param modal {@code true} if the popup window should be modal, {@code false} otherwise. 287 */ 288 public void setModal(boolean modal) { 289 mModal = modal; 290 mPopup.setFocusable(modal); 291 } 292 293 /** 294 * Returns whether the popup window will be modal when shown. 295 * 296 * @return {@code true} if the popup window will be modal, {@code false} otherwise. 297 */ 298 public boolean isModal() { 299 return mModal; 300 } 301 302 /** 303 * Forces outside touches to be ignored. Normally if {@link #isDropDownAlwaysVisible()} is 304 * false, we allow outside touch to dismiss the dropdown. If this is set to true, then we 305 * ignore outside touch even when the drop down is not set to always visible. 306 * 307 * @hide Used only by AutoCompleteTextView to handle some internal special cases. 308 */ 309 public void setForceIgnoreOutsideTouch(boolean forceIgnoreOutsideTouch) { 310 mForceIgnoreOutsideTouch = forceIgnoreOutsideTouch; 311 } 312 313 /** 314 * Sets whether the drop-down should remain visible under certain conditions. 315 * 316 * The drop-down will occupy the entire screen below {@link #getAnchorView} regardless 317 * of the size or content of the list. {@link #getBackground()} will fill any space 318 * that is not used by the list. 319 * 320 * @param dropDownAlwaysVisible Whether to keep the drop-down visible. 321 * 322 * @hide Only used by AutoCompleteTextView under special conditions. 323 */ 324 public void setDropDownAlwaysVisible(boolean dropDownAlwaysVisible) { 325 mDropDownAlwaysVisible = dropDownAlwaysVisible; 326 } 327 328 /** 329 * @return Whether the drop-down is visible under special conditions. 330 * 331 * @hide Only used by AutoCompleteTextView under special conditions. 332 */ 333 public boolean isDropDownAlwaysVisible() { 334 return mDropDownAlwaysVisible; 335 } 336 337 /** 338 * Sets the operating mode for the soft input area. 339 * 340 * @param mode The desired mode, see 341 * {@link android.view.WindowManager.LayoutParams#softInputMode} 342 * for the full list 343 * 344 * @see android.view.WindowManager.LayoutParams#softInputMode 345 * @see #getSoftInputMode() 346 */ 347 public void setSoftInputMode(int mode) { 348 mPopup.setSoftInputMode(mode); 349 } 350 351 /** 352 * Returns the current value in {@link #setSoftInputMode(int)}. 353 * 354 * @see #setSoftInputMode(int) 355 * @see android.view.WindowManager.LayoutParams#softInputMode 356 */ 357 public int getSoftInputMode() { 358 return mPopup.getSoftInputMode(); 359 } 360 361 /** 362 * Sets a drawable to use as the list item selector. 363 * 364 * @param selector List selector drawable to use in the popup. 365 */ 366 public void setListSelector(Drawable selector) { 367 mDropDownListHighlight = selector; 368 } 369 370 /** 371 * @return The background drawable for the popup window. 372 */ 373 public Drawable getBackground() { 374 return mPopup.getBackground(); 375 } 376 377 /** 378 * Sets a drawable to be the background for the popup window. 379 * 380 * @param d A drawable to set as the background. 381 */ 382 public void setBackgroundDrawable(Drawable d) { 383 mPopup.setBackgroundDrawable(d); 384 } 385 386 /** 387 * Set an animation style to use when the popup window is shown or dismissed. 388 * 389 * @param animationStyle Animation style to use. 390 */ 391 public void setAnimationStyle(int animationStyle) { 392 mPopup.setAnimationStyle(animationStyle); 393 } 394 395 /** 396 * Returns the animation style that will be used when the popup window is 397 * shown or dismissed. 398 * 399 * @return Animation style that will be used. 400 */ 401 public int getAnimationStyle() { 402 return mPopup.getAnimationStyle(); 403 } 404 405 /** 406 * Returns the view that will be used to anchor this popup. 407 * 408 * @return The popup's anchor view 409 */ 410 public View getAnchorView() { 411 return mDropDownAnchorView; 412 } 413 414 /** 415 * Sets the popup's anchor view. This popup will always be positioned relative to 416 * the anchor view when shown. 417 * 418 * @param anchor The view to use as an anchor. 419 */ 420 public void setAnchorView(View anchor) { 421 mDropDownAnchorView = anchor; 422 } 423 424 /** 425 * @return The horizontal offset of the popup from its anchor in pixels. 426 */ 427 public int getHorizontalOffset() { 428 return mDropDownHorizontalOffset; 429 } 430 431 /** 432 * Set the horizontal offset of this popup from its anchor view in pixels. 433 * 434 * @param offset The horizontal offset of the popup from its anchor. 435 */ 436 public void setHorizontalOffset(int offset) { 437 mDropDownHorizontalOffset = offset; 438 } 439 440 /** 441 * @return The vertical offset of the popup from its anchor in pixels. 442 */ 443 public int getVerticalOffset() { 444 if (!mDropDownVerticalOffsetSet) { 445 return 0; 446 } 447 return mDropDownVerticalOffset; 448 } 449 450 /** 451 * Set the vertical offset of this popup from its anchor view in pixels. 452 * 453 * @param offset The vertical offset of the popup from its anchor. 454 */ 455 public void setVerticalOffset(int offset) { 456 mDropDownVerticalOffset = offset; 457 mDropDownVerticalOffsetSet = true; 458 } 459 460 /** 461 * Set the gravity of the dropdown list. This is commonly used to 462 * set gravity to START or END for alignment with the anchor. 463 * 464 * @param gravity Gravity value to use 465 */ 466 public void setDropDownGravity(int gravity) { 467 mDropDownGravity = gravity; 468 } 469 470 /** 471 * @return The width of the popup window in pixels. 472 */ 473 public int getWidth() { 474 return mDropDownWidth; 475 } 476 477 /** 478 * Sets the width of the popup window in pixels. Can also be {@link #MATCH_PARENT} 479 * or {@link #WRAP_CONTENT}. 480 * 481 * @param width Width of the popup window. 482 */ 483 public void setWidth(int width) { 484 mDropDownWidth = width; 485 } 486 487 /** 488 * Sets the width of the popup window by the size of its content. The final width may be 489 * larger to accommodate styled window dressing. 490 * 491 * @param width Desired width of content in pixels. 492 */ 493 public void setContentWidth(int width) { 494 Drawable popupBackground = mPopup.getBackground(); 495 if (popupBackground != null) { 496 popupBackground.getPadding(mTempRect); 497 mDropDownWidth = mTempRect.left + mTempRect.right + width; 498 } else { 499 setWidth(width); 500 } 501 } 502 503 /** 504 * @return The height of the popup window in pixels. 505 */ 506 public int getHeight() { 507 return mDropDownHeight; 508 } 509 510 /** 511 * Sets the height of the popup window in pixels. Can also be {@link #MATCH_PARENT}. 512 * 513 * @param height Height of the popup window. 514 */ 515 public void setHeight(int height) { 516 mDropDownHeight = height; 517 } 518 519 /** 520 * Set the layout type for this popup window. 521 * <p> 522 * See {@link WindowManager.LayoutParams#type} for possible values. 523 * 524 * @param layoutType Layout type for this window. 525 * 526 * @see WindowManager.LayoutParams#type 527 */ 528 public void setWindowLayoutType(int layoutType) { 529 mDropDownWindowLayoutType = layoutType; 530 } 531 532 /** 533 * Sets a listener to receive events when a list item is clicked. 534 * 535 * @param clickListener Listener to register 536 * 537 * @see ListView#setOnItemClickListener(android.widget.AdapterView.OnItemClickListener) 538 */ 539 public void setOnItemClickListener(AdapterView.OnItemClickListener clickListener) { 540 mItemClickListener = clickListener; 541 } 542 543 /** 544 * Sets a listener to receive events when a list item is selected. 545 * 546 * @param selectedListener Listener to register. 547 * 548 * @see ListView#setOnItemSelectedListener(android.widget.AdapterView.OnItemSelectedListener) 549 */ 550 public void setOnItemSelectedListener(AdapterView.OnItemSelectedListener selectedListener) { 551 mItemSelectedListener = selectedListener; 552 } 553 554 /** 555 * Set a view to act as a user prompt for this popup window. Where the prompt view will appear 556 * is controlled by {@link #setPromptPosition(int)}. 557 * 558 * @param prompt View to use as an informational prompt. 559 */ 560 public void setPromptView(View prompt) { 561 boolean showing = isShowing(); 562 if (showing) { 563 removePromptView(); 564 } 565 mPromptView = prompt; 566 if (showing) { 567 show(); 568 } 569 } 570 571 /** 572 * Post a {@link #show()} call to the UI thread. 573 */ 574 public void postShow() { 575 mHandler.post(mShowDropDownRunnable); 576 } 577 578 /** 579 * Show the popup list. If the list is already showing, this method 580 * will recalculate the popup's size and position. 581 */ 582 public void show() { 583 int height = buildDropDown(); 584 585 final boolean noInputMethod = isInputMethodNotNeeded(); 586 mPopup.setAllowScrollingAnchorParent(!noInputMethod); 587 mPopup.setWindowLayoutType(mDropDownWindowLayoutType); 588 589 if (mPopup.isShowing()) { 590 final int widthSpec; 591 if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) { 592 // The call to PopupWindow's update method below can accept -1 for any 593 // value you do not want to update. 594 widthSpec = -1; 595 } else if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) { 596 widthSpec = getAnchorView().getWidth(); 597 } else { 598 widthSpec = mDropDownWidth; 599 } 600 601 final int heightSpec; 602 if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { 603 // The call to PopupWindow's update method below can accept -1 for any 604 // value you do not want to update. 605 heightSpec = noInputMethod ? height : ViewGroup.LayoutParams.MATCH_PARENT; 606 if (noInputMethod) { 607 mPopup.setWidth(mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ? 608 ViewGroup.LayoutParams.MATCH_PARENT : 0); 609 mPopup.setHeight(0); 610 } else { 611 mPopup.setWidth(mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ? 612 ViewGroup.LayoutParams.MATCH_PARENT : 0); 613 mPopup.setHeight(ViewGroup.LayoutParams.MATCH_PARENT); 614 } 615 } else if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { 616 heightSpec = height; 617 } else { 618 heightSpec = mDropDownHeight; 619 } 620 621 mPopup.setWidth(widthSpec); 622 mPopup.setHeight(heightSpec); 623 mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible); 624 625 mPopup.update(getAnchorView(), mDropDownHorizontalOffset, 626 mDropDownVerticalOffset, -1, -1); 627 } else { 628 final int widthSpec; 629 if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) { 630 widthSpec = ViewGroup.LayoutParams.MATCH_PARENT; 631 } else { 632 if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) { 633 widthSpec = getAnchorView().getWidth(); 634 } else { 635 widthSpec = mDropDownWidth; 636 } 637 } 638 639 final int heightSpec; 640 if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { 641 heightSpec = ViewGroup.LayoutParams.MATCH_PARENT; 642 } else { 643 if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { 644 heightSpec = height; 645 } else { 646 heightSpec = mDropDownHeight; 647 } 648 } 649 650 mPopup.setWidth(widthSpec); 651 mPopup.setHeight(heightSpec); 652 mPopup.setClipToScreenEnabled(true); 653 654 // use outside touchable to dismiss drop down when touching outside of it, so 655 // only set this if the dropdown is not always visible 656 mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible); 657 mPopup.setTouchInterceptor(mTouchInterceptor); 658 mPopup.showAsDropDown(getAnchorView(), mDropDownHorizontalOffset, 659 mDropDownVerticalOffset, mDropDownGravity); 660 mDropDownList.setSelection(ListView.INVALID_POSITION); 661 662 if (!mModal || mDropDownList.isInTouchMode()) { 663 clearListSelection(); 664 } 665 if (!mModal) { 666 mHandler.post(mHideSelector); 667 } 668 } 669 } 670 671 /** 672 * Dismiss the popup window. 673 */ 674 public void dismiss() { 675 mPopup.dismiss(); 676 removePromptView(); 677 mPopup.setContentView(null); 678 mDropDownList = null; 679 mHandler.removeCallbacks(mResizePopupRunnable); 680 } 681 682 /** 683 * Set a listener to receive a callback when the popup is dismissed. 684 * 685 * @param listener Listener that will be notified when the popup is dismissed. 686 */ 687 public void setOnDismissListener(PopupWindow.OnDismissListener listener) { 688 mPopup.setOnDismissListener(listener); 689 } 690 691 private void removePromptView() { 692 if (mPromptView != null) { 693 final ViewParent parent = mPromptView.getParent(); 694 if (parent instanceof ViewGroup) { 695 final ViewGroup group = (ViewGroup) parent; 696 group.removeView(mPromptView); 697 } 698 } 699 } 700 701 /** 702 * Control how the popup operates with an input method: one of 703 * {@link #INPUT_METHOD_FROM_FOCUSABLE}, {@link #INPUT_METHOD_NEEDED}, 704 * or {@link #INPUT_METHOD_NOT_NEEDED}. 705 * 706 * <p>If the popup is showing, calling this method will take effect only 707 * the next time the popup is shown or through a manual call to the {@link #show()} 708 * method.</p> 709 * 710 * @see #getInputMethodMode() 711 * @see #show() 712 */ 713 public void setInputMethodMode(int mode) { 714 mPopup.setInputMethodMode(mode); 715 } 716 717 /** 718 * Return the current value in {@link #setInputMethodMode(int)}. 719 * 720 * @see #setInputMethodMode(int) 721 */ 722 public int getInputMethodMode() { 723 return mPopup.getInputMethodMode(); 724 } 725 726 /** 727 * Set the selected position of the list. 728 * Only valid when {@link #isShowing()} == {@code true}. 729 * 730 * @param position List position to set as selected. 731 */ 732 public void setSelection(int position) { 733 DropDownListView list = mDropDownList; 734 if (isShowing() && list != null) { 735 list.mListSelectionHidden = false; 736 list.setSelection(position); 737 if (list.getChoiceMode() != ListView.CHOICE_MODE_NONE) { 738 list.setItemChecked(position, true); 739 } 740 } 741 } 742 743 /** 744 * Clear any current list selection. 745 * Only valid when {@link #isShowing()} == {@code true}. 746 */ 747 public void clearListSelection() { 748 final DropDownListView list = mDropDownList; 749 if (list != null) { 750 // WARNING: Please read the comment where mListSelectionHidden is declared 751 list.mListSelectionHidden = true; 752 list.hideSelector(); 753 list.requestLayout(); 754 } 755 } 756 757 /** 758 * @return {@code true} if the popup is currently showing, {@code false} otherwise. 759 */ 760 public boolean isShowing() { 761 return mPopup.isShowing(); 762 } 763 764 /** 765 * @return {@code true} if this popup is configured to assume the user does not need 766 * to interact with the IME while it is showing, {@code false} otherwise. 767 */ 768 public boolean isInputMethodNotNeeded() { 769 return mPopup.getInputMethodMode() == INPUT_METHOD_NOT_NEEDED; 770 } 771 772 /** 773 * Perform an item click operation on the specified list adapter position. 774 * 775 * @param position Adapter position for performing the click 776 * @return true if the click action could be performed, false if not. 777 * (e.g. if the popup was not showing, this method would return false.) 778 */ 779 public boolean performItemClick(int position) { 780 if (isShowing()) { 781 if (mItemClickListener != null) { 782 final DropDownListView list = mDropDownList; 783 final View child = list.getChildAt(position - list.getFirstVisiblePosition()); 784 final ListAdapter adapter = list.getAdapter(); 785 mItemClickListener.onItemClick(list, child, position, adapter.getItemId(position)); 786 } 787 return true; 788 } 789 return false; 790 } 791 792 /** 793 * @return The currently selected item or null if the popup is not showing. 794 */ 795 public Object getSelectedItem() { 796 if (!isShowing()) { 797 return null; 798 } 799 return mDropDownList.getSelectedItem(); 800 } 801 802 /** 803 * @return The position of the currently selected item or {@link ListView#INVALID_POSITION} 804 * if {@link #isShowing()} == {@code false}. 805 * 806 * @see ListView#getSelectedItemPosition() 807 */ 808 public int getSelectedItemPosition() { 809 if (!isShowing()) { 810 return ListView.INVALID_POSITION; 811 } 812 return mDropDownList.getSelectedItemPosition(); 813 } 814 815 /** 816 * @return The ID of the currently selected item or {@link ListView#INVALID_ROW_ID} 817 * if {@link #isShowing()} == {@code false}. 818 * 819 * @see ListView#getSelectedItemId() 820 */ 821 public long getSelectedItemId() { 822 if (!isShowing()) { 823 return ListView.INVALID_ROW_ID; 824 } 825 return mDropDownList.getSelectedItemId(); 826 } 827 828 /** 829 * @return The View for the currently selected item or null if 830 * {@link #isShowing()} == {@code false}. 831 * 832 * @see ListView#getSelectedView() 833 */ 834 public View getSelectedView() { 835 if (!isShowing()) { 836 return null; 837 } 838 return mDropDownList.getSelectedView(); 839 } 840 841 /** 842 * @return The {@link ListView} displayed within the popup window. 843 * Only valid when {@link #isShowing()} == {@code true}. 844 */ 845 public ListView getListView() { 846 return mDropDownList; 847 } 848 849 /** 850 * The maximum number of list items that can be visible and still have 851 * the list expand when touched. 852 * 853 * @param max Max number of items that can be visible and still allow the list to expand. 854 */ 855 void setListItemExpandMax(int max) { 856 mListItemExpandMaximum = max; 857 } 858 859 /** 860 * Filter key down events. By forwarding key down events to this function, 861 * views using non-modal ListPopupWindow can have it handle key selection of items. 862 * 863 * @param keyCode keyCode param passed to the host view's onKeyDown 864 * @param event event param passed to the host view's onKeyDown 865 * @return true if the event was handled, false if it was ignored. 866 * 867 * @see #setModal(boolean) 868 */ 869 public boolean onKeyDown(int keyCode, KeyEvent event) { 870 // when the drop down is shown, we drive it directly 871 if (isShowing()) { 872 // the key events are forwarded to the list in the drop down view 873 // note that ListView handles space but we don't want that to happen 874 // also if selection is not currently in the drop down, then don't 875 // let center or enter presses go there since that would cause it 876 // to select one of its items 877 if (keyCode != KeyEvent.KEYCODE_SPACE 878 && (mDropDownList.getSelectedItemPosition() >= 0 879 || !KeyEvent.isConfirmKey(keyCode))) { 880 int curIndex = mDropDownList.getSelectedItemPosition(); 881 boolean consumed; 882 883 final boolean below = !mPopup.isAboveAnchor(); 884 885 final ListAdapter adapter = mAdapter; 886 887 boolean allEnabled; 888 int firstItem = Integer.MAX_VALUE; 889 int lastItem = Integer.MIN_VALUE; 890 891 if (adapter != null) { 892 allEnabled = adapter.areAllItemsEnabled(); 893 firstItem = allEnabled ? 0 : 894 mDropDownList.lookForSelectablePosition(0, true); 895 lastItem = allEnabled ? adapter.getCount() - 1 : 896 mDropDownList.lookForSelectablePosition(adapter.getCount() - 1, false); 897 } 898 899 if ((below && keyCode == KeyEvent.KEYCODE_DPAD_UP && curIndex <= firstItem) || 900 (!below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN && curIndex >= lastItem)) { 901 // When the selection is at the top, we block the key 902 // event to prevent focus from moving. 903 clearListSelection(); 904 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); 905 show(); 906 return true; 907 } else { 908 // WARNING: Please read the comment where mListSelectionHidden 909 // is declared 910 mDropDownList.mListSelectionHidden = false; 911 } 912 913 consumed = mDropDownList.onKeyDown(keyCode, event); 914 if (DEBUG) Log.v(TAG, "Key down: code=" + keyCode + " list consumed=" + consumed); 915 916 if (consumed) { 917 // If it handled the key event, then the user is 918 // navigating in the list, so we should put it in front. 919 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); 920 // Here's a little trick we need to do to make sure that 921 // the list view is actually showing its focus indicator, 922 // by ensuring it has focus and getting its window out 923 // of touch mode. 924 mDropDownList.requestFocusFromTouch(); 925 show(); 926 927 switch (keyCode) { 928 // avoid passing the focus from the text view to the 929 // next component 930 case KeyEvent.KEYCODE_ENTER: 931 case KeyEvent.KEYCODE_DPAD_CENTER: 932 case KeyEvent.KEYCODE_DPAD_DOWN: 933 case KeyEvent.KEYCODE_DPAD_UP: 934 return true; 935 } 936 } else { 937 if (below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { 938 // when the selection is at the bottom, we block the 939 // event to avoid going to the next focusable widget 940 if (curIndex == lastItem) { 941 return true; 942 } 943 } else if (!below && keyCode == KeyEvent.KEYCODE_DPAD_UP && 944 curIndex == firstItem) { 945 return true; 946 } 947 } 948 } 949 } 950 951 return false; 952 } 953 954 /** 955 * Filter key down events. By forwarding key up events to this function, 956 * views using non-modal ListPopupWindow can have it handle key selection of items. 957 * 958 * @param keyCode keyCode param passed to the host view's onKeyUp 959 * @param event event param passed to the host view's onKeyUp 960 * @return true if the event was handled, false if it was ignored. 961 * 962 * @see #setModal(boolean) 963 */ 964 public boolean onKeyUp(int keyCode, KeyEvent event) { 965 if (isShowing() && mDropDownList.getSelectedItemPosition() >= 0) { 966 boolean consumed = mDropDownList.onKeyUp(keyCode, event); 967 if (consumed && KeyEvent.isConfirmKey(keyCode)) { 968 // if the list accepts the key events and the key event was a click, the text view 969 // gets the selected item from the drop down as its content 970 dismiss(); 971 } 972 return consumed; 973 } 974 return false; 975 } 976 977 /** 978 * Filter pre-IME key events. By forwarding {@link View#onKeyPreIme(int, KeyEvent)} 979 * events to this function, views using ListPopupWindow can have it dismiss the popup 980 * when the back key is pressed. 981 * 982 * @param keyCode keyCode param passed to the host view's onKeyPreIme 983 * @param event event param passed to the host view's onKeyPreIme 984 * @return true if the event was handled, false if it was ignored. 985 * 986 * @see #setModal(boolean) 987 */ 988 public boolean onKeyPreIme(int keyCode, KeyEvent event) { 989 if (keyCode == KeyEvent.KEYCODE_BACK && isShowing()) { 990 // special case for the back key, we do not even try to send it 991 // to the drop down list but instead, consume it immediately 992 final View anchorView = mDropDownAnchorView; 993 if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { 994 KeyEvent.DispatcherState state = anchorView.getKeyDispatcherState(); 995 if (state != null) { 996 state.startTracking(event, this); 997 } 998 return true; 999 } else if (event.getAction() == KeyEvent.ACTION_UP) { 1000 KeyEvent.DispatcherState state = anchorView.getKeyDispatcherState(); 1001 if (state != null) { 1002 state.handleUpEvent(event); 1003 } 1004 if (event.isTracking() && !event.isCanceled()) { 1005 dismiss(); 1006 return true; 1007 } 1008 } 1009 } 1010 return false; 1011 } 1012 1013 /** 1014 * Returns an {@link OnTouchListener} that can be added to the source view 1015 * to implement drag-to-open behavior. Generally, the source view should be 1016 * the same view that was passed to {@link #setAnchorView}. 1017 * <p> 1018 * When the listener is set on a view, touching that view and dragging 1019 * outside of its bounds will open the popup window. Lifting will select the 1020 * currently touched list item. 1021 * <p> 1022 * Example usage: 1023 * <pre> 1024 * ListPopupWindow myPopup = new ListPopupWindow(context); 1025 * myPopup.setAnchor(myAnchor); 1026 * OnTouchListener dragListener = myPopup.createDragToOpenListener(myAnchor); 1027 * myAnchor.setOnTouchListener(dragListener); 1028 * </pre> 1029 * 1030 * @param src the view on which the resulting listener will be set 1031 * @return a touch listener that controls drag-to-open behavior 1032 */ 1033 public OnTouchListener createDragToOpenListener(View src) { 1034 return new ForwardingListener(src) { 1035 @Override 1036 public ListPopupWindow getPopup() { 1037 return ListPopupWindow.this; 1038 } 1039 }; 1040 } 1041 1042 /** 1043 * <p>Builds the popup window's content and returns the height the popup 1044 * should have. Returns -1 when the content already exists.</p> 1045 * 1046 * @return the content's height or -1 if content already exists 1047 */ 1048 private int buildDropDown() { 1049 ViewGroup dropDownView; 1050 int otherHeights = 0; 1051 1052 if (mDropDownList == null) { 1053 Context context = mContext; 1054 1055 /** 1056 * This Runnable exists for the sole purpose of checking if the view layout has got 1057 * completed and if so call showDropDown to display the drop down. This is used to show 1058 * the drop down as soon as possible after user opens up the search dialog, without 1059 * waiting for the normal UI pipeline to do it's job which is slower than this method. 1060 */ 1061 mShowDropDownRunnable = new Runnable() { 1062 public void run() { 1063 // View layout should be all done before displaying the drop down. 1064 View view = getAnchorView(); 1065 if (view != null && view.getWindowToken() != null) { 1066 show(); 1067 } 1068 } 1069 }; 1070 1071 mDropDownList = new DropDownListView(context, !mModal); 1072 if (mDropDownListHighlight != null) { 1073 mDropDownList.setSelector(mDropDownListHighlight); 1074 } 1075 mDropDownList.setAdapter(mAdapter); 1076 mDropDownList.setOnItemClickListener(mItemClickListener); 1077 mDropDownList.setFocusable(true); 1078 mDropDownList.setFocusableInTouchMode(true); 1079 mDropDownList.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 1080 public void onItemSelected(AdapterView<?> parent, View view, 1081 int position, long id) { 1082 1083 if (position != -1) { 1084 DropDownListView dropDownList = mDropDownList; 1085 1086 if (dropDownList != null) { 1087 dropDownList.mListSelectionHidden = false; 1088 } 1089 } 1090 } 1091 1092 public void onNothingSelected(AdapterView<?> parent) { 1093 } 1094 }); 1095 mDropDownList.setOnScrollListener(mScrollListener); 1096 1097 if (mItemSelectedListener != null) { 1098 mDropDownList.setOnItemSelectedListener(mItemSelectedListener); 1099 } 1100 1101 dropDownView = mDropDownList; 1102 1103 View hintView = mPromptView; 1104 if (hintView != null) { 1105 // if a hint has been specified, we accomodate more space for it and 1106 // add a text view in the drop down menu, at the bottom of the list 1107 LinearLayout hintContainer = new LinearLayout(context); 1108 hintContainer.setOrientation(LinearLayout.VERTICAL); 1109 1110 LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams( 1111 ViewGroup.LayoutParams.MATCH_PARENT, 0, 1.0f 1112 ); 1113 1114 switch (mPromptPosition) { 1115 case POSITION_PROMPT_BELOW: 1116 hintContainer.addView(dropDownView, hintParams); 1117 hintContainer.addView(hintView); 1118 break; 1119 1120 case POSITION_PROMPT_ABOVE: 1121 hintContainer.addView(hintView); 1122 hintContainer.addView(dropDownView, hintParams); 1123 break; 1124 1125 default: 1126 Log.e(TAG, "Invalid hint position " + mPromptPosition); 1127 break; 1128 } 1129 1130 // measure the hint's height to find how much more vertical space 1131 // we need to add to the drop down's height 1132 int widthSpec = MeasureSpec.makeMeasureSpec(mDropDownWidth, MeasureSpec.AT_MOST); 1133 int heightSpec = MeasureSpec.UNSPECIFIED; 1134 hintView.measure(widthSpec, heightSpec); 1135 1136 hintParams = (LinearLayout.LayoutParams) hintView.getLayoutParams(); 1137 otherHeights = hintView.getMeasuredHeight() + hintParams.topMargin 1138 + hintParams.bottomMargin; 1139 1140 dropDownView = hintContainer; 1141 } 1142 1143 mPopup.setContentView(dropDownView); 1144 } else { 1145 dropDownView = (ViewGroup) mPopup.getContentView(); 1146 final View view = mPromptView; 1147 if (view != null) { 1148 LinearLayout.LayoutParams hintParams = 1149 (LinearLayout.LayoutParams) view.getLayoutParams(); 1150 otherHeights = view.getMeasuredHeight() + hintParams.topMargin 1151 + hintParams.bottomMargin; 1152 } 1153 } 1154 1155 // getMaxAvailableHeight() subtracts the padding, so we put it back 1156 // to get the available height for the whole window 1157 int padding = 0; 1158 Drawable background = mPopup.getBackground(); 1159 if (background != null) { 1160 background.getPadding(mTempRect); 1161 padding = mTempRect.top + mTempRect.bottom; 1162 1163 // If we don't have an explicit vertical offset, determine one from the window 1164 // background so that content will line up. 1165 if (!mDropDownVerticalOffsetSet) { 1166 mDropDownVerticalOffset = -mTempRect.top; 1167 } 1168 } else { 1169 mTempRect.setEmpty(); 1170 } 1171 1172 // Max height available on the screen for a popup. 1173 boolean ignoreBottomDecorations = 1174 mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED; 1175 final int maxHeight = mPopup.getMaxAvailableHeight( 1176 getAnchorView(), mDropDownVerticalOffset, ignoreBottomDecorations); 1177 1178 if (mDropDownAlwaysVisible || mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { 1179 return maxHeight + padding; 1180 } 1181 1182 final int childWidthSpec; 1183 switch (mDropDownWidth) { 1184 case ViewGroup.LayoutParams.WRAP_CONTENT: 1185 childWidthSpec = MeasureSpec.makeMeasureSpec( 1186 mContext.getResources().getDisplayMetrics().widthPixels - 1187 (mTempRect.left + mTempRect.right), 1188 MeasureSpec.AT_MOST); 1189 break; 1190 case ViewGroup.LayoutParams.MATCH_PARENT: 1191 childWidthSpec = MeasureSpec.makeMeasureSpec( 1192 mContext.getResources().getDisplayMetrics().widthPixels - 1193 (mTempRect.left + mTempRect.right), 1194 MeasureSpec.EXACTLY); 1195 break; 1196 default: 1197 childWidthSpec = MeasureSpec.makeMeasureSpec(mDropDownWidth, MeasureSpec.EXACTLY); 1198 break; 1199 } 1200 final int listContent = mDropDownList.measureHeightOfChildren(childWidthSpec, 1201 0, ListView.NO_POSITION, maxHeight - otherHeights, -1); 1202 // add padding only if the list has items in it, that way we don't show 1203 // the popup if it is not needed 1204 if (listContent > 0) otherHeights += padding; 1205 1206 return listContent + otherHeights; 1207 } 1208 1209 /** 1210 * Abstract class that forwards touch events to a {@link ListPopupWindow}. 1211 * 1212 * @hide 1213 */ 1214 public static abstract class ForwardingListener 1215 implements View.OnTouchListener, View.OnAttachStateChangeListener { 1216 /** Scaled touch slop, used for detecting movement outside bounds. */ 1217 private final float mScaledTouchSlop; 1218 1219 /** Timeout before disallowing intercept on the source's parent. */ 1220 private final int mTapTimeout; 1221 1222 /** Timeout before accepting a long-press to start forwarding. */ 1223 private final int mLongPressTimeout; 1224 1225 /** Source view from which events are forwarded. */ 1226 private final View mSrc; 1227 1228 /** Runnable used to prevent conflicts with scrolling parents. */ 1229 private Runnable mDisallowIntercept; 1230 1231 /** Runnable used to trigger forwarding on long-press. */ 1232 private Runnable mTriggerLongPress; 1233 1234 /** Whether this listener is currently forwarding touch events. */ 1235 private boolean mForwarding; 1236 1237 /** 1238 * Whether forwarding was initiated by a long-press. If so, we won't 1239 * force the window to dismiss when the touch stream ends. 1240 */ 1241 private boolean mWasLongPress; 1242 1243 /** The id of the first pointer down in the current event stream. */ 1244 private int mActivePointerId; 1245 1246 public ForwardingListener(View src) { 1247 mSrc = src; 1248 mScaledTouchSlop = ViewConfiguration.get(src.getContext()).getScaledTouchSlop(); 1249 mTapTimeout = ViewConfiguration.getTapTimeout(); 1250 1251 // Use a medium-press timeout. Halfway between tap and long-press. 1252 mLongPressTimeout = (mTapTimeout + ViewConfiguration.getLongPressTimeout()) / 2; 1253 1254 src.addOnAttachStateChangeListener(this); 1255 } 1256 1257 /** 1258 * Returns the popup to which this listener is forwarding events. 1259 * <p> 1260 * Override this to return the correct popup. If the popup is displayed 1261 * asynchronously, you may also need to override 1262 * {@link #onForwardingStopped} to prevent premature cancelation of 1263 * forwarding. 1264 * 1265 * @return the popup to which this listener is forwarding events 1266 */ 1267 public abstract ListPopupWindow getPopup(); 1268 1269 @Override 1270 public boolean onTouch(View v, MotionEvent event) { 1271 final boolean wasForwarding = mForwarding; 1272 final boolean forwarding; 1273 if (wasForwarding) { 1274 forwarding = onTouchForwarded(event) || !onForwardingStopped(); 1275 } else { 1276 forwarding = onTouchObserved(event) && onForwardingStarted(); 1277 1278 if (forwarding) { 1279 // Make sure we cancel any ongoing source event stream. 1280 final long now = SystemClock.uptimeMillis(); 1281 final MotionEvent e = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 1282 0.0f, 0.0f, 0); 1283 mSrc.onTouchEvent(e); 1284 e.recycle(); 1285 } 1286 } 1287 1288 mForwarding = forwarding; 1289 return forwarding || wasForwarding; 1290 } 1291 1292 @Override 1293 public void onViewAttachedToWindow(View v) { 1294 } 1295 1296 @Override 1297 public void onViewDetachedFromWindow(View v) { 1298 mForwarding = false; 1299 mActivePointerId = MotionEvent.INVALID_POINTER_ID; 1300 1301 if (mDisallowIntercept != null) { 1302 mSrc.removeCallbacks(mDisallowIntercept); 1303 } 1304 } 1305 1306 /** 1307 * Called when forwarding would like to start. 1308 * <p> 1309 * By default, this will show the popup returned by {@link #getPopup()}. 1310 * It may be overridden to perform another action, like clicking the 1311 * source view or preparing the popup before showing it. 1312 * 1313 * @return true to start forwarding, false otherwise 1314 */ 1315 protected boolean onForwardingStarted() { 1316 final ListPopupWindow popup = getPopup(); 1317 if (popup != null && !popup.isShowing()) { 1318 popup.show(); 1319 } 1320 return true; 1321 } 1322 1323 /** 1324 * Called when forwarding would like to stop. 1325 * <p> 1326 * By default, this will dismiss the popup returned by 1327 * {@link #getPopup()}. It may be overridden to perform some other 1328 * action. 1329 * 1330 * @return true to stop forwarding, false otherwise 1331 */ 1332 protected boolean onForwardingStopped() { 1333 final ListPopupWindow popup = getPopup(); 1334 if (popup != null && popup.isShowing()) { 1335 popup.dismiss(); 1336 } 1337 return true; 1338 } 1339 1340 /** 1341 * Observes motion events and determines when to start forwarding. 1342 * 1343 * @param srcEvent motion event in source view coordinates 1344 * @return true to start forwarding motion events, false otherwise 1345 */ 1346 private boolean onTouchObserved(MotionEvent srcEvent) { 1347 final View src = mSrc; 1348 if (!src.isEnabled()) { 1349 return false; 1350 } 1351 1352 final int actionMasked = srcEvent.getActionMasked(); 1353 switch (actionMasked) { 1354 case MotionEvent.ACTION_DOWN: 1355 mActivePointerId = srcEvent.getPointerId(0); 1356 mWasLongPress = false; 1357 1358 if (mDisallowIntercept == null) { 1359 mDisallowIntercept = new DisallowIntercept(); 1360 } 1361 src.postDelayed(mDisallowIntercept, mTapTimeout); 1362 1363 if (mTriggerLongPress == null) { 1364 mTriggerLongPress = new TriggerLongPress(); 1365 } 1366 src.postDelayed(mTriggerLongPress, mLongPressTimeout); 1367 break; 1368 case MotionEvent.ACTION_MOVE: 1369 final int activePointerIndex = srcEvent.findPointerIndex(mActivePointerId); 1370 if (activePointerIndex >= 0) { 1371 final float x = srcEvent.getX(activePointerIndex); 1372 final float y = srcEvent.getY(activePointerIndex); 1373 1374 // Has the pointer has moved outside of the view? 1375 if (!src.pointInView(x, y, mScaledTouchSlop)) { 1376 clearCallbacks(); 1377 1378 // Don't let the parent intercept our events. 1379 src.getParent().requestDisallowInterceptTouchEvent(true); 1380 return true; 1381 } 1382 } 1383 break; 1384 case MotionEvent.ACTION_CANCEL: 1385 case MotionEvent.ACTION_UP: 1386 clearCallbacks(); 1387 break; 1388 } 1389 1390 return false; 1391 } 1392 1393 private void clearCallbacks() { 1394 if (mTriggerLongPress != null) { 1395 mSrc.removeCallbacks(mTriggerLongPress); 1396 } 1397 1398 if (mDisallowIntercept != null) { 1399 mSrc.removeCallbacks(mDisallowIntercept); 1400 } 1401 } 1402 1403 private void onLongPress() { 1404 clearCallbacks(); 1405 1406 final View src = mSrc; 1407 if (!src.isEnabled() || src.isLongClickable()) { 1408 // Ignore long-press if the view is disabled or has its own 1409 // handler. 1410 return; 1411 } 1412 1413 if (!onForwardingStarted()) { 1414 return; 1415 } 1416 1417 // Don't let the parent intercept our events. 1418 src.getParent().requestDisallowInterceptTouchEvent(true); 1419 1420 // Make sure we cancel any ongoing source event stream. 1421 final long now = SystemClock.uptimeMillis(); 1422 final MotionEvent e = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0); 1423 src.onTouchEvent(e); 1424 e.recycle(); 1425 1426 mForwarding = true; 1427 mWasLongPress = true; 1428 } 1429 1430 /** 1431 * Handled forwarded motion events and determines when to stop 1432 * forwarding. 1433 * 1434 * @param srcEvent motion event in source view coordinates 1435 * @return true to continue forwarding motion events, false to cancel 1436 */ 1437 private boolean onTouchForwarded(MotionEvent srcEvent) { 1438 final View src = mSrc; 1439 final ListPopupWindow popup = getPopup(); 1440 if (popup == null || !popup.isShowing()) { 1441 return false; 1442 } 1443 1444 final DropDownListView dst = popup.mDropDownList; 1445 if (dst == null || !dst.isShown()) { 1446 return false; 1447 } 1448 1449 // Convert event to destination-local coordinates. 1450 final MotionEvent dstEvent = MotionEvent.obtainNoHistory(srcEvent); 1451 src.toGlobalMotionEvent(dstEvent); 1452 dst.toLocalMotionEvent(dstEvent); 1453 1454 // Forward converted event to destination view, then recycle it. 1455 final boolean handled = dst.onForwardedEvent(dstEvent, mActivePointerId); 1456 dstEvent.recycle(); 1457 1458 // Always cancel forwarding when the touch stream ends. 1459 final int action = srcEvent.getActionMasked(); 1460 final boolean keepForwarding = action != MotionEvent.ACTION_UP 1461 && action != MotionEvent.ACTION_CANCEL; 1462 1463 return handled && keepForwarding; 1464 } 1465 1466 private class DisallowIntercept implements Runnable { 1467 @Override 1468 public void run() { 1469 final ViewParent parent = mSrc.getParent(); 1470 parent.requestDisallowInterceptTouchEvent(true); 1471 } 1472 } 1473 1474 private class TriggerLongPress implements Runnable { 1475 @Override 1476 public void run() { 1477 onLongPress(); 1478 } 1479 } 1480 } 1481 1482 /** 1483 * <p>Wrapper class for a ListView. This wrapper can hijack the focus to 1484 * make sure the list uses the appropriate drawables and states when 1485 * displayed on screen within a drop down. The focus is never actually 1486 * passed to the drop down in this mode; the list only looks focused.</p> 1487 */ 1488 private static class DropDownListView extends ListView { 1489 /** Duration in milliseconds of the drag-to-open click animation. */ 1490 private static final long CLICK_ANIM_DURATION = 150; 1491 1492 /** Target alpha value for drag-to-open click animation. */ 1493 private static final int CLICK_ANIM_ALPHA = 0x80; 1494 1495 /** Wrapper around Drawable's <code>alpha</code> property. */ 1496 private static final IntProperty<Drawable> DRAWABLE_ALPHA = 1497 new IntProperty<Drawable>("alpha") { 1498 @Override 1499 public void setValue(Drawable object, int value) { 1500 object.setAlpha(value); 1501 } 1502 1503 @Override 1504 public Integer get(Drawable object) { 1505 return object.getAlpha(); 1506 } 1507 }; 1508 1509 /* 1510 * WARNING: This is a workaround for a touch mode issue. 1511 * 1512 * Touch mode is propagated lazily to windows. This causes problems in 1513 * the following scenario: 1514 * - Type something in the AutoCompleteTextView and get some results 1515 * - Move down with the d-pad to select an item in the list 1516 * - Move up with the d-pad until the selection disappears 1517 * - Type more text in the AutoCompleteTextView *using the soft keyboard* 1518 * and get new results; you are now in touch mode 1519 * - The selection comes back on the first item in the list, even though 1520 * the list is supposed to be in touch mode 1521 * 1522 * Using the soft keyboard triggers the touch mode change but that change 1523 * is propagated to our window only after the first list layout, therefore 1524 * after the list attempts to resurrect the selection. 1525 * 1526 * The trick to work around this issue is to pretend the list is in touch 1527 * mode when we know that the selection should not appear, that is when 1528 * we know the user moved the selection away from the list. 1529 * 1530 * This boolean is set to true whenever we explicitly hide the list's 1531 * selection and reset to false whenever we know the user moved the 1532 * selection back to the list. 1533 * 1534 * When this boolean is true, isInTouchMode() returns true, otherwise it 1535 * returns super.isInTouchMode(). 1536 */ 1537 private boolean mListSelectionHidden; 1538 1539 /** 1540 * True if this wrapper should fake focus. 1541 */ 1542 private boolean mHijackFocus; 1543 1544 /** Whether to force drawing of the pressed state selector. */ 1545 private boolean mDrawsInPressedState; 1546 1547 /** Current drag-to-open click animation, if any. */ 1548 private Animator mClickAnimation; 1549 1550 /** Helper for drag-to-open auto scrolling. */ 1551 private AbsListViewAutoScroller mScrollHelper; 1552 1553 /** 1554 * <p>Creates a new list view wrapper.</p> 1555 * 1556 * @param context this view's context 1557 */ 1558 public DropDownListView(Context context, boolean hijackFocus) { 1559 super(context, null, com.android.internal.R.attr.dropDownListViewStyle); 1560 mHijackFocus = hijackFocus; 1561 // TODO: Add an API to control this 1562 setCacheColorHint(0); // Transparent, since the background drawable could be anything. 1563 } 1564 1565 /** 1566 * Handles forwarded events. 1567 * 1568 * @param activePointerId id of the pointer that activated forwarding 1569 * @return whether the event was handled 1570 */ 1571 public boolean onForwardedEvent(MotionEvent event, int activePointerId) { 1572 boolean handledEvent = true; 1573 boolean clearPressedItem = false; 1574 1575 final int actionMasked = event.getActionMasked(); 1576 switch (actionMasked) { 1577 case MotionEvent.ACTION_CANCEL: 1578 handledEvent = false; 1579 break; 1580 case MotionEvent.ACTION_UP: 1581 handledEvent = false; 1582 // $FALL-THROUGH$ 1583 case MotionEvent.ACTION_MOVE: 1584 final int activeIndex = event.findPointerIndex(activePointerId); 1585 if (activeIndex < 0) { 1586 handledEvent = false; 1587 break; 1588 } 1589 1590 final int x = (int) event.getX(activeIndex); 1591 final int y = (int) event.getY(activeIndex); 1592 final int position = pointToPosition(x, y); 1593 if (position == INVALID_POSITION) { 1594 clearPressedItem = true; 1595 break; 1596 } 1597 1598 final View child = getChildAt(position - getFirstVisiblePosition()); 1599 setPressedItem(child, position, x, y); 1600 handledEvent = true; 1601 1602 if (actionMasked == MotionEvent.ACTION_UP) { 1603 clickPressedItem(child, position); 1604 } 1605 break; 1606 } 1607 1608 // Failure to handle the event cancels forwarding. 1609 if (!handledEvent || clearPressedItem) { 1610 clearPressedItem(); 1611 } 1612 1613 // Manage automatic scrolling. 1614 if (handledEvent) { 1615 if (mScrollHelper == null) { 1616 mScrollHelper = new AbsListViewAutoScroller(this); 1617 } 1618 mScrollHelper.setEnabled(true); 1619 mScrollHelper.onTouch(this, event); 1620 } else if (mScrollHelper != null) { 1621 mScrollHelper.setEnabled(false); 1622 } 1623 1624 return handledEvent; 1625 } 1626 1627 /** 1628 * Starts an alpha animation on the selector. When the animation ends, 1629 * the list performs a click on the item. 1630 */ 1631 private void clickPressedItem(final View child, final int position) { 1632 final long id = getItemIdAtPosition(position); 1633 final Animator anim = ObjectAnimator.ofInt( 1634 mSelector, DRAWABLE_ALPHA, 0xFF, CLICK_ANIM_ALPHA, 0xFF); 1635 anim.setDuration(CLICK_ANIM_DURATION); 1636 anim.setInterpolator(new AccelerateDecelerateInterpolator()); 1637 anim.addListener(new AnimatorListenerAdapter() { 1638 @Override 1639 public void onAnimationEnd(Animator animation) { 1640 performItemClick(child, position, id); 1641 } 1642 }); 1643 anim.start(); 1644 1645 if (mClickAnimation != null) { 1646 mClickAnimation.cancel(); 1647 } 1648 mClickAnimation = anim; 1649 } 1650 1651 private void clearPressedItem() { 1652 mDrawsInPressedState = false; 1653 setPressed(false); 1654 updateSelectorState(); 1655 1656 final View motionView = getChildAt(mMotionPosition - mFirstPosition); 1657 if (motionView != null) { 1658 motionView.setPressed(false); 1659 } 1660 1661 if (mClickAnimation != null) { 1662 mClickAnimation.cancel(); 1663 mClickAnimation = null; 1664 } 1665 } 1666 1667 private void setPressedItem(View child, int position, float x, float y) { 1668 mDrawsInPressedState = true; 1669 1670 // Ordering is essential. First, update the container's pressed state. 1671 drawableHotspotChanged(x, y); 1672 if (!isPressed()) { 1673 setPressed(true); 1674 } 1675 1676 // Next, run layout if we need to stabilize child positions. 1677 if (mDataChanged) { 1678 layoutChildren(); 1679 } 1680 1681 // Manage the pressed view based on motion position. This allows us to 1682 // play nicely with actual touch and scroll events. 1683 final View motionView = getChildAt(mMotionPosition - mFirstPosition); 1684 if (motionView != null && motionView != child && motionView.isPressed()) { 1685 motionView.setPressed(false); 1686 } 1687 mMotionPosition = position; 1688 1689 // Offset for child coordinates. 1690 final float childX = x - child.getLeft(); 1691 final float childY = y - child.getTop(); 1692 child.drawableHotspotChanged(childX, childY); 1693 if (!child.isPressed()) { 1694 child.setPressed(true); 1695 } 1696 1697 // Ensure that keyboard focus starts from the last touched position. 1698 setSelectedPositionInt(position); 1699 positionSelectorLikeTouch(position, child, x, y); 1700 1701 // Refresh the drawable state to reflect the new pressed state, 1702 // which will also update the selector state. 1703 refreshDrawableState(); 1704 1705 if (mClickAnimation != null) { 1706 mClickAnimation.cancel(); 1707 mClickAnimation = null; 1708 } 1709 } 1710 1711 @Override 1712 boolean touchModeDrawsInPressedState() { 1713 return mDrawsInPressedState || super.touchModeDrawsInPressedState(); 1714 } 1715 1716 /** 1717 * <p>Avoids jarring scrolling effect by ensuring that list elements 1718 * made of a text view fit on a single line.</p> 1719 * 1720 * @param position the item index in the list to get a view for 1721 * @return the view for the specified item 1722 */ 1723 @Override 1724 View obtainView(int position, boolean[] isScrap) { 1725 View view = super.obtainView(position, isScrap); 1726 1727 if (view instanceof TextView) { 1728 ((TextView) view).setHorizontallyScrolling(true); 1729 } 1730 1731 return view; 1732 } 1733 1734 @Override 1735 public boolean isInTouchMode() { 1736 // WARNING: Please read the comment where mListSelectionHidden is declared 1737 return (mHijackFocus && mListSelectionHidden) || super.isInTouchMode(); 1738 } 1739 1740 /** 1741 * <p>Returns the focus state in the drop down.</p> 1742 * 1743 * @return true always if hijacking focus 1744 */ 1745 @Override 1746 public boolean hasWindowFocus() { 1747 return mHijackFocus || super.hasWindowFocus(); 1748 } 1749 1750 /** 1751 * <p>Returns the focus state in the drop down.</p> 1752 * 1753 * @return true always if hijacking focus 1754 */ 1755 @Override 1756 public boolean isFocused() { 1757 return mHijackFocus || super.isFocused(); 1758 } 1759 1760 /** 1761 * <p>Returns the focus state in the drop down.</p> 1762 * 1763 * @return true always if hijacking focus 1764 */ 1765 @Override 1766 public boolean hasFocus() { 1767 return mHijackFocus || super.hasFocus(); 1768 } 1769 } 1770 1771 private class PopupDataSetObserver extends DataSetObserver { 1772 @Override 1773 public void onChanged() { 1774 if (isShowing()) { 1775 // Resize the popup to fit new content 1776 show(); 1777 } 1778 } 1779 1780 @Override 1781 public void onInvalidated() { 1782 dismiss(); 1783 } 1784 } 1785 1786 private class ListSelectorHider implements Runnable { 1787 public void run() { 1788 clearListSelection(); 1789 } 1790 } 1791 1792 private class ResizePopupRunnable implements Runnable { 1793 public void run() { 1794 if (mDropDownList != null && mDropDownList.isAttachedToWindow() 1795 && mDropDownList.getCount() > mDropDownList.getChildCount() 1796 && mDropDownList.getChildCount() <= mListItemExpandMaximum) { 1797 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); 1798 show(); 1799 } 1800 } 1801 } 1802 1803 private class PopupTouchInterceptor implements OnTouchListener { 1804 public boolean onTouch(View v, MotionEvent event) { 1805 final int action = event.getAction(); 1806 final int x = (int) event.getX(); 1807 final int y = (int) event.getY(); 1808 1809 if (action == MotionEvent.ACTION_DOWN && 1810 mPopup != null && mPopup.isShowing() && 1811 (x >= 0 && x < mPopup.getWidth() && y >= 0 && y < mPopup.getHeight())) { 1812 mHandler.postDelayed(mResizePopupRunnable, EXPAND_LIST_TIMEOUT); 1813 } else if (action == MotionEvent.ACTION_UP) { 1814 mHandler.removeCallbacks(mResizePopupRunnable); 1815 } 1816 return false; 1817 } 1818 } 1819 1820 private class PopupScrollListener implements ListView.OnScrollListener { 1821 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 1822 int totalItemCount) { 1823 1824 } 1825 1826 public void onScrollStateChanged(AbsListView view, int scrollState) { 1827 if (scrollState == SCROLL_STATE_TOUCH_SCROLL && 1828 !isInputMethodNotNeeded() && mPopup.getContentView() != null) { 1829 mHandler.removeCallbacks(mResizePopupRunnable); 1830 mResizePopupRunnable.run(); 1831 } 1832 } 1833 } 1834} 1835