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