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