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